Skip to content

Commit 2770f37

Browse files
authored
Merge pull request #28 from codebridger/claude/jovial-pascal-7c8a59
feat(auth): dev-gated password login + MCP-driven CCW agent workflow #86exrkrra
2 parents 85d40b5 + b4b6e46 commit 2770f37

11 files changed

Lines changed: 774 additions & 20 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ UNINSTALL_FORM_URL=
66
SUBTURTLE_API_URL=
77
SUBTURTLE_DASHBOARD_URL=
88
GOOGLE_OAUTH_CLIENT_ID=
9+
# dev/agent only — set to "true" in CCW or local dev builds to expose the
10+
# email+password form in the popup. Leave empty/false for stable + dev release
11+
# builds (form stays hidden, real users continue to use Google OAuth).
12+
ENABLE_PASSWORD_AUTH=

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ jobs:
8888
SUBTURTLE_API_URL=http://localhost:4173
8989
SUBTURTLE_DASHBOARD_URL=http://localhost:4173/_dashboard_stub
9090
GOOGLE_OAUTH_CLIENT_ID=ci_e2e_stub_oauth_client
91+
ENABLE_PASSWORD_AUTH=true
9192
EOF
9293
9394
- name: Build extension
@@ -169,6 +170,7 @@ jobs:
169170
SUBTURTLE_API_URL=${SUBTURTLE_API_URL}
170171
SUBTURTLE_DASHBOARD_URL=${SUBTURTLE_DASHBOARD_URL}
171172
GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
173+
ENABLE_PASSWORD_AUTH=false
172174
EOF
173175
174176
- name: Bump versions for build

.mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"chrome-extension-tester": {
4+
"command": "npx",
5+
"args": ["-y", "chrome-extension-tester-mcp@^2.1"]
6+
}
7+
}
8+
}

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# [1.12.0](https://github.com/codebridger/subturtle-extension-apps/compare/v1.11.2...v1.12.0) (2026-05-26)
2+
3+
4+
### Bug Fixes
5+
6+
* **save-modal:** break circular import to console-crane store ([ab00130](https://github.com/codebridger/subturtle-extension-apps/commit/ab00130647d72703147f8a96df6faa8e3b1fbbe5))
7+
* **save-modal:** refetch bundle options so post-save chip shows title ([25499da](https://github.com/codebridger/subturtle-extension-apps/commit/25499da35925bebeb854f20973c3ae39b4dbd6f2))
8+
9+
10+
### Features
11+
12+
* announce extension presence on dashboard origins for install nudge [#86](https://github.com/codebridger/subturtle-extension-apps/issues/86)exkh0z3 ([69dcf1b](https://github.com/codebridger/subturtle-extension-apps/commit/69dcf1bc0c13f293c01592545ec2ccb1111d5f3e)), closes [#86exkh0z3](https://github.com/codebridger/subturtle-extension-apps/issues/86exkh0z3)
13+
* **console-crane:** practice + flashcard-preview pages, near-translation actions ([224b9da](https://github.com/codebridger/subturtle-extension-apps/commit/224b9da79e8b9d2f094ae3f63b1f8524fcb78299))
14+
* **practice-now:** emphasize practiced phrase + cover login flows ([8ff3408](https://github.com/codebridger/subturtle-extension-apps/commit/8ff340831d3ca35dcbc97ddfcf7088f7f382582b))
15+
* **practice-now:** open config to logged-out users + clearer CTAs ([2f09e05](https://github.com/codebridger/subturtle-extension-apps/commit/2f09e0577de43a55d995e9277446d313b058beef))
16+
* **practice-now:** voice session config + dashboard deep-link ([db1a3fc](https://github.com/codebridger/subturtle-extension-apps/commit/db1a3fc08f2579c2d6c958a2e8c289c95e592339))
17+
* **save-modal:** chunk highlights, AI advice chat, bundle suggestion ([9954c22](https://github.com/codebridger/subturtle-extension-apps/commit/9954c22f8822f5a06912247d0098d937c20ec6b5))
18+
* **save-modal:** in-field bundle chips with dirty-aware save + inline removal ([374cbb4](https://github.com/codebridger/subturtle-extension-apps/commit/374cbb4b0c957c5def27d6b9f0211b3ed4fe17f5))
19+
* **save-modal:** per-chunk definitions, merged pronunciation, reorder save ([f766040](https://github.com/codebridger/subturtle-extension-apps/commit/f76604023886a6d88b18831c9e492ef8253e11b7))
20+
* **saved-phrase:** DB-first lookup, reuse stored translation, no AI re-call ([1315cc8](https://github.com/codebridger/subturtle-extension-apps/commit/1315cc86c3421ab64560e68e70d0b282c662d3ba))
21+
122
## [1.11.2](https://github.com/codebridger/subturtle-extension-apps/compare/v1.11.1...v1.11.2) (2026-05-06)
223

324

CLAUDE.md

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ And the `SettingsObject` type in [src/common/types/messaging.ts](src/common/type
141141
- **The mount root in Nibble must not block the page.** Set `width: 0; height: 0; position: fixed; top: 0; left: 0`. Children use their own `position: fixed` to position themselves relative to the viewport.
142142
- **Theme dark class lives on `.subturtle-scope`, not `<html>`.** Tailwind's `dark:` rules are rewritten by `postcss-prefix-selector` to `.subturtle-scope.dark ...` — so the same element must carry both classes. The settings store handles this and a `MutationObserver` keeps Vue Teleport subtrees in sync.
143143
- **`src/stores/profile.ts` imports types from a sibling repo.** The path `../../../dashboard-app/frontend/types/database.type` resolves to a directory _next to_ this repo's root, not inside it. The actual repo is [`codebridger/subturtle-dashboard-app`](https://github.com/codebridger/subturtle-dashboard-app); local builds work because devs check both repos out side-by-side. CI clones the dashboard repo into `../dashboard-app/` before `yarn build` runs (see [.github/workflows/release.yml](.github/workflows/release.yml)). Don't try to "fix" the import to a relative-internal path or vendor the file — both will drift.
144+
- **Playwright Chromium download isn't on CCW's Trusted allowlist.** The chrome-extension-tester-mcp's `postinstall` runs `playwright install chromium`, which pulls from `cdn.playwright.dev` / `playwright.download.prss.microsoft.com`. CCW environments must use Custom network access with those hosts added — see [§ Cloud agent workflow](#cloud-agent-workflow-claude-code-on-the-web). The setup script caches Chromium into the VM snapshot so per-session cost is zero.
144145

145146
## Adding things
146147

@@ -312,6 +313,8 @@ tests/
312313
selection-popup.test.ts # @mousedown.prevent.stop regression
313314
nibble-surface.test.ts # bridge-driven hide/show
314315
translate-card.test.ts # popup translate input flow
316+
login-password.test.ts # popup password form (ENABLE_PASSWORD_AUTH=true)
317+
login-password-disabled.test.ts # popup password form hidden when flag is unset
315318
e2e/
316319
extension-fixture.ts # chromium.launchPersistentContext + extension load
317320
server.mjs # static fixtures HTTP server
@@ -320,12 +323,132 @@ tests/
320323
nibble-flow.spec.ts # content script mounting + Persian emitOpen
321324
console-crane-lifecycle.spec.ts # modal stays open while Nibble toggles off
322325
translate-flow.spec.ts # full Persian translate-and-save with page.route stubs
326+
password-login.spec.ts # popup password form end-to-end with stubbed /user/login
323327
visual-scale.spec.ts # rem→px rewrite regression net
324328
```
325329

326330
### Test totals
327331

328-
79 unit / component tests across 9 files; 11 E2E specs across 5 files. Full suite runs in ~15s once Playwright's Chromium is warm.
332+
138 unit / component tests across 19 files; 16 E2E specs across 6 files. Full suite runs in ~20s once Playwright's Chromium is warm.
333+
334+
## Cloud agent workflow (Claude Code on the Web)
335+
336+
Lets a cloud Claude agent on [Claude Code on the Web (CCW)](https://code.claude.com/docs/en/web-quickstart) clone the repo, build the extension, install it into a headless Chromium, log in with username/password against the live dev server at `https://dev.dashboard.subturtle.app/`, and drive popup + content scripts with screenshots — no Google OAuth, no local backend.
337+
338+
The agent path uses **only** the [chrome-extension-tester-mcp](https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp) MCP server (declared in [.mcp.json](.mcp.json)). It is independent of the `tests/e2e/` Playwright fixture and shares no code with it; CI verify runs the spec, the agent runs the MCP.
339+
340+
### One-time CCW environment setup
341+
342+
Done once per CCW environment from [claude.ai/code](https://claude.ai/code) — these settings live in the cloud UI, not the repo.
343+
344+
**Network access:** `Custom`, keeping the Trusted defaults plus Playwright's Chromium-download hosts. Without these, the setup script's `npm install -g chrome-extension-tester-mcp` hangs while pulling Chromium:
345+
```
346+
cdn.playwright.dev
347+
playwright.download.prss.microsoft.com
348+
```
349+
350+
**Environment variables** (`.env` format, no quotes):
351+
```
352+
ENABLE_PASSWORD_AUTH=true
353+
SUBTURTLE_API_URL=https://dev.dashboard.subturtle.app
354+
SUBTURTLE_DASHBOARD_URL=https://dev.dashboard.subturtle.app
355+
AGENT_EMAIL=<provided by user>
356+
AGENT_PASSWORD=<provided by user>
357+
# stubs sufficient for the build, not used by the agent flow:
358+
MIXPANEL_PROJECT_TOKEN=dev_stub
359+
MIXPANEL_API_HOST=https://api-js.mixpanel.com
360+
GOOGLE_TRANSLATE_KEY=dev_stub
361+
GOOGLE_TRANSLATE_PROXY_URL=https://translate.googleapis.com
362+
UNINSTALL_FORM_URL=https://example.com/uninstall
363+
GOOGLE_OAUTH_CLIENT_ID=dev_stub
364+
```
365+
366+
`AGENT_EMAIL` / `AGENT_PASSWORD` are not consumed by the build — they exist so the agent's Bash step can reference `$AGENT_EMAIL` / `$AGENT_PASSWORD` without hardcoding into the prompt. They must match an account that exists on the dev server (the agent does not register; the human or dashboard team seeds the account).
367+
368+
**Setup script** (runs as root on Ubuntu 24.04, cached as a VM snapshot — first session ~5 min, subsequent ones reuse the cache):
369+
```bash
370+
#!/bin/bash
371+
set -e
372+
373+
cd "${CLAUDE_PROJECT_DIR:-/workspace}"
374+
375+
# 1. Install extension deps.
376+
yarn install --frozen-lockfile
377+
378+
# 2. Materialize .env.production from CCW env vars (dotenv-webpack's safe:true
379+
# requires every key in .env.example to be present at build time).
380+
cat > .env.production <<EOF
381+
MIXPANEL_PROJECT_TOKEN=${MIXPANEL_PROJECT_TOKEN}
382+
MIXPANEL_API_HOST=${MIXPANEL_API_HOST}
383+
GOOGLE_TRANSLATE_KEY=${GOOGLE_TRANSLATE_KEY}
384+
GOOGLE_TRANSLATE_PROXY_URL=${GOOGLE_TRANSLATE_PROXY_URL}
385+
UNINSTALL_FORM_URL=${UNINSTALL_FORM_URL}
386+
SUBTURTLE_API_URL=${SUBTURTLE_API_URL}
387+
SUBTURTLE_DASHBOARD_URL=${SUBTURTLE_DASHBOARD_URL}
388+
GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
389+
ENABLE_PASSWORD_AUTH=${ENABLE_PASSWORD_AUTH}
390+
EOF
391+
392+
# 3. Build the extension once into dist/.
393+
NODE_ENV=production yarn build
394+
395+
# 4. Install the MCP globally so Playwright Chromium is downloaded once
396+
# into the cached snapshot. npx in .mcp.json resolves to this install.
397+
npm install -g chrome-extension-tester-mcp@^2.1
398+
```
399+
400+
### Driving the extension via the MCP
401+
402+
Once the environment is set up and the agent session starts, the cached snapshot already has `dist/` built and Playwright Chromium installed. The full login + screenshot loop becomes:
403+
404+
```
405+
# 1. Load the unpacked extension into headless Chromium.
406+
load_extension({ extension_path: "$PWD/dist" })
407+
408+
# 2. Get a JWT from the dev server with the credentials in CCW env vars.
409+
# modular-rest's authentication.login POSTs to /user/login.
410+
# The password MUST be base64-encoded — modular-rest's client library
411+
# does this internally, but raw curl has to pre-encode. Without it
412+
# the server returns HTTP 412 {"status":"error","e":{}}.
413+
PW_B64=$(printf '%s' "$AGENT_PASSWORD" | base64)
414+
curl -sX POST "$SUBTURTLE_API_URL/user/login" \
415+
-H 'Content-Type: application/json' \
416+
-d "{\"idType\":\"email\",\"id\":\"$AGENT_EMAIL\",\"password\":\"$PW_B64\"}"
417+
# → { "status": "success", "token": "<jwt>" }
418+
419+
# 3. Inject the JWT into chrome.storage.sync — same slot background.ts:62
420+
# reads on every load. The extension is now "logged in".
421+
extension_storage({ action: "set", area: "sync", data: { token: "<jwt>" } })
422+
423+
# 4. Open the popup and screenshot the logged-in view.
424+
interact_with_popup({ action: "open" })
425+
take_screenshot({ output_path: ".agent/popup.png" })
426+
# Expected: "Logged In Successfully!" view (LoginView.vue:57-68 v-else branch).
427+
```
428+
429+
The MCP exposes 14 tools — others worth knowing about: `inspect_dom` (eval JS in a page), `monitor_network` (capture requests during navigation), `send_message_to_background` (drive `chrome.runtime.onMessage` listeners), `get_service_worker_logs` (read background SW console output), `run_assertion` (returns structured PASS/FAIL). Full reference: the [chrome-extension-tester-mcp README](https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp).
430+
431+
### Why password auth exists in this build
432+
433+
`ENABLE_PASSWORD_AUTH` gates the email + password form in [src/popup/views/LoginView.vue](src/popup/views/LoginView.vue) at build time via `dotenv-webpack`. CCW builds set it true so the agent (or a human dev) can log in by typing credentials; stable + dev release builds in [.github/workflows/release.yml](.github/workflows/release.yml) set it false so production users see only Google OAuth. The agent's direct-API path doesn't need the UI, but the UI is what makes manual testing possible.
434+
435+
### How auth works under the hood
436+
437+
The agent's direct-API path POSTs `/user/login` (via `curl`) and gets a JWT. Injecting that JWT into `chrome.storage.sync["token"]` (via the MCP's `extension_storage` tool) lands it in the *same slot* the post-OAuth `StoreUserTokenMessage` path uses — see [src/background.ts:62](src/background.ts) for the read side. modular-rest's client, the profile store, the translate service, and ConsoleCrane all see no difference between an OAuth-issued token and a password-issued token.
438+
439+
### Local dev fallback (no CCW, no MCP)
440+
441+
```bash
442+
echo "ENABLE_PASSWORD_AUTH=true" >> .env.production # plus the other 8 keys
443+
yarn build && yarn dev # webpack --watch
444+
# Load dist/ at chrome://extensions, click the extension icon, use the form.
445+
```
446+
447+
The popup form drives `authentication.login` from `@modular-rest/client`, hitting whatever `SUBTURTLE_API_URL` points at. No MCP, no Playwright — just the same UI a real user would see.
448+
449+
### Boundary
450+
451+
The agent path uses only the MCP. `tests/e2e/` is the testing ground for CI verify and stays untouched by agent tooling; the two never share code. If you need to add a new agent capability, route it through the MCP's tools or a new MCP — not through the Playwright fixture.
329452

330453
## Verification checklist
331454

@@ -341,6 +464,7 @@ Automated:
341464
- Per-host Nibble toggle persists and normalizes (`www.` strip, case fold, dedup). → [tests/settings-host.test.ts](tests/settings-host.test.ts).
342465
- ConsoleCrane on Persian / CJK / emoji inputs throws no `InvalidCharacterError` from `btoa` — covered at the encode level, the bridge level, and the full select-and-save flow. → [tests/route-params.test.ts](tests/route-params.test.ts), [tests/e2e/nibble-flow.spec.ts](tests/e2e/nibble-flow.spec.ts), [tests/e2e/translate-flow.spec.ts](tests/e2e/translate-flow.spec.ts).
343466
- Visual scale is consistent on default-html-fontsize and 24px-html-fontsize hosts (postcss `rem→px` rewrite regression net). → [tests/e2e/visual-scale.spec.ts](tests/e2e/visual-scale.spec.ts).
467+
- Password login form: build-flag gating, validation, success path lands JWT in `chrome.storage.sync["token"]`, 401 surfaces inline error. → [tests/login-password.test.ts](tests/login-password.test.ts), [tests/login-password-disabled.test.ts](tests/login-password-disabled.test.ts), [tests/e2e/password-login.spec.ts](tests/e2e/password-login.spec.ts).
344468

345469
Still manual:
346470

@@ -374,3 +498,5 @@ Still manual:
374498
- Playwright fixtures server: [tests/e2e/server.mjs](tests/e2e/server.mjs)
375499
- Typecheck wrapper (with upstream-error filter): [scripts/typecheck.mjs](scripts/typecheck.mjs)
376500
- Vue 3 SFC ambient declaration: [src/vue-shim.d.ts](src/vue-shim.d.ts)
501+
- MCP server config (chrome-extension-tester for CCW): [.mcp.json](.mcp.json)
502+
- Popup LoginView (password form + OAuth buttons): [src/popup/views/LoginView.vue](src/popup/views/LoginView.vue)

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "subturtle-extension",
3-
"version": "1.12.0-dev.3",
3+
"version": "1.12.0",
44
"private": true,
55
"scripts": {
66
"dev": "webpack --watch",
@@ -60,4 +60,4 @@
6060
"vue": "3.5.17",
6161
"vue-router": "4.5.1"
6262
}
63-
}
63+
}

0 commit comments

Comments
 (0)