Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
ae7533d
feat: astro integration package, initial import
a-d-j-i May 29, 2026
bc31d09
refactor: remove dead vite-plugin from astro integration
a-d-j-i May 29, 2026
b8d0ec9
chore: update package-lock.json with astro integration
a-d-j-i May 29, 2026
339d68f
refactor: replace isNavigation flag with request.mode checks
a-d-j-i May 29, 2026
ff9e293
test: fix unit tests
a-d-j-i Jun 2, 2026
e3fc65c
Merge pull request #4 from a-d-j-i/feat/astro-integration-package
a-d-j-i Jun 2, 2026
507c4ea
test: expand unit coverageD, inject IndexedDB and add tests for fetch…
a-d-j-i May 29, 2026
a1e0d16
Merge pull request #3 from a-d-j-i/test/expand-unit-coverage
a-d-j-i Jun 2, 2026
28d6cf5
Merge pull request #2 from a-d-j-i/refactor/fetch-handler-cleanup
a-d-j-i Jun 2, 2026
6a02bb4
refactor: make dev-server a reusable startServer() factory
a-d-j-i May 29, 2026
8cb7198
Merge pull request #5 from a-d-j-i/feat/dev-server-factory
a-d-j-i Jun 2, 2026
cb70879
feat: add @dappfence/next and @dappfence/manifest-tools packages
a-d-j-i Jun 1, 2026
f5cd2f3
feat: @dappfence/next initial version for static pages
a-d-j-i Jun 2, 2026
41a0dee
test: fix manifest-service unit tests after rebase
a-d-j-i Jun 2, 2026
8df5029
fix: fix package-lock.json
a-d-j-i Jun 2, 2026
3fbe7aa
Merge pull request #1 from a-d-j-i/feat/nextjs-integration
a-d-j-i Jun 2, 2026
ece94ad
docs: add docs/ and scripts/ directories
a-d-j-i Jun 2, 2026
c82dab2
feat: port rules-based manifest verification pipeline
a-d-j-i Jun 4, 2026
6ede606
refactor: split manifest-service into loader, verifier, and compositi…
a-d-j-i Jun 5, 2026
8ddba67
Merge pull request #6 from a-d-j-i/feat/rules-manifest-port
a-d-j-i Jun 5, 2026
cc50d72
feat: emit pathRules + contentRules from integration packages
a-d-j-i Jun 5, 2026
98ddbbe
Merge pull request #8 from a-d-j-i/feat/integration-rules-format
a-d-j-i Jun 5, 2026
dc31091
Merge pull request #7 from a-d-j-i/docs/scripts-and-docs
a-d-j-i Jun 5, 2026
25a3fb6
feat: SSR hashing for @dappfence/astro (Tier 1 + Tier 2)
a-d-j-i Jun 5, 2026
7bbeba1
chore: remove astro peer dependency
a-d-j-i Jun 6, 2026
dcf7b40
Merge pull request #9 from a-d-j-i/feat/ssr-hashing
a-d-j-i Jun 6, 2026
79ea969
feat: non ok responses
a-d-j-i Jun 8, 2026
f46918f
Merge pull request #10 from a-d-j-i/feat/non-ok-response-verification
a-d-j-i Jun 8, 2026
4e2f171
feat: launch browser with proxy when --with-browser flag is used
a-d-j-i Jun 8, 2026
4bc0e5f
Merge pull request #11 from a-d-j-i/feat/browser-proxy-launch
a-d-j-i Jun 8, 2026
c413bbb
feat: skip destination="" fetches, record opaque script rewrites
a-d-j-i Jun 9, 2026
efe9d78
Merge pull request #12 from a-d-j-i/feat/destination-skip-and-rewrite…
a-d-j-i Jun 9, 2026
d398ec6
feat: not-found pathRule + multi-manifest build refactor
a-d-j-i Jun 9, 2026
b6f8857
refactor: consolidate manifest rules into rules.js, drop filters.js
a-d-j-i Jun 9, 2026
036f8f2
chore: markdown linter
a-d-j-i Jun 9, 2026
06ae1ad
Merge pull request #13 from a-d-j-i/refactor/build-multi-manifest
a-d-j-i Jun 9, 2026
f76468c
refactor: manifest escalation + rule engine cleanup
a-d-j-i Jun 11, 2026
75f6b6b
docs: remove the obsolete hash lookup references
a-d-j-i Jun 11, 2026
87df26f
fix: guard against null fallback when all manifest attempts fail
a-d-j-i Jun 11, 2026
5eadfc2
fix: test page: always send clear intercepts to the server
a-d-j-i Jun 11, 2026
fe81460
doc: comment on ignoring blocking on manifest errors
a-d-j-i Jun 11, 2026
cd168bc
chore: fix dev-server shutdown hang with closeAllConnections
a-d-j-i Jun 11, 2026
6e20c48
chore: linter/formatter
a-d-j-i Jun 11, 2026
8b55c0e
Merge pull request #14 from a-d-j-i/refactor/manifest-escalation
a-d-j-i Jun 11, 2026
5c492ed
fix: ensure recordSecurityViolation always receives complete violatio…
a-d-j-i Jun 12, 2026
fc217c3
Merge pull request #16 from a-d-j-i/fix/manifest-asset-type
a-d-j-i Jun 12, 2026
6cb24b4
fix: harden fetch handler error handling and fix undefined response
a-d-j-i Jun 12, 2026
58ce0cf
Merge pull request #17 from a-d-j-i/fix/fetch-handler-error-handling
a-d-j-i Jun 12, 2026
c6dcf59
feat: @dappfence/next SSR mode + base path support
a-d-j-i Jun 12, 2026
b62b0c6
Merge pull request #18 from a-d-j-i/feat/next-integration
a-d-j-i Jun 12, 2026
6995dfb
feat: add netlify option to @dappfence/astro and @dappfence/next
a-d-j-i Jun 16, 2026
280d5c6
Merge pull request #19 from a-d-j-i/feat/netlify-option
a-d-j-i Jun 16, 2026
94eb4b6
chore: bump packages to 0.1.1 + improve sync-versions script
a-d-j-i Jun 16, 2026
3254ac6
chore: add update-local-consumer script for tarball-based consumers
a-d-j-i Jun 16, 2026
aaf7a37
chore: linter/formatter
a-d-j-i Jun 16, 2026
384ee5b
Merge pull request #20 from a-d-j-i/chore/sync-versions
a-d-j-i Jun 16, 2026
a735508
feat: Netlify CDP hashing + workspace-aware tooling fixes
a-d-j-i Jun 16, 2026
1779c3a
Merge pull request #21 from a-d-j-i/feat/netlify-cdp-hashing
a-d-j-i Jun 16, 2026
ed5b557
chore: add run-ws.js script for sequential workspace test runs
a-d-j-i Jun 16, 2026
2e02d21
Merge pull request #22 from a-d-j-i/chore/run-ws-script
a-d-j-i Jun 16, 2026
86f5a12
fix: update buildContentRules test to expect 3 rules
a-d-j-i Jun 16, 2026
dbbae0f
Merge pull request #23 from a-d-j-i/fix/netlify-cdp-content-rules-test
a-d-j-i Jun 16, 2026
e0e55c2
ci: run all workspace unit tests in separate steps
a-d-j-i Jun 16, 2026
b61ad3b
fix: lazy-resolve @dappfence/core path in webpack plugin
a-d-j-i Jun 16, 2026
4e13b97
Merge pull request #24 from a-d-j-i/ci/add-missing-workspace-tests
a-d-j-i Jun 16, 2026
1569fba
chore: bump vite-plugin-bundle-obfuscator 1.8.0 → 1.12.0
a-d-j-i Jun 16, 2026
238bb96
chore: bump vite 7.3.2 → 8.0.16
a-d-j-i Jun 16, 2026
da71831
Merge pull request #25 from a-d-j-i/chore/package-upgrade
a-d-j-i Jun 16, 2026
60be4bb
chore: bump version to 0.1.2
a-d-j-i Jun 16, 2026
62b72ad
Merge pull request #26 from a-d-j-i/chore/version-0.1.2
a-d-j-i Jun 16, 2026
55ea7d2
chore: improve request and response logging
a-d-j-i Jun 17, 2026
1896004
Merge pull request #27 from a-d-j-i/chore/log-request-response
a-d-j-i Jun 17, 2026
2fb7211
fix: migrate expectedHash to expectedHashes array throughout
a-d-j-i Jun 24, 2026
9f26e8b
Merge pull request #29 from a-d-j-i/fix/expected-hashes
a-d-j-i Jun 24, 2026
b9173c3
feat: upgrade no-cors script requests to cors+omit
a-d-j-i Jun 18, 2026
432bbfa
Merge pull request #28 from a-d-j-i/feat/force-cors-scripts
a-d-j-i Jun 25, 2026
e56fd60
feat: introduce isExecutableDestination and broaden CORS retry
a-d-j-i Jun 26, 2026
72108c2
Merge pull request #30 from a-d-j-i/feat/executable-destinations
a-d-j-i Jun 26, 2026
f32838c
refactor: pre-fetch allow rule check, verifier rename, resolveManifes…
a-d-j-i Jun 26, 2026
6595c62
Merge pull request #31 from a-d-j-i/feat/pre-fetch-rule-check
a-d-j-i Jun 26, 2026
44342ba
feat: destination safety tests + expand INERT_DESTINATIONS
a-d-j-i Jun 29, 2026
dbc0cc7
Merge pull request #32 from a-d-j-i/feat/destination-safety-tests
a-d-j-i Jun 29, 2026
e1b78f7
refactor: add TRANSFORM/VALIDATOR constants, replace netlify-cdp stri…
a-d-j-i Jul 2, 2026
551f3c3
Merge pull request #34 from a-d-j-i/refactor/netlify-rename-and-const…
a-d-j-i Jul 2, 2026
f0cd6cf
refactor: normalizeManifestData guarantees full manifest shape
a-d-j-i Jul 2, 2026
c71c8ac
Merge pull request #35 from a-d-j-i/refactor/normalize-manifest-arrays
a-d-j-i Jul 2, 2026
d68bdac
refactor: consolidate verifier dispatch and result handling
a-d-j-i Jul 2, 2026
8cd7165
Merge pull request #36 from a-d-j-i/refactor/verifier-action-dispatch
a-d-j-i Jul 2, 2026
f8b7b25
refactor: extract transforms.js + response-wrapper; lazy body reading
a-d-j-i Jul 2, 2026
9ce8eb1
Merge pull request #37 from a-d-j-i/refactor/transforms-extraction
a-d-j-i Jul 2, 2026
70a3580
refactor: remove isTerminal from verdicts; add keepTryingActions to a…
a-d-j-i Jul 7, 2026
b092783
Merge pull request #38 from a-d-j-i/refactor/remove-is-terminal
a-d-j-i Jul 7, 2026
27b97db
refactor: separate pinning from onManifestResult; fix manifestDecided…
a-d-j-i Jul 7, 2026
bfd0514
Merge pull request #39 from a-d-j-i/refactor/pinning-and-manifest-dec…
a-d-j-i Jul 7, 2026
690d52b
fix: clone manifest response before storeManifestFromResponse
a-d-j-i Jul 16, 2026
8d9cce8
Merge pull request #42 from a-d-j-i/fix/manifest-response-clone
a-d-j-i Jul 16, 2026
f8d5d96
docs: add CSP injection strategy, collection, and verification cases
a-d-j-i Jul 16, 2026
ad32903
Merge pull request #43 from a-d-j-i/docs/csp-injection
a-d-j-i Jul 16, 2026
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
10 changes: 8 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ jobs:
node-version: 24
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Run unit tests with coverage
- name: Test core
run: npm run test:coverage -w @dappfence/core
- name: Test astro
run: npm test -w @dappfence/astro
- name: Test manifest-tools
run: npm test -w @dappfence/manifest-tools
- name: Test next
run: npm test -w @dappfence/next
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: coverage-report
path: packages/dappfence/coverage/
retention-days: 30
retention-days: 30
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ npm run dev

