diff --git a/RELEASE_SMOKE.md b/RELEASE_SMOKE.md new file mode 100644 index 0000000..a397aaa --- /dev/null +++ b/RELEASE_SMOKE.md @@ -0,0 +1,107 @@ +# Release Smoke Checklist + +Repeatable verification before Chrome Web Store submission. Run after every release candidate build. + +## Quick start + +```bash +npm run smoke +``` + +This runs all **automated** checks (build, test, lint, dist verification, metadata-only compliance) and prints the **manual** Chrome steps below. + +--- + +## Automated checks + +| Step | Command | Pass criteria | +|------|---------|---------------| +| Type-check | `tsc --noEmit` | No TypeScript errors | +| Lint | `eslint src/**/*.{js,ts}` | No lint errors | +| Unit tests | `jest` | All tests pass (includes metadata-only compliance) | +| Production build | `webpack --mode production` | `dist/` created without errors | +| Dist artifacts | `scripts/release-smoke.sh` | manifest, background, popup, tracking, icons present | +| MV3 manifest | smoke script | `manifest_version: 3`, required permissions | +| Metadata-only | smoke script + `tests/metadata-only.test.ts` | No PDF bytes persisted to `chrome.storage` | + +--- + +## Manual Chrome verification + +Requires Chrome desktop and Binect test credentials. + +### 1. Load unpacked extension + +1. Run `npm run build` (or `npm run smoke` which builds automatically). +2. Open `chrome://extensions/`. +3. Enable **Developer mode**. +4. Click **Load unpacked** and select the `dist/` directory. +5. Confirm BinectChrome appears with version from `public/manifest.json`. +6. Accept any new permission prompts. + +**Pass:** Extension loads without service-worker registration errors. Click **service worker** — console shows `BinectChrome service worker loaded`. + +### 2. Trigger PDF detection + +**Recommended (viewer detection):** + +1. Open: `https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf` +2. Click the BinectChrome toolbar icon. + +**Pass:** Popup shows `dummy.pdf`, source domain, and **Send PDF to Binect** button. + +**Optional (download detection):** + +1. Download: `https://www.africau.edu/images/default/sample.pdf` +2. Check service worker console for `[PDF Detector] PDF detected!` +3. Badge shows `1` (when service worker is awake). + +**Pass:** Popup finds the PDF (directly or via recent-downloads fallback). + +### 3. Verify Binect upload (metadata-only path) + +1. Sign in with Binect credentials in the popup. +2. Click **Send PDF to Binect**. +3. Wait for **Uploading…** → **Success!** with a document ID. + +**Pass:** Upload completes; popup shows document in basket (`in_basket` status). + +### 4. Confirm zero-retention storage + +1. Right-click popup → **Inspect** → **Application** → **Storage** → **Extension Storage**. +2. Inspect stored keys: + +| Key | Expected content | +|-----|------------------| +| `documentProxies` | Filename, URL, size, hash, Binect status — **no PDF bytes** | +| `credentials` | Encrypted username/password — **no PDF data** | +| `transferTracking` | Timestamp, domain, size, result — **no PDF content** | + +**Pass:** No key contains base64 PDF content or raw byte arrays. Only metadata and encrypted credentials. + +### 5. Tracking page + +1. Click **?** in the popup footer. +2. Confirm the transfer appears with timestamp, domain, size, and success/failure. + +**Pass:** Transfer logged locally; CSV export works. + +--- + +## Failure triage + +| Symptom | Check | +|---------|-------| +| Service worker won't register | `dist/background.js` exists; reload extension | +| PDF not detected | Try viewer URL first; check popup console | +| Upload fails | Credentials, network, popup Network tab for `api.binect.de` | +| Storage has PDF bytes | **Blocker** — violates zero-retention; do not ship | + +--- + +## Related docs + +- `QUICK_TEST_GUIDE.md` — detailed debug steps +- `DEVELOPMENT.md` — dev workflow and Chrome URLs +- `SCOPE.md` — in-scope metadata-only proxy model +- `dev-helper.sh verify` — quick dist check without full smoke \ No newline at end of file diff --git a/dev-helper.sh b/dev-helper.sh index ff68f0c..8b5728f 100755 --- a/dev-helper.sh +++ b/dev-helper.sh @@ -36,6 +36,7 @@ usage() { echo " open-chrome - Open Chrome extension management" echo " open-sw - Open Chrome service worker internals" echo " verify - Verify dist build is valid" + echo " smoke - Full release smoke (build, test, lint, dist, metadata-only)" echo " help - Show this help" echo "" echo "Quick Test-Fix Loop:" @@ -184,6 +185,9 @@ case "${1:-}" in verify) verify ;; + smoke) + bash "$(dirname "$0")/scripts/release-smoke.sh" + ;; help|--help|-h) usage ;; diff --git a/package.json b/package.json index 52366dd..dbf02fa 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:watch": "jest --watch", "lint": "eslint src/**/*.{js,ts}", "lint:fix": "eslint src/**/*.{js,ts} --fix", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "smoke": "bash scripts/release-smoke.sh" }, "keywords": [ "chrome-extension", diff --git a/scripts/release-smoke.sh b/scripts/release-smoke.sh new file mode 100755 index 0000000..0840e5c --- /dev/null +++ b/scripts/release-smoke.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Release smoke path — automated pre-flight checks before Chrome manual verification. +# Run: npm run smoke or ./scripts/release-smoke.sh +# +# Automates: install, type-check, lint, test, production build, dist verification, +# and static metadata-only compliance checks. +# Manual Chrome steps are printed at the end (see RELEASE_SMOKE.md). + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST="$ROOT/dist" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +step=0 +pass() { echo -e "${GREEN}✅ [$((++step))] $1${NC}"; } +fail() { echo -e "${RED}❌ $1${NC}"; exit 1; } +info() { echo -e "${BLUE} $1${NC}"; } +warn() { echo -e "${YELLOW}⚠️ $1${NC}"; } + +echo "" +echo "BinectChrome — Release Smoke (automated)" +echo "========================================" +echo "" + +cd "$ROOT" + +# --- Prerequisites --- +if ! command -v node >/dev/null 2>&1; then + fail "Node.js is required" +fi +if ! command -v npm >/dev/null 2>&1; then + fail "npm is required" +fi +if [ ! -d "../binect-js" ]; then + fail "Sibling repo ../binect-js not found — required for @binect/js dependency" +fi +pass "Prerequisites (node, npm, binect-js)" + +# --- Dependencies --- +if [ ! -d node_modules ] || [ ! -f node_modules/.bin/jest ]; then + info "Installing dependencies..." + npm install +fi +pass "Dependencies ready" + +# --- Static analysis --- +info "Running type-check..." +npm run type-check +pass "Type-check" + +info "Running lint..." +npm run lint +pass "Lint" + +info "Running unit tests..." +npm test +pass "Unit tests (24+)" + +# --- Production build --- +info "Building production dist/..." +npm run build +pass "Production build" + +# --- Dist verification --- +REQUIRED=( + "$DIST/manifest.json" + "$DIST/background.js" + "$DIST/popup.html" + "$DIST/popup.js" + "$DIST/tracking.html" + "$DIST/tracking.js" + "$DIST/icons/icon-128.png" + "$DIST/_locales/en/messages.json" +) +for f in "${REQUIRED[@]}"; do + [ -f "$f" ] || fail "Missing dist artifact: $f" +done + +MANIFEST="$DIST/manifest.json" +grep -q '"manifest_version": 3' "$MANIFEST" || fail "manifest.json must be MV3" +for perm in downloads storage alarms activeTab; do + grep -q "\"$perm\"" "$MANIFEST" || fail "manifest.json missing permission: $perm" +done +grep -q 'https://api.binect.de' "$MANIFEST" || fail "manifest.json missing Binect API host permission" + +VERSION=$(grep -o '"version": "[^"]*"' "$MANIFEST" | head -1 | cut -d'"' -f4) +pass "Dist verification (v${VERSION})" + +# --- Metadata-only compliance (static) --- +info "Checking metadata-only storage compliance..." + +# Proxies must not define PDF content fields +if grep -qE 'pdfContent|pdfBytes|contentBase64|base64Content' "$ROOT/src/utils/pdf-queue.ts"; then + fail "pdf-queue.ts must not store PDF content fields" +fi + +# No chrome.storage writes of PDF byte data in src/ (same-line check) +STORAGE_OFFENDERS=$(rg -n 'chrome\.storage.*\.set' "$ROOT/src" \ + | rg 'pdfBytes|pdfData|base64Content|pdfContent' || true) +if [ -n "$STORAGE_OFFENDERS" ]; then + fail "Source persists PDF bytes to chrome.storage — violates zero-retention" +fi + +# Upload path: bytes fetched in-memory only, not written to storage +rg -q 'fetchPDFBytes' "$ROOT/src/popup/popup.ts" \ + || fail "popup.ts must fetch PDF bytes at send time" +rg -q 'uploadPDF' "$ROOT/src/popup/popup.ts" \ + || fail "popup.ts must call uploadPDF for Binect dispatch" + +pass "Metadata-only compliance (no PDF persistence in storage)" + +# --- Summary --- +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Automated smoke checks: ALL PASSED${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "Dist ready at: $DIST" +echo " background.js $(du -h "$DIST/background.js" | cut -f1)" +echo " popup.js $(du -h "$DIST/popup.js" | cut -f1)" +echo "" +echo -e "${YELLOW}Manual Chrome verification (required before store submission):${NC}" +echo "" +echo " 1. Open chrome://extensions/ → Developer mode ON" +echo " 2. Load unpacked → select: $DIST" +echo " 3. Open test PDF:" +echo " https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" +echo " 4. Click extension icon → confirm PDF detected (filename, domain)" +echo " 5. Sign in with Binect credentials → Send PDF to Binect" +echo " 6. Confirm upload success and document ID in popup" +echo " 7. Open popup DevTools → Application → Storage → Extension Storage" +echo " Verify keys are metadata-only (documentProxies, credentials, transferTracking)" +echo " — no PDF byte content stored" +echo " 8. Open tracking page (?) → confirm transfer logged with size, not content" +echo "" +echo "Full checklist: RELEASE_SMOKE.md" +echo "" \ No newline at end of file diff --git a/src/popup/popup.ts b/src/popup/popup.ts index 85bb848..f6f176a 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -4,10 +4,10 @@ import './popup.css'; import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage'; -import { uploadPDF, testConnection, BinectAPIError, Document } from '../utils/binect-api'; +import { uploadPDF, testConnection, BinectAPIError } from '../utils/binect-api'; import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector'; import { addTrackingEntry } from '../tracking/tracker'; -import { DocumentProxy, PDFQueueEntry, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue'; +import { DocumentProxy, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue'; import { computeContentFingerprint } from '../utils/hash'; // DOM Elements diff --git a/tests/metadata-only.test.ts b/tests/metadata-only.test.ts new file mode 100644 index 0000000..4224889 --- /dev/null +++ b/tests/metadata-only.test.ts @@ -0,0 +1,103 @@ +/** + * Release smoke — metadata-only compliance checks + * + * Verifies the extension never persists PDF content to chrome.storage. + * Run as part of `npm test` and `npm run smoke`. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +const SRC = path.join(__dirname, '..', 'src'); + +const PDF_CONTENT_FIELD_PATTERN = + /\b(pdfContent|pdfBytes|contentBase64|base64Content|pdfData)\b/; + +function readSourceFiles(dir: string): string[] { + const files: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...readSourceFiles(full)); + } else if (entry.name.endsWith('.ts')) { + files.push(full); + } + } + return files; +} + +describe('metadata-only storage compliance', () => { + test('DocumentProxy queue does not define PDF content fields', () => { + const queueSource = fs.readFileSync( + path.join(SRC, 'utils', 'pdf-queue.ts'), + 'utf-8' + ); + expect(queueSource).not.toMatch(PDF_CONTENT_FIELD_PATTERN); + expect(queueSource).toContain('contentHash'); + expect(queueSource).not.toContain('pdfContent'); + }); + + test('DetectedPDF interface is metadata-only', () => { + const detectorSource = fs.readFileSync( + path.join(SRC, 'utils', 'pdf-detector.ts'), + 'utf-8' + ); + const interfaceMatch = detectorSource.match( + /export interface DetectedPDF \{([^}]+)\}/ + ); + expect(interfaceMatch).not.toBeNull(); + const fields = interfaceMatch![1]; + expect(fields).toMatch(/filename/); + expect(fields).toMatch(/url/); + expect(fields).toMatch(/size/); + expect(fields).not.toMatch(PDF_CONTENT_FIELD_PATTERN); + }); + + test('no source file persists PDF bytes via chrome.storage', () => { + const offenders: string[] = []; + for (const file of readSourceFiles(SRC)) { + const content = fs.readFileSync(file, 'utf-8'); + if (!content.includes('chrome.storage')) { + continue; + } + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if ( + line.includes('chrome.storage') && + line.includes('.set') && + PDF_CONTENT_FIELD_PATTERN.test(line) + ) { + offenders.push(`${path.relative(SRC, file)}:${i + 1}`); + } + } + } + expect(offenders).toEqual([]); + }); + + test('upload path fetches bytes in-memory and delegates to Binect API', () => { + const popupSource = fs.readFileSync( + path.join(SRC, 'popup', 'popup.ts'), + 'utf-8' + ); + expect(popupSource).toMatch(/fetchPDFBytes/); + expect(popupSource).toMatch(/uploadPDF/); + expect(popupSource).toMatch(/computeContentFingerprint/); + }); + + test('manifest declares required MV3 permissions', () => { + const manifest = JSON.parse( + fs.readFileSync( + path.join(__dirname, '..', 'public', 'manifest.json'), + 'utf-8' + ) + ); + expect(manifest.manifest_version).toBe(3); + for (const perm of ['downloads', 'storage', 'alarms', 'activeTab']) { + expect(manifest.permissions).toContain(perm); + } + expect(manifest.host_permissions).toEqual( + expect.arrayContaining(['https://api.binect.de/*']) + ); + }); +}); \ No newline at end of file diff --git a/workplans/BINECT-CHROME-WP-0002-release-smoke-path.md b/workplans/BINECT-CHROME-WP-0002-release-smoke-path.md index ac733c6..79e261f 100644 --- a/workplans/BINECT-CHROME-WP-0002-release-smoke-path.md +++ b/workplans/BINECT-CHROME-WP-0002-release-smoke-path.md @@ -4,11 +4,11 @@ type: workplan title: "Extension release smoke path" domain: communication repo: binect-chrome -status: ready +status: finished owner: codex topic_slug: communication created: "2026-06-22" -updated: "2026-06-22" +updated: "2026-06-24" state_hub_workstream_id: "085de6b3-4627-4678-a1ce-e03e4cbab445" --- @@ -20,9 +20,11 @@ Establish repeatable build, load-unpacked, and PDF-send smoke verification befor ```task id: BINECT-CHROME-WP-0002-T01 -status: todo +status: done priority: high state_hub_task_id: "436e1136-a94e-454e-b8ca-e1e2817e9796" ``` +Result 2026-06-24: Added `npm run smoke`, `scripts/release-smoke.sh`, `RELEASE_SMOKE.md`, and `tests/metadata-only.test.ts`. Automated checks cover build, test, lint, dist verification, and metadata-only compliance; manual Chrome checklist documents load-unpacked, PDF detection, and upload verification. + Document and automate smoke steps: build, load `dist/`, trigger PDF detection, verify Binect upload metadata-only path.