Skip to content
Merged
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
72 changes: 72 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,75 @@ jobs:

- name: Build
run: npm run build

# Cross-browser smoke verification of assess() in real engines. The unit
# suite is jsdom-only (no Worker → the DevTools debugger detector never runs);
# this proves the detection pipeline survives in Chromium and Firefox.
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: e2e/package-lock.json

- name: Install e2e dependencies
working-directory: e2e
run: npm ci

- name: Install Playwright browsers
working-directory: e2e
run: npx playwright install --with-deps chromium firefox

- name: Run cross-browser smoke (Chromium + Firefox)
working-directory: e2e
run: npm test

- name: Upload report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-linux
path: e2e/playwright-report
retention-days: 7

# WebKit (Safari engine) cannot run under Playwright on Linux, so it is
# verified on a macOS runner — the engine where timing-based DevTools
# heuristics diverge most.
e2e-webkit:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: e2e/package-lock.json

- name: Install e2e dependencies
working-directory: e2e
run: npm ci

- name: Install WebKit
working-directory: e2e
run: npx playwright install --with-deps webkit

- name: Run cross-browser smoke (WebKit)
working-directory: e2e
run: npx playwright test --project=webkit

- name: Upload report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-webkit
path: e2e/playwright-report
retention-days: 7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ coverage/
*.tgz
.env

# Playwright (e2e) output
e2e/test-results/
e2e/playwright-report/

16 changes: 13 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

These changes don't ship in `dist/` — they affect the dev tree, CI infra, and
internal architecture only. No version bump warranted until a consumer-visible
change lands; rolled up into the next release notes when it does.
A consumer-visible fix now lands (see **Fixed** below), so the next release
should be a patch (`0.1.1`). The Changed/Internal/Chore items — which only
affect the dev tree, CI, and internal architecture — roll up into it.

### Fixed
- **Self-referential `@/` import leaked into published `dist/`.** `dist/otel.js`
imported `@/core/index.js` (and `@/types/index.js`) — shield's internal
TypeScript path alias, which `tsc` does **not** rewrite to a relative path on
emit. This broke `attachShieldToSpan` for any consumer bundling shield without
replicating the alias (Webpack/Next, Vite, etc.): `Module not found: Can't
resolve '@/core/index.js'`. `src/otel.ts` (the only file that used the alias)
now imports relatively, so `dist/` is alias-free. Surfaced while wiring shield
into the tindalabs.dev static build.

### Changed
- **`engines.node` pinned to `>=20.0.0`** in `package.json`. Matches what CI tests against; pre-empts the `EBADENGINE` warning that dev-dep updates have started emitting on Node 18 and below. Mild consumer-facing change (npm warns at install time on older Node) — wouldn't justify a version bump on its own, but worth flagging in the next release notes.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ The cross-cutting finding: the product is built, distribution is the bottleneck,

### Next Sprint (1–4 weeks)

- [ ] **Cross-browser Playwright smoke job in CI.** One `assess()` run against Chrome + Firefox + WebKit per PR. Today the detection logic is jsdom-verified only — a Chrome update can silently turn timing detectors into false negatives. This is the biggest single credibility / reliability gap.
- [x] **Cross-browser Playwright smoke job in CI.** ✅ Added the `e2e/` package: a Vite-built fixture loads Shield's source and exposes `assess()` on `window`; the spec drives it via `page.evaluate` and asserts result shape, the `navigator.webdriver` signal mirrors the live engine, a forced `__REACT_DEVTOOLS_GLOBAL_HOOK__` signature composes risk → flags → `spanAttributes` end-to-end, and a clean session stays lean. Crucially this exercises the **Worker-based DevTools debugger detector that jsdom cannot run** (the path pinned at 23% unit coverage). CI runs Chromium + Firefox on `ubuntu-latest` and **WebKit on `macos-latest`** (Playwright's WebKit throws an "internal error" on Linux regardless of the static server — confirmed locally — so it is gated off Linux and verified natively on macOS, the engine where timing heuristics diverge most). 8/8 green on Chromium + Firefox locally.
- [ ] **Write "Migrating from disable-devtool" guide** (carried over from the May 19 backlog; still the highest-leverage CMO action). Title for the search query: "Migrating from disable-devtool to a structured, OTel-native tamper detection library." Cross-post to Dev.to and r/javascript.
- [ ] **Host the `demo/` SPA publicly** (Vercel / GitHub Pages / `shield.tindalabs.dev`) with a "copy as JSON" button on the output. Currently localhost-only — the most shareable asset Shield has is unreachable to prospects.
- [ ] **Add bundle-size badge** to README + CI (`size-limit` or `bundlejs`). "Zero deps" sells; "X kB gzipped" closes.
Expand Down
40 changes: 40 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# @tindalabs/shield-e2e

Cross-browser smoke verification of `assess()` in **real** engines.

The unit suite (`src/tests`) runs in jsdom, which has no `Worker` — so the
DevTools debugger detector and the engine-specific timing/navigator behaviour
are never exercised there. This package runs the detection pipeline in
Chromium, Firefox and WebKit so a browser update can't silently turn a detector
into a false negative without CI noticing.

## How it works

- `fixture/` — a minimal page built by Vite that imports Shield's source (same
alias the `demo/` app uses) and exposes `assess()` on `window.Shield`.
- `serve.mjs` — a zero-dependency static server for the built fixture. (Used
instead of `vite preview` because Playwright's WebKit on Linux cannot reach
Vite's server.)
- `tests/assess.spec.ts` — drives `assess()` via `page.evaluate` and asserts:
result shape, the `shield.automation.webdriver` signal mirrors the live
`navigator.webdriver`, a forced extension signature composes risk →
`spanAttributes` end-to-end, and a clean session stays lean.

## Run locally

```bash
cd e2e
npm install
npm run install-browsers # chromium firefox webkit
npm test # Chromium + Firefox (WebKit auto-runs on macOS)
```

WebKit is omitted automatically on Linux (Playwright's WebKit throws an
"internal error" there). On macOS `npm test` includes it; in CI it runs on a
dedicated `macos-latest` job.

## CI

`.github/workflows/ci.yml` runs the `e2e` job (Chromium + Firefox on
`ubuntu-latest`) and the `e2e-webkit` job (WebKit on `macos-latest`) on every
push and PR to `main`.
13 changes: 13 additions & 0 deletions e2e/fixture/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shield — E2E fixture</title>
</head>
<body>
<div id="status">loading</div>
<p id="protect-me">Selectable content used by protection smoke checks.</p>
<script type="module" src="./main.ts"></script>
</body>
</html>
12 changes: 12 additions & 0 deletions e2e/fixture/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { assess } from '@tindalabs/shield';

// The fixture is a thin loader: it exposes the public API on `window` so the
// Playwright specs drive assess() via page.evaluate() in the real engine.
declare global {
interface Window {
Shield: { assess: typeof assess };
}
}

window.Shield = { assess };
document.getElementById('status')!.textContent = 'ready';
Loading
Loading