## Repository Structure

This is a monorepo with three packages. Each has its own README with detailed documentation:
This is a monorepo with four packages. Each has its own README with detailed documentation:

- [`packages/dappfence`](packages/dappfence/README.md) — core framework architecture, module
descriptions, manifest format, and design patterns
- [`packages/astro-integration`](packages/astro-integration/README.md) — Astro integration: script
injection, manifest generation, dynamic content tagging
- [`packages/test-app`](packages/test-app/README.md) — test scenarios, dev server configuration,
and libfaketime setup for cache expiration testing
- [`packages/signer`](packages/signer/) — manifest signing library (`signManifest`,
Expand Down Expand Up @@ -138,6 +140,8 @@ dappfence/
- `npm run build:prod` - Production build of `@dappfence/core` (minified, obfuscated)
- `npm run build:watch` - Watch mode: auto-rebuild core + manifests on source changes
- `npm run clean` - Remove all build output from every package
- `npm run publish:local` - Build production bundle and pack `@dappfence/core` as a tarball to
`dist/` for use by local examples
- `npm run check` - Run linting and formatting checks
- `npm run lint` - Run ESLint with auto-fix
- `npm run format` - Format code with Prettier
Expand All @@ -153,6 +157,28 @@ npm run test:headed -w @dappfence/test-app # E2e tests in headed browser
npm run test:debug -w @dappfence/test-app # Debug e2e tests
```

### Local Examples

The `examples/` directory contains standalone apps. They reference monorepo packages via `file:`
symlinks so changes are picked up immediately without a pack/install cycle.

```bash
# Install dependencies in the example (run from the example directory)
cd examples/astro
npm install

# Build and preview
npm run build
npm run preview
```

If you change any `packages/*` source, rebuild the relevant package before re-running the example:

```bash
# From repo root — rebuild core (required when dappfence.js source changes)
npm run build -w @dappfence/core
```

### Development Requirements

- Node.js 16.0.0 or higher
Expand Down
149 changes: 149 additions & 0 deletions docs/csp-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# CSP Hash Collection — Integration Strategy

How each integration collects inline script hashes for `csp.pages` at build time.

---

## Overview

`manifest.csp.pages` maps a page key to `{ scripts, attrs }` — the SHA-256 hashes of every inline
`<script>` block and `on*` attribute value that appears on that page. The SW injects a
`Content-Security-Policy` header using these hashes before the browser sees the response.

Keys are matched by the SW using exact-or-prefix logic:

- Exact key (`/dashboard`) — matches only that path.
- Prefix key (`/posts/`) — matches any path that starts with `/posts/`. Used for parameterised
routes where all IDs share the same static inline scripts.

Empty entries (`{ scripts: [], attrs: [] }`) are emitted for dynamic routes that have no inline
scripts. This tells the SW to inject a CSP that blocks all inline scripts on that page, which is the
correct default — the page has no known-good inline scripts to allowlist.

---

## Verification modes

These classify pages by the guarantee DappFence can provide. They are defined in
`docs/csp-injection-strategy.md`.

- **staticPages** — prerendered pages (SSG, static export). Full body hash + CSP. Any modification
to any byte in the response is detected.
- **stableInlineScripts** — SSR pages whose inline scripts are identical on every render. No body
hash (content changes per request); CSP is the primary protection.
- **dynamicRSC** — Next.js `force-dynamic` routes (opt-in). CSP blocks RSC push scripts;
`dappfence.js` re-executes them after strict pattern validation. Data injection residual
remains.

---

## SSR collection route classes

The Astro integration subdivides SSR routes by how much is knowable at build time. This drives
`extractFixedRoutes`, `extractEnumerableRoutes`, `extractProbedPatterns` in the code.

- **fixedRoute** — param-free SSR (e.g. `/live`, `/api/version`): boot the built server, fetch the
URL once, parse the HTML. Produces an exact key in `csp.pages`.
- **enumerableRoute** — parameterized SSR + `getStaticPaths()` (e.g. `/snippets/[id]`): enumerate
all valid IDs via `getStaticPaths()`, fetch each concrete URL, produce one exact key per ID.
- **probedRoute** — parameterized SSR, IDs not enumerable: fetch one sentinel probe URL
(`/posts/__probe__`), extract hashes from that response, store under a prefix key (`/posts/`).
Weakest guarantee — assumes all IDs share the same static inline scripts.

---

## Astro

### Source of dynamic routes

`astro:routes:resolved` hook — Astro passes the full resolved route list before the build runs.
`extractDynamicRoutes` filters to `!r.isPrerendered`.

### Inline script hash collection

| Route class | Route type | Technique |
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| staticPages | Prerendered HTML on disk | `generateManifest` (manifest-tools) walks `outDir` and calls `extractInlineHashesFromHtml` on every `.html` file |
| fixedRoute | SSR, no URL params (e.g. `/live`) | Build-time server: imports `server/entry.mjs`, starts it on a random port, fetches the URL, parses the HTML response |
| enumerableRoute | SSR + `getStaticPaths()` | Imports the Vite-compiled chunk from `server/chunks/`, calls `mod.page().getStaticPaths()` to enumerate concrete URLs, then same fetch+parse as fixedRoute |
| probedRoute | SSR + URL params, no enumeration (e.g. `/posts/[id]`) | Fetches a sentinel probe URL (`/posts/__probe__`), parses the response for hashes, stores them under the prefix key `/posts/` |

### Prefix key derivation

`routePatternToPrefixKey('/posts/[id]')` strips from the first `[` back to the last `/`:
`/posts/[id]` → `/posts/`, `/blog/[year]/[slug]` → `/blog/`. Exact paths pass through unchanged.

---

## Next.js

### Source of dynamic routes

`readDynamicRoutes` reads three `.next/` manifests written by `next build`:

| Manifest | What it contributes |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `routes-manifest.json` | Rewrite source patterns; `dynamicRoutes[].page` (e.g. `/blog/[slug]`) |
| `prerender-manifest.json` | The set of already-prerendered paths — used to exclude from the dynamic list |
| `pages-manifest.json` + `app-paths-manifest.json` | Any page/route not in the prerender set is SSR; App Router keys are normalised (`/dashboard/page` → `/dashboard`) |

### Inline script hash collection (current)

| Route class | Technique |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| staticPages | `walkHtmlFiles` reads `.next/server/app/` and `.next/server/pages/`, calls `extractInlineScriptHashes` + `extractInlineAttrHashes` on each `.html` file |
| fixedRoute / enumerableRoute / probedRoute | Empty entry only — `{ scripts: [], attrs: [] }` via `routePatternToPrefixKey` |

### Gap — SSR pages get no hash extraction

Next.js SSR pages (those not in the prerender manifest) are never fetched at build time, so their
inline scripts are never extracted. The CSP injected for these pages will block all inline scripts,
including any legitimate bootstrapper scripts the framework emits on every SSR response.

### Proposed fix — Next.js programmatic server (mirrors Astro)

Next.js ships a programmatic API that produces the same `handler(req, res)` shape as Astro's
`entry.mjs`:

```js
const { default: next } = await import('next');
const app = next({ dev: false, dir: projectRoot });
await app.prepare();
const handler = app.getRequestHandler();
```

This lets us reuse the same spin-up-server → fetch → parse-HTML loop already implemented in
`hashSSRRoutes` (Astro). `next` is already a peer dependency — no new dependency required.

| Route class | Route type | Technique |
| --------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| fixedRoute | SSR, no URL params (e.g. `/dashboard`) | Start programmatic server, fetch the URL, parse the HTML |
| enumerableRoute | SSR + `generateStaticParams` | Read the concrete paths already enumerated by `next build` in `prerender-manifest.json` under `dynamicRoutes`, then fetch+parse each one via the programmatic server |
| probedRoute | SSR + URL params, IDs not in prerender manifest | Fetch a sentinel probe URL, parse, store under prefix key |

Note on enumerableRoute: `prerender-manifest.json` is already read by `readDynamicRoutes` —
currently only to build the exclusion set. The enumerableRoute change reuses the same data: the
`dynamicRoutes` entries inside it are the concrete paths to fetch and hash. Routes that use
`generateStaticParams` and are fully prerendered (no `dynamicParams`) are already in the staticPages
walk and never reach this path. enumerableRoute only applies when `dynamicParams = true` (the
default) — known IDs are prerendered and in the manifest, while unknown IDs fall back to SSR.

---

## Shared output shape

Both integrations converge on the same final shape passed to `generateManifest`:

```js
csp: {
pages: {
'/index.html': { scripts: ['sha256-...'], attrs: [] }, // staticPages, hashes from walk
'/live': { scripts: ['sha256-...'], attrs: [] }, // fixedRoute, hashes from fetch
'/posts/': { scripts: ['sha256-...'], attrs: [] }, // probedRoute, hashes from probe
'/dashboard': { scripts: [], attrs: [] }, // fixedRoute, no inline scripts
}
}
```

`manifest-tools/generateManifest` merges `cspBuiltPages` (from the static file walk) with the
`csp.pages` map passed by the integration. The integration's entries take precedence.
Loading
Loading