From 730d0e33cf9ec3dfe7876db82cb45605498d544f Mon Sep 17 00:00:00 2001 From: Ade Oshineye Date: Fri, 10 Jul 2026 18:51:55 +0100 Subject: [PATCH] Harden runtime, browser flows, and release checks --- .github/workflows/preview.yml | 8 +- .github/workflows/verify.yml | 9 +- CONTRIBUTING.md | 6 +- Makefile | 12 +- README.md | 18 +- docs/example-graph-see-also.md | 8 +- docs/learner-analytics.md | 62 +- docs/lessons-learned.md | 10 +- docs/pr-evidence/README.md | 25 +- .../audit-remediation-after-runner-dark.png | Bin 0 -> 55096 bytes .../audit-remediation-before-runner-dark.png | Bin 0 -> 55044 bytes package-lock.json | 1551 +++++++++++++++++ package.json | 12 + ...a550bf2266bd.js => editor.3827f6497b54.js} | 12 +- public/editor.js | 12 +- ...d3515b57bc3e.js => runner.96b105eef218.js} | 117 +- public/runner.js | 117 +- ...158bd7afa114.js => search.e5a5822d9917.js} | 24 +- public/search.js | 24 +- ...4ad21a6ce8a3.css => site.07c59ae07f87.css} | 19 +- public/site.css | 19 +- ...f5.js => syntax-highlight.c405e604fc10.js} | 69 +- public/syntax-highlight.js | 69 +- pyproject.toml | 1 + scripts/build_social_cards.py | 26 + scripts/capture_browser_screenshot.mjs | 15 + scripts/check_browser_layout.mjs | 202 ++- src/app.py | 48 +- src/asset_manifest.py | 4 +- src/main.py | 57 +- src/marginalia.py | 5 +- src/templates/about.html | 2 +- src/templates/example.html | 8 +- src/templates/home.html | 9 +- src/templates/layout.html | 5 +- src/worker_asgi_bridge.py | 23 +- tests/test_app.py | 50 +- tests/test_main_observability.py | 69 + tests/test_main_routes.py | 3 + tests/test_main_turnstile.py | 48 +- tests/test_social_cards.py | 22 +- tests/test_worker_asgi_bridge_scope.py | 155 +- uv.lock | 39 +- 43 files changed, 2716 insertions(+), 278 deletions(-) create mode 100644 docs/pr-evidence/audit-remediation-after-runner-dark.png create mode 100644 docs/pr-evidence/audit-remediation-before-runner-dark.png create mode 100644 package-lock.json create mode 100644 package.json rename public/{editor.a550bf2266bd.js => editor.3827f6497b54.js} (79%) rename public/{runner.d3515b57bc3e.js => runner.96b105eef218.js} (69%) rename public/{search.158bd7afa114.js => search.e5a5822d9917.js} (84%) rename public/{site.4ad21a6ce8a3.css => site.07c59ae07f87.css} (95%) rename public/{syntax-highlight.d6178fb3eef5.js => syntax-highlight.c405e604fc10.js} (52%) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 7853c54..f951855 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -30,15 +30,17 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '22' - - name: Install dependencies - run: uv sync --all-groups + - name: Install locked dependencies + run: | + uv sync --locked --all-groups + npm ci --ignore-scripts - name: Build generated assets run: make build - name: Verify Cloudflare auth env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: npx --yes wrangler@4.90.0 whoami + run: npm exec -- wrangler whoami - name: Sync Python Workers vendor run: uv run pywrangler sync # Workflow inputs are passed through env vars rather than interpolated diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index cdadbaa..5d5129a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -27,10 +27,10 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '22' - - name: Install dependencies - run: uv sync --all-groups - - name: Pin Wrangler for local Worker runtime - run: npm install --global wrangler@4.90.0 + - name: Install locked dependencies + run: | + uv sync --locked --all-groups + npm ci --ignore-scripts - name: Start local Worker for browser checks run: | uv run --group workers pywrangler dev --port 9696 > /tmp/pythonbyexample-ci.log 2>&1 & @@ -50,6 +50,7 @@ jobs: env: CHROME_PATH: /usr/bin/google-chrome run: | + npm audit --audit-level=high make verify scripts/format_examples.py --check make verify-python-version VERSION=3.13 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09427e9..baea918 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,10 +39,12 @@ make build ## Before opening a pull request -Start the local Worker for browser-backed checks: +Install the committed dependency locks and start the local Worker with the repository-pinned Wrangler: ```bash -uv run --group workers pywrangler dev --port 9696 +uv sync --locked --all-groups +npm ci --ignore-scripts +make dev ``` Then run: diff --git a/Makefile b/Makefile index 1404ce3..d808413 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,14 @@ # All Python tooling runs through uv pinned to 3.13 so the suite works on any # machine regardless of the system python3 (examples use 3.12+ `type` syntax). PY := uv run --python 3.13 +NODE_DEPS_STAMP := node_modules/.package-lock.json -.PHONY: test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint +.PHONY: node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint + +$(NODE_DEPS_STAMP): package.json package-lock.json + npm ci --ignore-scripts + +node-deps: $(NODE_DEPS_STAMP) test: $(PY) python -m unittest discover -s tests -v @@ -104,12 +110,12 @@ lint: verify: build test seo-cache-lint verify-examples quality-checks browser-layout-test search-ranking-test lint check-generated -dev: +dev: node-deps uv run --group workers pywrangler dev --port 9696 smoke-deployment: $(PY) scripts/smoke_deployment.py $(URL) -deploy: check-generated +deploy: node-deps check-generated uv run --group workers pywrangler sync uv run --group workers pywrangler deploy diff --git a/README.md b/README.md index ad74856..cd1cc9d 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,11 @@ This project is developed with red-green-refactor TDD: 2. Implement the smallest change that makes it pass. 3. Refactor while keeping tests green. -Install dependencies with `uv`, then run: +Install the exact committed Python and Node dependency sets, then run: ```bash +uv sync --locked --all-groups +npm ci --ignore-scripts make test ``` @@ -81,10 +83,10 @@ After cloning, install the local git hooks once so merges and rebases regenerate ./scripts/install-git-hooks.sh ``` -Run locally on Workers: +Run locally on Workers with the repository-pinned Wrangler: ```bash -uv run --group workers pywrangler dev --port 9696 +make dev ``` Open: @@ -98,7 +100,7 @@ http://localhost:9696 Run a local Worker before the full browser-backed verification: ```bash -uv run --group workers pywrangler dev --port 9696 +make dev ``` Then run the main checks before deploying or pushing: @@ -197,8 +199,8 @@ make check-generated After adding an example or journey (or changing card title, summary, figure, or card CSS), regenerate social cards. `make check-generated` verifies the -committed card-input provenance and exact JPEG set, without byte-comparing -Chrome's platform-variable raster output: +committed card-input provenance, exact JPEG set, JPEG decodability, and 1200×630 +dimensions without byte-comparing Chrome's platform-variable raster output: ```bash make social-cards @@ -228,4 +230,6 @@ Use the active Cloudflare-supported Python version. - The parent Worker uses a `LOADER` Worker Loader binding. - Dynamic Worker IDs include Python version, example slug, and submitted code hash. - Dynamic Workers run with `globalOutbound: null` and tight CPU/subrequest limits. -- POST example runs are never cached. +- POST example runs carry `Cache-Control: no-store` and are never cached. +- Wrangler is an exact dev dependency in `package-lock.json`; `make dev` and `make deploy` install/use that local version through Pywrangler. +- Production still requires the account-level WAF/rate-limit rule documented in `docs/turnstile-runner-protection-spec.md`; repository settings alone do not prove that external control is active. diff --git a/docs/example-graph-see-also.md b/docs/example-graph-see-also.md index 88a3d55..5f2ec72 100644 --- a/docs/example-graph-see-also.md +++ b/docs/example-graph-see-also.md @@ -38,14 +38,12 @@ see_also = [ ] ``` -The loader exposes `see_also`, the example renderer displays a compact `See also` section, and tests verify that every linked slug exists and does not point to itself. +The loader exposes `see_also`, the example renderer displays a compact `See also` section with registry-backed edge labels such as `contrast` and `next depth`, and tests verify that every linked slug exists and does not point to itself. The same graph supplies bounded recommendations on missing-example pages. ## Graph audit `scripts/audit_example_graph.py` reports orphan pages, high in-degree pages, and reciprocal links. Its `--check` mode runs in `make quality-checks` and fails the build on broken targets, self-links, out-degree above 4, or orphaned examples. -## Future improvements +## Review rule -- Show short edge labels later, e.g. `contrast`, `prerequisite`, `next depth`. -- Use graph data to recommend examples on 404 pages or search results. -- Keep the first version minimal until we know the links improve reading rather than distracting from the code. +Keep labels and recommendations minimal. Each edge still needs editorial review: passing graph checks proves structural integrity, not that a link improves the lesson. diff --git a/docs/learner-analytics.md b/docs/learner-analytics.md index 0e16be7..499d2f0 100644 --- a/docs/learner-analytics.md +++ b/docs/learner-analytics.md @@ -1,23 +1,27 @@ # Learner analytics The Worker emits one structured wide event per request (see -`docs/observability-spec.md`). Those events already carry the fields -that matter for content decisions: the page path, the example slug on -POST runs, whether the submitted code was edited, the Dynamic Worker -outcome, and the execution time. - -`scripts/learner_report.py` turns an export of those events into a -learner-behavior report so content work is steered by external signal -rather than internal quality scores: - -- **Most-read pages** — GET traffic per path. -- **Most-run examples** — POST runs per slug, with the edited share - (are people experimenting or just pressing Run?), the error share - (where edited code fails), and p50/p95 execution times. -- **Journey traffic** — which curated arcs get used. -- **Missing-example requests** — 404s under `/examples/`, i.e. demand - for pages that do not exist yet. These are content candidates. -- **Turnstile outcomes** — how often runs are challenged or blocked. +`docs/observability-spec.md`). Those events carry the page path, the +example slug on POST runs, whether submitted code differed from the +example, the Dynamic Worker outcome, and execution time. + +`scripts/learner_report.py` aggregates those events into an +**operational interaction scorecard**. It does not identify people, +deduplicate visits, measure comprehension, or prove that a content +change caused learning. Every row is a request count within the export's +time window: + +- **Most-requested pages** — successful GET requests per path, including + repeat requests and possible automated traffic. +- **Most-run examples** — accepted POST attempts per slug, with edited, + error, and p50/p95 execution-time distributions. "Edited" means only + that submitted code differed; it is not evidence of successful learning. +- **Journey requests** — GET request volume for curated-arc routes. +- **Missing-example requests** — 404s under `/examples/`. Recurring, + human-validated paths may suggest content candidates, but typos, scanners, + and bots are confounders. +- **Turnstile outcomes** — challenge and rejection counts used to monitor + runner availability and abuse controls, not learner quality. ## Getting events @@ -37,12 +41,18 @@ envelope, so exports from any of the three sources work unmodified. `--json` emits the aggregated report as JSON for further processing; `--limit N` controls rows per section. -## Reading the report - -- A high **edited share** with a low error share means the example - invites successful experimentation — the ideal. -- A high **error share** on edited runs marks pages where learners try - something the example did not prepare them for; consider extending - the walkthrough or notes there. -- **Missing-example requests** that recur are the strongest possible - signal for what to write next. +## Reading the scorecard + +Use the report as a queue for investigation, not as a composite score or +a learning-outcome dashboard. Record the export interval and absolute +sample size, compare equivalent time windows, and inspect paths or code +patterns before changing content. + +- A high **edited share** can justify reviewing an example for useful + extension points. Pair it with error samples and direct learner feedback. +- A high **error share** can indicate unclear teaching, intentionally invalid + experiments, abuse, or runtime trouble. Diagnose those alternatives first. +- Recurring **missing-example requests** are candidates only after filtering + obvious automation and validating the intended topic. +- Do not rank authors or examples by one ratio, and do not claim learning + improvement without a separate outcome measure and an appropriate study. diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index 083031a..144ce06 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -117,7 +117,7 @@ git diff --check - **Lines must terminate AT elements, not in their gaps or interiors.** A 1.5px gap between a tree edge and a leaf dot reads as "the tree is disconnected" (the `exception-group-peel` bug). A line endpoint 2px inside a circle reads as "the arrow pierces the node" (the `context-bowtie` bug). When connecting to a dot, end the line at the dot's centre and let the dot draw on top — the visual termination is the circumference, with zero gap or overshoot. - **Journey pages render section figures inline.** `SECTION_FIGURES` lives in `src/marginalia.py` (single source of truth, keyed by section title) and `render_for_section(title)` is invoked from `render_journey_page` between each section's meta and its example list. The same paint code that produces the `/prototyping/journey-figures-gestalt` review page renders on production journey pages; drift between the two is structurally impossible. Contract 10 asserts every section in `JOURNEYS` has a figure and every figure name resolves. - **An explicit comparison loop should iterate over the topic's whole spectrum.** When a cell teaches by doing `for label, value in [(...), (...)]: print(...)`, the bracketed list IS the lesson. Two items is a binary contrast; three items reads as a progression. The strings example presented English (pure ASCII, 1 byte/char) against Thai (3 bytes/char) but skipped the Latin-extended middle (French `café`: 4 code points, 5 bytes — `é` is 2 UTF-8 bytes). Adding the middle row turned the cell from "ASCII vs non-Latin" into "1-byte / 2-byte / 3-byte progression." The rule is narrow — most examples spread categories across cells, which is also a valid pattern — but when a comparison loop exists, fill it with the topic's actual spectrum, not just the endpoints. -- **Quality debt must be tracked, not normalized away.** `docs/example-quality-rubric.md` sets a 9.0 target and `scripts/check_quality_scores.py` enforces the score registry: pages below the hard minimum need a concrete improvement backlog entry, stale backlog entries fail once a page clears the gate, and Hello World is the only standing waiver because first examples are traditionally tiny. A score below target is allowed only when the remaining work is named. +- **Quality debt must be tracked, not normalized away.** `docs/example-quality-rubric.md` sets a 9.0 target and `scripts/check_quality_scores.py` enforces the registry's hard minimum; pages below the 9.0 target belong in the scored review queue, and Hello World is the only standing waiver because first examples are traditionally tiny. Registry totals are editorial signals, not measured learning outcomes. - **No-figure decisions need a registry.** Some examples should not have figures, but that cannot be an invisible omission. `scripts/check_no_figure_rationales.py` validates `no_figure_rationales` so future constraint-shaped pages can opt out explicitly instead of shipping weak diagrams. - **Journey sections need outcome contracts.** `scripts/check_journey_outcomes.py` ties each journey section to learner outcomes and support examples so journey pages stay mental maps rather than catalog slices. - **Opaque scores hide the next move.** `scripts/score_example_criteria.py` breaks each page into rubric criteria so quality work can target decomposition, boundaries, source/result pairing, graph support, or practical payoff directly. The bottom-28 pass showed that most misses were boundary/neighbor problems, not syntax problems. `docs/quality-search.md` records the hill-climbing and simulated-annealing loop for escaping locally tidy but globally weak page shapes. @@ -133,12 +133,12 @@ git diff --check - **Extend the existing registry's vocabulary instead of adding a parallel one.** The banner rollout was originally specced as a new `BANNERS` dict keyed by position. But `ATTACHMENTS` is load-bearing for five contract families (score sync, figure usage, caption uniqueness, anchor resolution, gestalt builders), and a second registry would have split that coverage. Extending the anchor vocabulary (`before`, `after-walkthrough`, `after-cell-N` alongside legacy `cell-N`) delivered the same grammar with every existing contract intact. Same lesson as "one paint registry, not two," applied to data shape migrations. - **Dark mode for locked-palette SVGs: change the mat, not the art.** The marginalia grammar hardcodes the light palette in every figure, and recolouring 109 figures would have meant touching the locked grammar and re-auditing geometry. Instead, dark mode renders each figure on a light "paper" chip (`--figure-paper` background, small padding, rounded corners). The figures stay byte-identical, read as intentional artifacts, and the grammar's palette constraint survives. -- **Dual-theme code highlighting needs both pipelines.** Shiki supports `themes: { light, dark }` and emits `--shiki-dark` CSS variables that a `prefers-color-scheme` block activates — no second render pass. CodeMirror has no equivalent, so `editor.js` picks `defaultHighlightStyle` vs `oneDarkHighlightStyle` from `matchMedia` at init. Two highlighters, two theming mechanisms; forgetting either leaves unreadable code in one scheme. -- **Rasterized artifacts do not belong in byte-parity gates.** Social cards are committed PNGs-turned-JPEGs rendered by headless Chrome, and rasterized bytes vary across Chrome versions and platforms. Putting `public/og/` under `make check-generated` would flap on every environment difference. The SEO linter instead checks *existence* — every `og:image` URL must resolve to a committed file — which catches the real failure (a new example without a card) without the false ones. Corollary: JPEG q90 beats PNG ~4x on file size for cards with gradient backgrounds, with no visible text degradation at 1200x630. -- **Social cards should reuse the curated figure set.** Each example's card composes its marginalia figure beside the title and summary, so a shared link carries the same diagram the page teaches with. `render_first_figure(slug)` is the only new marginalia surface the card builder needed; one Chrome session rasterizes all 110 cards in seconds via CDP navigation + `Page.captureScreenshot` clips. +- **Theme changes need live browser state, not just first-paint CSS.** Read-only Shiki blocks are rendered with the current light/dark theme and rerender when the OS preference changes. CodeMirror rebuilds its theme compartment from the same `matchMedia` change signal. Both also retain readable plain-text fallbacks when the optional CDN graph is unavailable. +- **Rasterized artifacts need structural gates, not byte-parity gates.** Social-card bytes vary across Chrome versions and platforms, so CI validates deterministic input provenance, the exact expected JPEG filename set, JPEG decodability, and 1200×630 dimensions rather than comparing raster bytes. The SEO linter separately verifies that every `og:image` URL resolves to a committed card. +- **Social cards should reuse the curated figure set.** Each example's card composes its marginalia figure beside the title and summary, so a shared link carries the same diagram the page teaches with. `render_first_figure(slug)` is the only new marginalia surface the card builder needed; one Chrome session rasterizes the current 119-card set via CDP navigation and `Page.captureScreenshot` clips. - **Copy that describes data state goes stale silently.** Journey meta descriptions still claimed "explicit placeholders for missing examples" long after the last gap placeholder was filled, and the gap-rendering UI suggested holes that no longer existed. Prose that asserts a property of the data ("all examples run," "placeholders mark gaps") should either be derived from the data or covered by a check; otherwise fixing the data falsifies the copy. - **Make targets that import the example loader must go through uv.** The loader executes example code that uses 3.12+ syntax (`type UserId = int`), so any script importing `src.app` breaks under an older system `python3` even though its shebang says `python3`. CI masks this by installing 3.13 as the default interpreter. Build steps that import the catalog (`build_search_index`, `build_social_cards`) run as `uv run --python 3.13 scripts/...` in the Makefile so local machines with an older `python3` behave like CI. - **Index notes text, not just titles, and normalize at build time.** The search index concatenates the slug words and every note line, lowercased at build time, so concept queries ("walrus", "GIL"-style vocabulary that titles avoid) hit the right example and the client never re-normalizes entry content. Exporting the ranking function from `search.js` lets a plain Node script (`make search-ranking-test`) assert ranking behaviour against the real generated index without a JS test framework. - **The system stack is the typography; never lead a stack with a face nobody has.** From the first commit the font stacks led with FT Kunst Grotesk and Apercu Mono Pro, but no `@font-face` ever shipped and the faces were not installed anywhere — not on visitors' machines, not on the maintainer's Mac, not on the build machine that rasterizes the social cards. They rendered for no one, ever, and were removed on 2026-07-10. The stacks now declare what actually renders: `system-ui` for prose, `ui-monospace` first for code. Body type is `100%/1.6` so browser text-size settings are respected, and `/about` documents the stack honestly. - **UI icons are a deliberate, minimal vocabulary.** The site's chrome is text-first; the only UI icons are the Lucide copy/check/x glyphs on the code-cell copy button, drawn as `currentColor` CSS masks so every token state (muted, hover, accent-on-copied, dark mode) flows through with no icon-specific color rules. Toolbar actions (Run, Reset, Copy link) stay text because no glyph says "copy a URL containing your edit" unambiguously. Any further icon must update this entry and justify itself the same way. -- **The wide events already knew what learners do; nobody was asking.** `example.slug`, `code_edited`, `execution_ms`, turnstile outcome, and 404 paths were all in the observability payload before any analytics existed. `scripts/learner_report.py` is a pure consumer: it auto-detects the raw payload, `wrangler tail` envelope, and Workers Logs envelope per line, so exports from any source work unmodified. The most valuable section costs nothing to collect: recurring 404s under `/examples/` are direct demand for pages that do not exist. +- **Wide events support an operational interaction scorecard, not learning claims.** `example.slug`, `code_edited`, `execution_ms`, Turnstile outcome, and 404 paths were already present. `scripts/learner_report.py` aggregates raw, `wrangler tail`, and Workers Logs envelopes, but its counts include repeats and possible bots. Recurring 404s and edited-run ratios create investigation queues only after sample-size and traffic-quality review; they do not measure comprehension. diff --git a/docs/pr-evidence/README.md b/docs/pr-evidence/README.md index c1b99cd..38aa6db 100644 --- a/docs/pr-evidence/README.md +++ b/docs/pr-evidence/README.md @@ -1,4 +1,25 @@ -# PR #6 visual evidence +# PR visual evidence + +## Audit remediation (2026-07-10) + +These captures isolate the dark-mode Run-button contrast correction on the same `/examples/values` runner at a 1200×900 desktop viewport. + +| Evidence | Review point | Commit | SHA-256 | +| --- | --- | --- | --- | +| [Before](audit-remediation-before-runner-dark.png) | The prior light text on the orange Run button measured 2.19:1 contrast. | `46000c7` (`origin/main`) | `4f15bc0d6f2c965197265bff2f6b300aee5a7a32e4ef152001aeb6c8d290e860` | +| [After](audit-remediation-after-runner-dark.png) | Dark action text raises measured contrast to 7.87:1 while preserving the runner layout. | `audit-remediation-2026-07-10` | `53882092e58e02bbe323c5ed70e0d1c491f1e8e9f6887416b7ec83222ad305cc` | + +Reproduce each capture by serving the corresponding revision, then run: + +```bash +TARGET_URL=http://127.0.0.1:/examples/values \ +CAPTURE_SELECTOR=.runner-grid WIDTH=1200 HEIGHT=900 COLOR_SCHEME=dark \ +OUTPUT=.png node scripts/capture_browser_screenshot.mjs +``` + +`make browser-layout-test` independently calculates the rendered button contrast and fails below 4.5:1. + +## PR #6 evidence These artifacts provide reviewable evidence for the runner, journey-page, and social-card changes. @@ -9,7 +30,7 @@ These artifacts provide reviewable evidence for the runner, journey-page, and so | [Runtime journey page](pr-6-journey-runtime.png) | `/journeys/runtime` renders its overview, first learning outcome, figure, and linked examples at a 1200×1260 desktop viewport. | `a85bbf7` | `eb1db35d5ff7fc7974ad3a3891b694aaa5f8f277ba291b94a24c62767340e04d` | | [`/journeys` social card](../../public/og/journeys.jpg) | The exact 1200×630 JPEG referenced by the `/journeys` `og:image` tag. | `a85bbf7` | `3042997bb2b3acd9e2ab74106f5b4d3d4c58dafcfe23f1ddc548ed39e4548a3c` | -`public/og/journeys.jpg` is a tracked PR artifact and its deterministic HTML input is recorded in `public/og/manifest.json`. `make check-social-cards` verifies both its provenance and the exact 118-card JPEG set. +`public/og/journeys.jpg` is a tracked PR artifact and its deterministic HTML input is recorded in `public/og/manifest.json`. `make check-social-cards` verifies its provenance, the exact 119-card JPEG set, and each card's JPEG decodability and 1200×630 dimensions. Regenerate each page with its commit checked out, serve the rendered `/examples/values` HTML plus `public/` on a local HTTP server, then run: diff --git a/docs/pr-evidence/audit-remediation-after-runner-dark.png b/docs/pr-evidence/audit-remediation-after-runner-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..366e820deaf48b9f83533803aa0dc5689035d45c GIT binary patch literal 55096 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

}&}RIgRCy+;^NuXk%})Nx6AODyn= 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 Nx~Hq3%Q~loCIHTs$X@^e literal 0 HcmV?d00001 diff --git a/docs/pr-evidence/audit-remediation-before-runner-dark.png b/docs/pr-evidence/audit-remediation-before-runner-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8c9fe4c8034214c27d6247042c2e9bd0d10b14a6 GIT binary patch literal 55044 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9d69098 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1551 @@ +{ + "name": "pythonbyexample-tooling", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pythonbyexample-tooling", + "devDependencies": { + "wrangler": "4.110.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260708.1.tgz", + "integrity": "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260708.1.tgz", + "integrity": "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260708.1.tgz", + "integrity": "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260708.1.tgz", + "integrity": "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260708.1.tgz", + "integrity": "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260708.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.1.tgz", + "integrity": "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260708.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260708.1.tgz", + "integrity": "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260708.1", + "@cloudflare/workerd-darwin-arm64": "1.20260708.1", + "@cloudflare/workerd-linux-64": "1.20260708.1", + "@cloudflare/workerd-linux-arm64": "1.20260708.1", + "@cloudflare/workerd-windows-64": "1.20260708.1" + } + }, + "node_modules/wrangler": { + "version": "4.110.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.110.0.tgz", + "integrity": "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260708.1", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260708.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260708.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb81650 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "pythonbyexample-tooling", + "private": true, + "type": "module", + "packageManager": "npm@11.4.2", + "scripts": { + "wrangler": "wrangler" + }, + "devDependencies": { + "wrangler": "4.110.0" + } +} diff --git a/public/editor.a550bf2266bd.js b/public/editor.3827f6497b54.js similarity index 79% rename from public/editor.a550bf2266bd.js rename to public/editor.3827f6497b54.js index e774839..9296650 100644 --- a/public/editor.a550bf2266bd.js +++ b/public/editor.3827f6497b54.js @@ -1,11 +1,12 @@ -import { EditorState } from 'https://esm.sh/@codemirror/state@6.5.2'; +import { Compartment, EditorState } from 'https://esm.sh/@codemirror/state@6.5.2'; import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2'; import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1'; import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3'; import { oneDarkHighlightStyle } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3'; -const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const highlightStyle = prefersDark ? oneDarkHighlightStyle : defaultHighlightStyle; +const themePreference = window.matchMedia('(prefers-color-scheme: dark)'); +const highlightStyle = () => themePreference.matches ? oneDarkHighlightStyle : defaultHighlightStyle; +const highlightCompartment = new Compartment(); const textarea = document.getElementById('code-editor'); const form = document.querySelector('form.runner-editor'); @@ -18,7 +19,7 @@ if (textarea && form) { doc: textarea.value, extensions: [ python(), - syntaxHighlighting(highlightStyle), + highlightCompartment.of(syntaxHighlighting(highlightStyle())), lineNumbers(), EditorView.lineWrapping, EditorView.contentAttributes.of({ @@ -33,6 +34,9 @@ if (textarea && form) { textarea.insertAdjacentElement('afterend', view.dom); textarea.hidden = true; + themePreference.addEventListener?.('change', () => { + view.dispatch({ effects: highlightCompartment.reconfigure(syntaxHighlighting(highlightStyle())) }); + }); function resize() { view.dom.style.minHeight = ''; diff --git a/public/editor.js b/public/editor.js index e774839..9296650 100644 --- a/public/editor.js +++ b/public/editor.js @@ -1,11 +1,12 @@ -import { EditorState } from 'https://esm.sh/@codemirror/state@6.5.2'; +import { Compartment, EditorState } from 'https://esm.sh/@codemirror/state@6.5.2'; import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2'; import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1'; import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3'; import { oneDarkHighlightStyle } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3'; -const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const highlightStyle = prefersDark ? oneDarkHighlightStyle : defaultHighlightStyle; +const themePreference = window.matchMedia('(prefers-color-scheme: dark)'); +const highlightStyle = () => themePreference.matches ? oneDarkHighlightStyle : defaultHighlightStyle; +const highlightCompartment = new Compartment(); const textarea = document.getElementById('code-editor'); const form = document.querySelector('form.runner-editor'); @@ -18,7 +19,7 @@ if (textarea && form) { doc: textarea.value, extensions: [ python(), - syntaxHighlighting(highlightStyle), + highlightCompartment.of(syntaxHighlighting(highlightStyle())), lineNumbers(), EditorView.lineWrapping, EditorView.contentAttributes.of({ @@ -33,6 +34,9 @@ if (textarea && form) { textarea.insertAdjacentElement('afterend', view.dom); textarea.hidden = true; + themePreference.addEventListener?.('change', () => { + view.dispatch({ effects: highlightCompartment.reconfigure(syntaxHighlighting(highlightStyle())) }); + }); function resize() { view.dom.style.minHeight = ''; diff --git a/public/runner.d3515b57bc3e.js b/public/runner.96b105eef218.js similarity index 69% rename from public/runner.d3515b57bc3e.js rename to public/runner.96b105eef218.js index eed3988..52e2a6f 100644 --- a/public/runner.d3515b57bc3e.js +++ b/public/runner.96b105eef218.js @@ -1,3 +1,6 @@ +const MAX_SHARE_FRAGMENT_CHARS = 24000; +const TURNSTILE_ACTION = 'run-example'; + function initializeRunner() { const textarea = document.getElementById('code-editor'); const form = document.querySelector('form.runner-editor'); @@ -17,22 +20,30 @@ function initializeRunner() { window.pythonByExampleEditor?.setValue(value); }; + let cancelActiveRun = () => {}; const resetButton = form.querySelector('[data-reset]'); if (resetButton) { resetButton.addEventListener('click', () => { + cancelActiveRun(); setCode(originalCode); window.pythonByExampleEditor?.focus(); }); } let sharedCodeLoaded = false; + let sharedCodeRejected = false; const hash = new URL(window.location.href).hash; if (hash.startsWith('#code=')) { - try { - setCode(decodeURIComponent(escape(atob(hash.slice(6))))); - sharedCodeLoaded = textarea.value !== originalCode; - } catch (_) { - // Ignore malformed share fragments and leave the authored example intact. + const encodedCode = hash.slice(6); + if (encodedCode.length > MAX_SHARE_FRAGMENT_CHARS) { + sharedCodeRejected = true; + } else { + try { + setCode(decodeURIComponent(escape(atob(encodedCode)))); + sharedCodeLoaded = textarea.value !== originalCode; + } catch (_) { + sharedCodeRejected = true; + } } } @@ -85,9 +96,11 @@ function initializeRunner() { const outputPanel = document.querySelector('.output-panel'); if (!outputPanel) return; - if (sharedCodeLoaded) { + if (sharedCodeLoaded || sharedCodeRejected) { outputPanel.querySelector('h3').textContent = 'Output'; - outputPanel.querySelector('code').textContent = 'This link included edited code. Press Run to execute it.'; + outputPanel.querySelector('code').textContent = sharedCodeLoaded + ? 'This link included edited code. Press Run to execute it.' + : 'The shared-code fragment was invalid or too large, so the authored example was kept.'; } const challengeBox = document.querySelector('[data-turnstile-sitekey]'); let turnstileWidgetId = null; @@ -101,8 +114,20 @@ function initializeRunner() { script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; script.async = true; script.defer = true; - script.onload = resolve; - script.onerror = () => reject(new Error('Turnstile failed to load')); + script.onload = () => { + if (window.turnstile) { + resolve(); + return; + } + loadingTurnstile = null; + script.remove(); + reject(new Error('Turnstile loaded without its browser API')); + }; + script.onerror = () => { + loadingTurnstile = null; + script.remove(); + reject(new Error('Turnstile failed to load; press Run to retry')); + }; document.head.appendChild(script); }); return loadingTurnstile; @@ -126,27 +151,30 @@ function initializeRunner() { challengeBox.innerHTML = ''; } - async function requestTurnstileToken() { + async function requestTurnstileToken(signal) { if (!challengeBox) throw new Error('Turnstile challenge is not configured'); await loadTurnstile(); + if (signal.aborted) throw new DOMException('Run cancelled', 'AbortError'); challengeBox.hidden = false; return new Promise((resolve, reject) => { + let settled = false; + const finish = (callback, value) => { + if (settled) return; + settled = true; + signal.removeEventListener('abort', onAbort); + removeTurnstile(); + callback(value); + }; + const onAbort = () => finish(reject, new DOMException('Run cancelled', 'AbortError')); + signal.addEventListener('abort', onAbort, { once: true }); challengeBox.innerHTML = ''; turnstileWidgetId = turnstile.render(challengeBox, { sitekey: challengeBox.dataset.turnstileSitekey, + action: TURNSTILE_ACTION, execution: 'execute', - callback: (token) => { - removeTurnstile(); - resolve(token); - }, - 'error-callback': () => { - removeTurnstile(); - reject(new Error('Turnstile challenge failed')); - }, - 'expired-callback': () => { - removeTurnstile(); - reject(new Error('Turnstile challenge expired')); - }, + callback: token => finish(resolve, token), + 'error-callback': () => finish(reject, new Error('Turnstile challenge failed')), + 'expired-callback': () => finish(reject, new Error('Turnstile challenge expired')), }); turnstile.execute(turnstileWidgetId); }); @@ -162,37 +190,68 @@ function initializeRunner() { || `HTTP ${response.status}`; } - async function submitRun(turnstileToken = '') { - outputPanel.removeAttribute('data-output-placeholder'); + let latestRunId = 0; + let activeController = null; + const runButton = form.querySelector('button[type="submit"]'); + + async function submitRun(runId, signal, turnstileToken = '') { window.pythonByExampleEditor?.syncTextarea(); - outputPanel.querySelector('code').textContent = 'Running in a Dynamic Python Worker…'; const formData = new FormData(form); if (turnstileToken) formData.set('cf-turnstile-response', turnstileToken); const response = await fetch(form.action, { method: 'POST', body: new URLSearchParams(formData), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + signal, }); const html = await response.text(); + if (runId !== latestRunId) return; if (!response.ok) throw new Error(responseErrorMessage(response, html)); const document = new DOMParser().parseFromString(html, 'text/html'); const challengeRequired = document.querySelector('[data-turnstile-required]'); if (challengeRequired) { outputPanel.querySelector('code').textContent = challengeRequired.textContent || 'Verification required before running edited code…'; - return submitRun(await requestTurnstileToken()); + const token = await requestTurnstileToken(signal); + if (runId !== latestRunId) return; + return submitRun(runId, signal, token); } const nextOutput = document.querySelector('.output-panel'); if (nextOutput) outputPanel.innerHTML = nextOutput.innerHTML; } + cancelActiveRun = () => { + latestRunId += 1; + activeController?.abort(); + activeController = null; + removeTurnstile(); + form.removeAttribute('aria-busy'); + if (runButton) runButton.disabled = false; + }; + form.addEventListener('submit', async (event) => { event.preventDefault(); + activeController?.abort(); + const runId = ++latestRunId; + const controller = new AbortController(); + activeController = controller; + outputPanel.removeAttribute('data-output-placeholder'); + outputPanel.querySelector('code').textContent = 'Running in a Dynamic Python Worker…'; + form.setAttribute('aria-busy', 'true'); + if (runButton) runButton.disabled = true; try { - await submitRun(); + await submitRun(runId, controller.signal); } catch (error) { - removeTurnstile(); - outputPanel.querySelector('code').textContent = `Run failed: ${error.message}`; + if (runId === latestRunId && error.name !== 'AbortError') { + removeTurnstile(); + outputPanel.querySelector('code').textContent = `Run failed: ${error.message}`; + } + } finally { + if (runId === latestRunId) { + activeController = null; + form.removeAttribute('aria-busy'); + if (runButton) runButton.disabled = false; + } } }); } diff --git a/public/runner.js b/public/runner.js index eed3988..52e2a6f 100644 --- a/public/runner.js +++ b/public/runner.js @@ -1,3 +1,6 @@ +const MAX_SHARE_FRAGMENT_CHARS = 24000; +const TURNSTILE_ACTION = 'run-example'; + function initializeRunner() { const textarea = document.getElementById('code-editor'); const form = document.querySelector('form.runner-editor'); @@ -17,22 +20,30 @@ function initializeRunner() { window.pythonByExampleEditor?.setValue(value); }; + let cancelActiveRun = () => {}; const resetButton = form.querySelector('[data-reset]'); if (resetButton) { resetButton.addEventListener('click', () => { + cancelActiveRun(); setCode(originalCode); window.pythonByExampleEditor?.focus(); }); } let sharedCodeLoaded = false; + let sharedCodeRejected = false; const hash = new URL(window.location.href).hash; if (hash.startsWith('#code=')) { - try { - setCode(decodeURIComponent(escape(atob(hash.slice(6))))); - sharedCodeLoaded = textarea.value !== originalCode; - } catch (_) { - // Ignore malformed share fragments and leave the authored example intact. + const encodedCode = hash.slice(6); + if (encodedCode.length > MAX_SHARE_FRAGMENT_CHARS) { + sharedCodeRejected = true; + } else { + try { + setCode(decodeURIComponent(escape(atob(encodedCode)))); + sharedCodeLoaded = textarea.value !== originalCode; + } catch (_) { + sharedCodeRejected = true; + } } } @@ -85,9 +96,11 @@ function initializeRunner() { const outputPanel = document.querySelector('.output-panel'); if (!outputPanel) return; - if (sharedCodeLoaded) { + if (sharedCodeLoaded || sharedCodeRejected) { outputPanel.querySelector('h3').textContent = 'Output'; - outputPanel.querySelector('code').textContent = 'This link included edited code. Press Run to execute it.'; + outputPanel.querySelector('code').textContent = sharedCodeLoaded + ? 'This link included edited code. Press Run to execute it.' + : 'The shared-code fragment was invalid or too large, so the authored example was kept.'; } const challengeBox = document.querySelector('[data-turnstile-sitekey]'); let turnstileWidgetId = null; @@ -101,8 +114,20 @@ function initializeRunner() { script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; script.async = true; script.defer = true; - script.onload = resolve; - script.onerror = () => reject(new Error('Turnstile failed to load')); + script.onload = () => { + if (window.turnstile) { + resolve(); + return; + } + loadingTurnstile = null; + script.remove(); + reject(new Error('Turnstile loaded without its browser API')); + }; + script.onerror = () => { + loadingTurnstile = null; + script.remove(); + reject(new Error('Turnstile failed to load; press Run to retry')); + }; document.head.appendChild(script); }); return loadingTurnstile; @@ -126,27 +151,30 @@ function initializeRunner() { challengeBox.innerHTML = ''; } - async function requestTurnstileToken() { + async function requestTurnstileToken(signal) { if (!challengeBox) throw new Error('Turnstile challenge is not configured'); await loadTurnstile(); + if (signal.aborted) throw new DOMException('Run cancelled', 'AbortError'); challengeBox.hidden = false; return new Promise((resolve, reject) => { + let settled = false; + const finish = (callback, value) => { + if (settled) return; + settled = true; + signal.removeEventListener('abort', onAbort); + removeTurnstile(); + callback(value); + }; + const onAbort = () => finish(reject, new DOMException('Run cancelled', 'AbortError')); + signal.addEventListener('abort', onAbort, { once: true }); challengeBox.innerHTML = ''; turnstileWidgetId = turnstile.render(challengeBox, { sitekey: challengeBox.dataset.turnstileSitekey, + action: TURNSTILE_ACTION, execution: 'execute', - callback: (token) => { - removeTurnstile(); - resolve(token); - }, - 'error-callback': () => { - removeTurnstile(); - reject(new Error('Turnstile challenge failed')); - }, - 'expired-callback': () => { - removeTurnstile(); - reject(new Error('Turnstile challenge expired')); - }, + callback: token => finish(resolve, token), + 'error-callback': () => finish(reject, new Error('Turnstile challenge failed')), + 'expired-callback': () => finish(reject, new Error('Turnstile challenge expired')), }); turnstile.execute(turnstileWidgetId); }); @@ -162,37 +190,68 @@ function initializeRunner() { || `HTTP ${response.status}`; } - async function submitRun(turnstileToken = '') { - outputPanel.removeAttribute('data-output-placeholder'); + let latestRunId = 0; + let activeController = null; + const runButton = form.querySelector('button[type="submit"]'); + + async function submitRun(runId, signal, turnstileToken = '') { window.pythonByExampleEditor?.syncTextarea(); - outputPanel.querySelector('code').textContent = 'Running in a Dynamic Python Worker…'; const formData = new FormData(form); if (turnstileToken) formData.set('cf-turnstile-response', turnstileToken); const response = await fetch(form.action, { method: 'POST', body: new URLSearchParams(formData), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + signal, }); const html = await response.text(); + if (runId !== latestRunId) return; if (!response.ok) throw new Error(responseErrorMessage(response, html)); const document = new DOMParser().parseFromString(html, 'text/html'); const challengeRequired = document.querySelector('[data-turnstile-required]'); if (challengeRequired) { outputPanel.querySelector('code').textContent = challengeRequired.textContent || 'Verification required before running edited code…'; - return submitRun(await requestTurnstileToken()); + const token = await requestTurnstileToken(signal); + if (runId !== latestRunId) return; + return submitRun(runId, signal, token); } const nextOutput = document.querySelector('.output-panel'); if (nextOutput) outputPanel.innerHTML = nextOutput.innerHTML; } + cancelActiveRun = () => { + latestRunId += 1; + activeController?.abort(); + activeController = null; + removeTurnstile(); + form.removeAttribute('aria-busy'); + if (runButton) runButton.disabled = false; + }; + form.addEventListener('submit', async (event) => { event.preventDefault(); + activeController?.abort(); + const runId = ++latestRunId; + const controller = new AbortController(); + activeController = controller; + outputPanel.removeAttribute('data-output-placeholder'); + outputPanel.querySelector('code').textContent = 'Running in a Dynamic Python Worker…'; + form.setAttribute('aria-busy', 'true'); + if (runButton) runButton.disabled = true; try { - await submitRun(); + await submitRun(runId, controller.signal); } catch (error) { - removeTurnstile(); - outputPanel.querySelector('code').textContent = `Run failed: ${error.message}`; + if (runId === latestRunId && error.name !== 'AbortError') { + removeTurnstile(); + outputPanel.querySelector('code').textContent = `Run failed: ${error.message}`; + } + } finally { + if (runId === latestRunId) { + activeController = null; + form.removeAttribute('aria-busy'); + if (runButton) runButton.disabled = false; + } } }); } diff --git a/public/search.158bd7afa114.js b/public/search.e5a5822d9917.js similarity index 84% rename from public/search.158bd7afa114.js rename to public/search.e5a5822d9917.js index 2496724..9f18203 100644 --- a/public/search.158bd7afa114.js +++ b/public/search.e5a5822d9917.js @@ -34,16 +34,28 @@ function wireSearch() { const container = document.querySelector('.site-search'); const input = document.getElementById('site-search-input'); const results = document.getElementById('site-search-results'); - if (!container || !input || !results) return; + const status = document.getElementById('site-search-status'); + if (!container || !input || !results || !status) return; let entries = null; let loading = null; function loadIndex() { if (!loading) { loading = fetch(container.dataset.searchIndex) - .then((response) => response.json()) - .then((data) => { entries = data; }) - .catch(() => { loading = null; }); + .then((response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then((data) => { + if (!Array.isArray(data)) throw new Error('Search index is not an array'); + entries = data; + status.textContent = ''; + }) + .catch(() => { + loading = null; + entries = null; + status.textContent = 'Search is temporarily unavailable. Please browse the example sections below.'; + }); } return loading; } @@ -83,11 +95,14 @@ function wireSearch() { results.replaceChildren(empty); results.hidden = false; input.setAttribute('aria-expanded', 'true'); + status.textContent = 'No examples match your search.'; } else { hideResults(); + status.textContent = ''; } return; } + status.textContent = `${matches.length} matching example${matches.length === 1 ? '' : 's'}.`; results.replaceChildren(...matches.map(resultNode)); results.hidden = false; input.setAttribute('aria-expanded', 'true'); @@ -100,6 +115,7 @@ function wireSearch() { if (event.key === 'Escape') { input.value = ''; hideResults(); + status.textContent = ''; } else if (event.key === 'ArrowDown' && links.length) { event.preventDefault(); links[0].focus(); diff --git a/public/search.js b/public/search.js index 2496724..9f18203 100644 --- a/public/search.js +++ b/public/search.js @@ -34,16 +34,28 @@ function wireSearch() { const container = document.querySelector('.site-search'); const input = document.getElementById('site-search-input'); const results = document.getElementById('site-search-results'); - if (!container || !input || !results) return; + const status = document.getElementById('site-search-status'); + if (!container || !input || !results || !status) return; let entries = null; let loading = null; function loadIndex() { if (!loading) { loading = fetch(container.dataset.searchIndex) - .then((response) => response.json()) - .then((data) => { entries = data; }) - .catch(() => { loading = null; }); + .then((response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then((data) => { + if (!Array.isArray(data)) throw new Error('Search index is not an array'); + entries = data; + status.textContent = ''; + }) + .catch(() => { + loading = null; + entries = null; + status.textContent = 'Search is temporarily unavailable. Please browse the example sections below.'; + }); } return loading; } @@ -83,11 +95,14 @@ function wireSearch() { results.replaceChildren(empty); results.hidden = false; input.setAttribute('aria-expanded', 'true'); + status.textContent = 'No examples match your search.'; } else { hideResults(); + status.textContent = ''; } return; } + status.textContent = `${matches.length} matching example${matches.length === 1 ? '' : 's'}.`; results.replaceChildren(...matches.map(resultNode)); results.hidden = false; input.setAttribute('aria-expanded', 'true'); @@ -100,6 +115,7 @@ function wireSearch() { if (event.key === 'Escape') { input.value = ''; hideResults(); + status.textContent = ''; } else if (event.key === 'ArrowDown' && links.length) { event.preventDefault(); links[0].focus(); diff --git a/public/site.4ad21a6ce8a3.css b/public/site.07c59ae07f87.css similarity index 95% rename from public/site.4ad21a6ce8a3.css rename to public/site.07c59ae07f87.css index b700b56..088a60c 100644 --- a/public/site.4ad21a6ce8a3.css +++ b/public/site.07c59ae07f87.css @@ -1,6 +1,8 @@ :root { color-scheme: light; --accent: #FF4801; --accent-text: #C83800; --accent-action: #C83800; --accent-hover: #FF7038; --accent-soft: rgba(255, 72, 1, 0.08); --text: #521000; --muted: rgba(82, 16, 0, 0.7); --page: #F5F1EB; --surface: #FFFBF5; --surface-2: #FFFDFB; --surface-3: #FEF7ED; --hairline: #EBD5C1; --hairline-soft: rgba(235, 213, 193, 0.5); --header-veil: rgba(245, 241, 235, 0.82); --header-veil-0: rgba(245, 241, 235, 0); --header-veil-solid: rgba(245, 241, 235, 0.95); --figure-paper: #F5F1EB; --terminal-bg: #0b1020; --terminal-ink: #f9fafb; --space-1: .5rem; --space-2: .75rem; --space-3: 1rem; --space-4: 1.5rem; --space-5: 2rem; --space-6: 3rem; } * { box-sizing: border-box; } html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } + :where(a, button, input, textarea, [tabindex]):focus-visible { outline: 3px solid var(--accent-text); outline-offset: 3px; } + :where(button, input, textarea):disabled { cursor: not-allowed; opacity: .68; } body { max-width: 1040px; margin: 0 auto; padding: var(--space-4); color: var(--text); font: 100%/1.6 system-ui, -apple-system, Segoe UI, sans-serif; background: radial-gradient(circle at top left, rgba(255, 72, 1, 0.10), transparent 34rem), var(--page); } header { position: sticky; top: 0; z-index: 2; margin: 0 calc(-1 * var(--space-4)) var(--space-5); padding: var(--space-2) var(--space-4); backdrop-filter: blur(16px); background: var(--header-veil); } .skip-link { position: absolute; left: -9999px; } @@ -15,11 +17,14 @@ p, li { text-wrap: pretty; } pre { overflow: auto; padding: 1rem; border-radius: 1rem; background: var(--terminal-bg); color: var(--terminal-ink); box-shadow: 0 1px 1px rgba(0,0,0,.12), 0 12px 42px rgba(0,0,0,.18); } textarea { box-sizing: border-box; width: 100%; height: auto; min-height: 18rem; padding: 0; overflow: hidden; resize: vertical; border: 0; outline: 0; background: transparent; color: var(--text); font: 14px/1.5 ui-monospace, SF Mono, Fira Code, Consolas, monospace; tab-size: 4; transition-property: box-shadow; transition-duration: 150ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } - textarea:focus { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.18); } + textarea:focus-visible { outline: 3px solid var(--accent-text); outline-offset: 3px; box-shadow: 0 0 0 6px var(--accent-soft); } .textarea-fallback[hidden] { display: none; } .cm-editor { flex: 1; min-height: 18rem; background: transparent; color: var(--text); font: 14px/1.5 ui-monospace, SF Mono, Fira Code, Consolas, monospace; } - .cm-editor.cm-focused { outline: 0; box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.18); } - .cm-scroller { font-family: inherit; line-height: 1.5; } + .cm-editor.cm-focused { outline: 3px solid var(--accent-text); outline-offset: 3px; box-shadow: 0 0 0 6px var(--accent-soft); } + .cm-scroller { font-family: inherit; line-height: 1.5; scrollbar-color: var(--accent-text) var(--surface-3); scrollbar-width: thin; } + .cm-scroller::-webkit-scrollbar { width: 10px; height: 10px; } + .cm-scroller::-webkit-scrollbar-track { background: var(--surface-3); } + .cm-scroller::-webkit-scrollbar-thumb { border: 2px solid var(--surface-3); border-radius: 999px; background: var(--accent-text); } .cm-content { padding: 0; } .cm-line { padding: 0; } .cm-gutters { background: transparent; border-right: 1px solid var(--hairline-soft); color: var(--muted); font-variant-numeric: tabular-nums; } @@ -69,12 +74,13 @@ } .site-search { position: relative; width: 100%; max-width: none; margin: 0 0 var(--space-5); } .site-search input { width: 100%; min-height: 44px; padding: .6rem 1rem; border: 1px solid var(--hairline); border-radius: .75rem; background: var(--surface-2); color: var(--text); font: inherit; transition: border-color 160ms cubic-bezier(0.2, 0, 0, 1), box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } - .site-search input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } + .site-search input:focus-visible { outline: 3px solid var(--accent-text); outline-offset: 2px; border-color: var(--accent-text); box-shadow: 0 0 0 6px var(--accent-soft); } .site-search input::placeholder { color: var(--muted); } + .search-status { min-height: 1.4em; margin: .35rem 0 0; color: var(--muted); font-size: .88rem; } .search-results { position: absolute; inset-inline: 0; top: 100%; z-index: 3; margin: .35rem 0 0; padding: .35rem; list-style: none; border: 1px solid var(--hairline); border-radius: .75rem; background: var(--surface-2); box-shadow: 0 12px 42px rgba(0, 0, 0, .14); } .search-results li { margin: 0; } .search-results a { display: block; padding: .5rem .65rem; border-radius: .5rem; color: var(--text); text-decoration: none; } - .search-results a:hover, .search-results a:focus { background: var(--accent-soft); outline: 0; } + .search-results a:hover, .search-results a:focus-visible { background: var(--accent-soft); outline: 3px solid var(--accent-text); outline-offset: -1px; } .search-results .search-empty { padding: .5rem .65rem; color: var(--muted); } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } .home-section { margin-top: var(--space-6); } @@ -242,8 +248,7 @@ --header-veil-solid: rgba(27, 18, 11, 0.95); } .cell-banner figure svg, .journey-section-figure svg { padding: var(--space-2); border-radius: .5rem; background: var(--figure-paper); } - .button { color: #FFF7EF; } - .shiki, .shiki span { color: var(--shiki-dark) !important; } + .button { color: #2A0B00; } } /* Non-motion accessibility signals. Reduced motion is handled by the transition kill switch and no-preference gates above; these diff --git a/public/site.css b/public/site.css index b700b56..088a60c 100644 --- a/public/site.css +++ b/public/site.css @@ -1,6 +1,8 @@ :root { color-scheme: light; --accent: #FF4801; --accent-text: #C83800; --accent-action: #C83800; --accent-hover: #FF7038; --accent-soft: rgba(255, 72, 1, 0.08); --text: #521000; --muted: rgba(82, 16, 0, 0.7); --page: #F5F1EB; --surface: #FFFBF5; --surface-2: #FFFDFB; --surface-3: #FEF7ED; --hairline: #EBD5C1; --hairline-soft: rgba(235, 213, 193, 0.5); --header-veil: rgba(245, 241, 235, 0.82); --header-veil-0: rgba(245, 241, 235, 0); --header-veil-solid: rgba(245, 241, 235, 0.95); --figure-paper: #F5F1EB; --terminal-bg: #0b1020; --terminal-ink: #f9fafb; --space-1: .5rem; --space-2: .75rem; --space-3: 1rem; --space-4: 1.5rem; --space-5: 2rem; --space-6: 3rem; } * { box-sizing: border-box; } html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } + :where(a, button, input, textarea, [tabindex]):focus-visible { outline: 3px solid var(--accent-text); outline-offset: 3px; } + :where(button, input, textarea):disabled { cursor: not-allowed; opacity: .68; } body { max-width: 1040px; margin: 0 auto; padding: var(--space-4); color: var(--text); font: 100%/1.6 system-ui, -apple-system, Segoe UI, sans-serif; background: radial-gradient(circle at top left, rgba(255, 72, 1, 0.10), transparent 34rem), var(--page); } header { position: sticky; top: 0; z-index: 2; margin: 0 calc(-1 * var(--space-4)) var(--space-5); padding: var(--space-2) var(--space-4); backdrop-filter: blur(16px); background: var(--header-veil); } .skip-link { position: absolute; left: -9999px; } @@ -15,11 +17,14 @@ p, li { text-wrap: pretty; } pre { overflow: auto; padding: 1rem; border-radius: 1rem; background: var(--terminal-bg); color: var(--terminal-ink); box-shadow: 0 1px 1px rgba(0,0,0,.12), 0 12px 42px rgba(0,0,0,.18); } textarea { box-sizing: border-box; width: 100%; height: auto; min-height: 18rem; padding: 0; overflow: hidden; resize: vertical; border: 0; outline: 0; background: transparent; color: var(--text); font: 14px/1.5 ui-monospace, SF Mono, Fira Code, Consolas, monospace; tab-size: 4; transition-property: box-shadow; transition-duration: 150ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } - textarea:focus { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.18); } + textarea:focus-visible { outline: 3px solid var(--accent-text); outline-offset: 3px; box-shadow: 0 0 0 6px var(--accent-soft); } .textarea-fallback[hidden] { display: none; } .cm-editor { flex: 1; min-height: 18rem; background: transparent; color: var(--text); font: 14px/1.5 ui-monospace, SF Mono, Fira Code, Consolas, monospace; } - .cm-editor.cm-focused { outline: 0; box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.18); } - .cm-scroller { font-family: inherit; line-height: 1.5; } + .cm-editor.cm-focused { outline: 3px solid var(--accent-text); outline-offset: 3px; box-shadow: 0 0 0 6px var(--accent-soft); } + .cm-scroller { font-family: inherit; line-height: 1.5; scrollbar-color: var(--accent-text) var(--surface-3); scrollbar-width: thin; } + .cm-scroller::-webkit-scrollbar { width: 10px; height: 10px; } + .cm-scroller::-webkit-scrollbar-track { background: var(--surface-3); } + .cm-scroller::-webkit-scrollbar-thumb { border: 2px solid var(--surface-3); border-radius: 999px; background: var(--accent-text); } .cm-content { padding: 0; } .cm-line { padding: 0; } .cm-gutters { background: transparent; border-right: 1px solid var(--hairline-soft); color: var(--muted); font-variant-numeric: tabular-nums; } @@ -69,12 +74,13 @@ } .site-search { position: relative; width: 100%; max-width: none; margin: 0 0 var(--space-5); } .site-search input { width: 100%; min-height: 44px; padding: .6rem 1rem; border: 1px solid var(--hairline); border-radius: .75rem; background: var(--surface-2); color: var(--text); font: inherit; transition: border-color 160ms cubic-bezier(0.2, 0, 0, 1), box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } - .site-search input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } + .site-search input:focus-visible { outline: 3px solid var(--accent-text); outline-offset: 2px; border-color: var(--accent-text); box-shadow: 0 0 0 6px var(--accent-soft); } .site-search input::placeholder { color: var(--muted); } + .search-status { min-height: 1.4em; margin: .35rem 0 0; color: var(--muted); font-size: .88rem; } .search-results { position: absolute; inset-inline: 0; top: 100%; z-index: 3; margin: .35rem 0 0; padding: .35rem; list-style: none; border: 1px solid var(--hairline); border-radius: .75rem; background: var(--surface-2); box-shadow: 0 12px 42px rgba(0, 0, 0, .14); } .search-results li { margin: 0; } .search-results a { display: block; padding: .5rem .65rem; border-radius: .5rem; color: var(--text); text-decoration: none; } - .search-results a:hover, .search-results a:focus { background: var(--accent-soft); outline: 0; } + .search-results a:hover, .search-results a:focus-visible { background: var(--accent-soft); outline: 3px solid var(--accent-text); outline-offset: -1px; } .search-results .search-empty { padding: .5rem .65rem; color: var(--muted); } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } .home-section { margin-top: var(--space-6); } @@ -242,8 +248,7 @@ --header-veil-solid: rgba(27, 18, 11, 0.95); } .cell-banner figure svg, .journey-section-figure svg { padding: var(--space-2); border-radius: .5rem; background: var(--figure-paper); } - .button { color: #FFF7EF; } - .shiki, .shiki span { color: var(--shiki-dark) !important; } + .button { color: #2A0B00; } } /* Non-motion accessibility signals. Reduced motion is handled by the transition kill switch and no-preference gates above; these diff --git a/public/syntax-highlight.d6178fb3eef5.js b/public/syntax-highlight.c405e604fc10.js similarity index 52% rename from public/syntax-highlight.d6178fb3eef5.js rename to public/syntax-highlight.c405e604fc10.js index aa99729..4f9b866 100644 --- a/public/syntax-highlight.d6178fb3eef5.js +++ b/public/syntax-highlight.c405e604fc10.js @@ -1,7 +1,7 @@ // Copy affordance for read-only source blocks. The button attaches to -// the .cell-source wrapper (not the

) so the Shiki replacement
-// below leaves it in place, and it is injected client-side so no-JS
-// pages render no dead buttons.
+// the .cell-source wrapper (not the 
) so Shiki replacement leaves it
+// in place, and it is injected client-side so no-JS pages render no dead
+// controls.
 async function copyText(text) {
   if (navigator.clipboard && window.isSecureContext) {
     await navigator.clipboard.writeText(text);
@@ -42,7 +42,7 @@ for (const source of document.querySelectorAll('.cell-source')) {
       await copyText(code.textContent);
       button.classList.add('copied');
       status.textContent = 'Copied';
-    } catch (error) {
+    } catch (_) {
       button.classList.add('failed');
       status.textContent = 'Copy failed';
     }
@@ -54,33 +54,42 @@ for (const source of document.querySelectorAll('.cell-source')) {
   source.append(button);
 }
 
-// Shiki is imported dynamically so a CDN failure degrades to plain
-// server-rendered code while the copy buttons above keep working.
-let codeToHtml = null;
-try {
-  ({ codeToHtml } = await import('https://esm.sh/shiki@1.29.2'));
-} catch (error) {
-  codeToHtml = null;
-}
-
-const blocks = codeToHtml ? document.querySelectorAll('pre code.language-python') : [];
+// Read-only Python cells are the only pages that need Shiki. Avoid even
+// requesting the CDN module on the home, About, and other code-free pages.
+const sourceBlocks = [...document.querySelectorAll('pre code.language-python')].map((code) => ({
+  source: code.textContent,
+  pre: code.closest('pre'),
+}));
 
-for (const block of blocks) {
-  const source = block.textContent;
+if (sourceBlocks.length) {
+  let codeToHtml = null;
   try {
-    const highlighted = await codeToHtml(source, {
-      lang: 'python',
-      themes: { light: 'github-light', dark: 'github-dark' },
-      defaultColor: 'light',
-    });
-    const wrapper = document.createElement('div');
-    wrapper.innerHTML = highlighted;
-    const shikiPre = wrapper.firstElementChild;
-    if (!shikiPre) continue;
-    shikiPre.classList.add('shiki-block');
-    const currentPre = block.closest('pre');
-    currentPre.replaceWith(shikiPre);
-  } catch (error) {
-    block.dataset.highlightFailed = 'true';
+    ({ codeToHtml } = await import('https://esm.sh/shiki@1.29.2'));
+  } catch (_) {
+    // Plain server-rendered code remains readable when the optional CDN fails.
+  }
+
+  if (codeToHtml) {
+    const themePreference = window.matchMedia('(prefers-color-scheme: dark)');
+    const highlight = async () => {
+      const theme = themePreference.matches ? 'github-dark' : 'github-light';
+      for (const block of sourceBlocks) {
+        try {
+          const highlighted = await codeToHtml(block.source, { lang: 'python', theme });
+          const wrapper = document.createElement('div');
+          wrapper.innerHTML = highlighted;
+          const shikiPre = wrapper.firstElementChild;
+          if (!shikiPre || !block.pre?.isConnected) continue;
+          shikiPre.classList.add('shiki-block');
+          block.pre.replaceWith(shikiPre);
+          block.pre = shikiPre;
+        } catch (_) {
+          block.pre?.setAttribute('data-highlight-failed', 'true');
+        }
+      }
+    };
+
+    await highlight();
+    themePreference.addEventListener?.('change', highlight);
   }
 }
diff --git a/public/syntax-highlight.js b/public/syntax-highlight.js
index aa99729..4f9b866 100644
--- a/public/syntax-highlight.js
+++ b/public/syntax-highlight.js
@@ -1,7 +1,7 @@
 // Copy affordance for read-only source blocks. The button attaches to
-// the .cell-source wrapper (not the 
) so the Shiki replacement
-// below leaves it in place, and it is injected client-side so no-JS
-// pages render no dead buttons.
+// the .cell-source wrapper (not the 
) so Shiki replacement leaves it
+// in place, and it is injected client-side so no-JS pages render no dead
+// controls.
 async function copyText(text) {
   if (navigator.clipboard && window.isSecureContext) {
     await navigator.clipboard.writeText(text);
@@ -42,7 +42,7 @@ for (const source of document.querySelectorAll('.cell-source')) {
       await copyText(code.textContent);
       button.classList.add('copied');
       status.textContent = 'Copied';
-    } catch (error) {
+    } catch (_) {
       button.classList.add('failed');
       status.textContent = 'Copy failed';
     }
@@ -54,33 +54,42 @@ for (const source of document.querySelectorAll('.cell-source')) {
   source.append(button);
 }
 
-// Shiki is imported dynamically so a CDN failure degrades to plain
-// server-rendered code while the copy buttons above keep working.
-let codeToHtml = null;
-try {
-  ({ codeToHtml } = await import('https://esm.sh/shiki@1.29.2'));
-} catch (error) {
-  codeToHtml = null;
-}
-
-const blocks = codeToHtml ? document.querySelectorAll('pre code.language-python') : [];
+// Read-only Python cells are the only pages that need Shiki. Avoid even
+// requesting the CDN module on the home, About, and other code-free pages.
+const sourceBlocks = [...document.querySelectorAll('pre code.language-python')].map((code) => ({
+  source: code.textContent,
+  pre: code.closest('pre'),
+}));
 
-for (const block of blocks) {
-  const source = block.textContent;
+if (sourceBlocks.length) {
+  let codeToHtml = null;
   try {
-    const highlighted = await codeToHtml(source, {
-      lang: 'python',
-      themes: { light: 'github-light', dark: 'github-dark' },
-      defaultColor: 'light',
-    });
-    const wrapper = document.createElement('div');
-    wrapper.innerHTML = highlighted;
-    const shikiPre = wrapper.firstElementChild;
-    if (!shikiPre) continue;
-    shikiPre.classList.add('shiki-block');
-    const currentPre = block.closest('pre');
-    currentPre.replaceWith(shikiPre);
-  } catch (error) {
-    block.dataset.highlightFailed = 'true';
+    ({ codeToHtml } = await import('https://esm.sh/shiki@1.29.2'));
+  } catch (_) {
+    // Plain server-rendered code remains readable when the optional CDN fails.
+  }
+
+  if (codeToHtml) {
+    const themePreference = window.matchMedia('(prefers-color-scheme: dark)');
+    const highlight = async () => {
+      const theme = themePreference.matches ? 'github-dark' : 'github-light';
+      for (const block of sourceBlocks) {
+        try {
+          const highlighted = await codeToHtml(block.source, { lang: 'python', theme });
+          const wrapper = document.createElement('div');
+          wrapper.innerHTML = highlighted;
+          const shikiPre = wrapper.firstElementChild;
+          if (!shikiPre || !block.pre?.isConnected) continue;
+          shikiPre.classList.add('shiki-block');
+          block.pre.replaceWith(shikiPre);
+          block.pre = shikiPre;
+        } catch (_) {
+          block.pre?.setAttribute('data-highlight-failed', 'true');
+        }
+      }
+    };
+
+    await highlight();
+    themePreference.addEventListener?.('change', highlight);
   }
 }
diff --git a/pyproject.toml b/pyproject.toml
index dab24f6..e85073c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,6 +14,7 @@ workers = [
     "workers-runtime-sdk",
 ]
 dev = [
+    "pillow>=11.3,<12",
     "ruff>=0.8",
 ]
 
diff --git a/scripts/build_social_cards.py b/scripts/build_social_cards.py
index a89ff8e..d2001c2 100644
--- a/scripts/build_social_cards.py
+++ b/scripts/build_social_cards.py
@@ -15,6 +15,8 @@
 import sys
 from pathlib import Path
 
+from PIL import Image, UnidentifiedImageError
+
 ROOT = Path(__file__).resolve().parents[1]
 sys.path.insert(0, str(ROOT))
 
@@ -110,6 +112,19 @@ def provenance(cards: dict[str, str]) -> dict[str, object]:
     return {"version": PROVENANCE_VERSION, "inputs": {f"{name}.jpg": hashlib.sha256(source.encode()).hexdigest() for name, source in sorted(cards.items())}}
 
 
+def jpeg_dimensions(path: Path) -> tuple[int, int]:
+    """Decode enough of a JPEG to verify integrity and return its dimensions."""
+    try:
+        with Image.open(path) as image:
+            if image.format != "JPEG":
+                raise ValueError(f"expected JPEG format, got {image.format or 'unknown'}")
+            dimensions = image.size
+            image.load()
+    except (OSError, UnidentifiedImageError) as exc:
+        raise ValueError(str(exc)) from exc
+    return dimensions
+
+
 def check_current(cards: dict[str, str]) -> list[str]:
     failures: list[str] = []
     expected = provenance(cards)
@@ -125,6 +140,17 @@ def check_current(cards: dict[str, str]) -> list[str]:
         failures.append(f"missing social card {name}; run make social-cards")
     for name in sorted(actual_files - expected_files):
         failures.append(f"unexpected stale social card {name}; run make social-cards")
+    for name in sorted(expected_files & actual_files):
+        try:
+            dimensions = jpeg_dimensions(OUTPUT_DIR / name)
+        except (OSError, ValueError) as exc:
+            failures.append(f"invalid JPEG social card {name}: {exc}; run make social-cards")
+            continue
+        if dimensions != (CARD_WIDTH, CARD_HEIGHT):
+            failures.append(
+                f"social card {name} is {dimensions[0]}x{dimensions[1]}, "
+                f"expected {CARD_WIDTH}x{CARD_HEIGHT}; run make social-cards"
+            )
     return failures
 
 
diff --git a/scripts/capture_browser_screenshot.mjs b/scripts/capture_browser_screenshot.mjs
index cff96cc..78dc3a5 100755
--- a/scripts/capture_browser_screenshot.mjs
+++ b/scripts/capture_browser_screenshot.mjs
@@ -14,6 +14,8 @@ const url = `${target}${target.includes('?') ? '&' : '?'}screenshot_test=${Date.
 const width = Number(process.env.WIDTH || 2040);
 const height = Number(process.env.HEIGHT || 626);
 const captureSelector = process.env.CAPTURE_SELECTOR || '.literate-program';
+const colorScheme = process.env.COLOR_SCHEME || '';
+const focusSelector = process.env.FOCUS_SELECTOR || '';
 const port = 9444 + Math.floor(Math.random() * 1000);
 const profile = await mkdtemp(path.join(tmpdir(), 'pythonbyexample-shot-chrome-'));
 
@@ -82,6 +84,12 @@ try {
   await client.send('Network.setCacheDisabled', { cacheDisabled: true });
   await client.send('Page.enable');
   await client.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false });
+  if (colorScheme) {
+    await client.send('Emulation.setEmulatedMedia', {
+      media: 'screen',
+      features: [{ name: 'prefers-color-scheme', value: colorScheme }],
+    });
+  }
   await client.send('Page.navigate', { url });
 
   for (let i = 0; i < 100; i++) {
@@ -93,6 +101,13 @@ try {
     await sleep(100);
   }
 
+  if (focusSelector) {
+    await client.send('Runtime.evaluate', {
+      expression: `document.querySelector(${JSON.stringify(focusSelector)})?.focus()`,
+    });
+    await sleep(100);
+  }
+
   if (args.has('--legacy-shiki')) {
     await client.send('Runtime.evaluate', {
       awaitPromise: true,
diff --git a/scripts/check_browser_layout.mjs b/scripts/check_browser_layout.mjs
index 8286ee5..38d476e 100755
--- a/scripts/check_browser_layout.mjs
+++ b/scripts/check_browser_layout.mjs
@@ -182,10 +182,50 @@ try {
     throw new Error(`Timed out waiting for ${label}`);
   }
 
-  // Exercise the public browser contracts rather than merely asserting source
-  // strings: copy text, fragment sharing/restoration, resize, and arrow guards.
+  // Exercise public browser contracts rather than asserting implementation
+  // strings: run ordering/reset, sharing bounds, copy, resize, and arrow guards.
   await client.send('Page.navigate', { url: target });
   await waitFor("document.readyState === 'complete' && !!window.pythonByExampleEditor && !!document.querySelector('.copy-button') && !!document.querySelector('.share-button')", 'feature test page');
+  const runnerAsset = await evaluateValue(`document.querySelector('script[src*="/runner."]')?.src`);
+  const runnerOrdering = await evaluateValue(`(async () => {
+    const form = document.querySelector('form.runner-editor');
+    const output = document.querySelector('.output-panel code');
+    const pending = [];
+    const originalFetch = window.fetch;
+    window.fetch = () => new Promise(resolve => pending.push(resolve));
+    const response = text => ({
+      ok: true,
+      status: 200,
+      text: async () => '

Output

' + text + '
', + }); + window.pythonByExampleEditor.setValue('print("first")'); + form.requestSubmit(); + await new Promise(resolve => setTimeout(resolve, 0)); + window.pythonByExampleEditor.setValue('print("second")'); + form.requestSubmit(); + await new Promise(resolve => setTimeout(resolve, 0)); + if (pending.length !== 2) { + window.fetch = originalFetch; + return { error: 'concurrent submissions did not create two requests', count: pending.length }; + } + pending[1](response('second result')); + await new Promise(resolve => setTimeout(resolve, 0)); + pending[0](response('stale first result')); + await new Promise(resolve => setTimeout(resolve, 20)); + const finalOutput = document.querySelector('.output-panel code')?.textContent; + const reset = form.querySelector('[data-reset]'); + reset.click(); + await new Promise(resolve => setTimeout(resolve, 0)); + const resetValue = document.getElementById('code-editor').value; + window.fetch = originalFetch; + return { + finalOutput, + resetType: reset.type, + resetValue, + originalCode: document.getElementById('code-editor').dataset.originalCode, + busy: form.hasAttribute('aria-busy'), + }; + })()`); const share = await evaluateValue(`(async () => { const writes = []; window.__pbeClipboardWrites = writes; @@ -265,8 +305,63 @@ try { searchWidths.push(await searchWidthAt('desktop', 1200, 900, false)); searchWidths.push(await searchWidthAt('mobile-portrait', 390, 844, true)); searchWidths.push(await searchWidthAt('mobile-landscape', 844, 390, true)); + const searchFailure = await evaluateValue(`(async () => { + const originalFetch = window.fetch; + window.fetch = async () => ({ ok: false, status: 503, json: async () => ({}) }); + const input = document.getElementById('site-search-input'); + input.focus(); + input.value = 'decorator'; + input.dispatchEvent(new Event('input', { bubbles: true })); + await new Promise(resolve => setTimeout(resolve, 50)); + const state = { + status: document.getElementById('site-search-status')?.textContent, + outlineWidth: getComputedStyle(input).outlineWidth, + shikiRequests: performance.getEntriesByType('resource').filter(entry => entry.name.includes('esm.sh/shiki')).length, + }; + window.fetch = originalFetch; + return state; + })()`); await client.send('Emulation.setDeviceMetricsOverride', { width: 390, height: 1800, deviceScaleFactor: 2, mobile: true }); + // Theme changes are a live OS preference, not a reload-only setting. + await client.send('Emulation.setEmulatedMedia', { + media: 'screen', + features: [{ name: 'prefers-color-scheme', value: 'light' }], + }); + await client.send('Page.navigate', { url: `${target}?browser_theme=light` }); + await waitFor("document.readyState === 'complete' && !!document.querySelector('.shiki-block') && !!document.querySelector('.cm-editor')", 'light theme page'); + const lightTheme = await evaluateValue(`({ + shikiColor: getComputedStyle(document.querySelector('.shiki-block span')).color, + editorColors: [...new Set([...document.querySelectorAll('.cm-line span')].map(node => getComputedStyle(node).color))].sort(), + })`); + await client.send('Emulation.setEmulatedMedia', { + media: 'screen', + features: [{ name: 'prefers-color-scheme', value: 'dark' }], + }); + await waitFor(`getComputedStyle(document.querySelector('.shiki-block span')).color !== ${JSON.stringify(lightTheme.shikiColor)}`, 'live dark-theme Shiki rerender'); + const darkTheme = await evaluateValue(`(() => { + const parse = color => color.match(/[\\d.]+/g).slice(0, 3).map(Number); + const luminance = color => { + const channels = parse(color).map(value => { + const normalized = value / 255; + return normalized <= .04045 ? normalized / 12.92 : ((normalized + .055) / 1.055) ** 2.4; + }); + return .2126 * channels[0] + .7152 * channels[1] + .0722 * channels[2]; + }; + const button = document.querySelector('button[type="submit"]'); + const style = getComputedStyle(button); + const a = luminance(style.color); + const b = luminance(style.backgroundColor); + document.querySelector('.cm-content').focus(); + const editorStyle = getComputedStyle(document.querySelector('.cm-editor')); + return { + shikiColor: getComputedStyle(document.querySelector('.shiki-block span')).color, + editorColors: [...new Set([...document.querySelectorAll('.cm-line span')].map(node => getComputedStyle(node).color))].sort(), + runContrast: (Math.max(a, b) + .05) / (Math.min(a, b) + .05), + editorOutlineWidth: editorStyle.outlineWidth, + }; + })()`); + // Keep esm.sh import requests pending. The runner has no imports and is // emitted after its markup, so its controls must attach before the editor's // CDN graph can let DOMContentLoaded fire. @@ -284,6 +379,90 @@ try { removePauseListener(); await client.send('Fetch.disable'); + const oversizedUrl = new URL(target); + oversizedUrl.searchParams.set('browser_oversized_fragment', Date.now()); + oversizedUrl.hash = `code=${'A'.repeat(24001)}`; + await client.send('Page.navigate', { url: oversizedUrl.href }); + await waitFor("!!document.querySelector('.share-button')", 'oversized-fragment page'); + const fragmentBound = await evaluateValue(`({ + unchanged: document.getElementById('code-editor').value === document.getElementById('code-editor').dataset.originalCode, + notice: document.querySelector('.output-panel code')?.textContent, + })`); + + // Build a dependency-free runner fixture on a code-free page. The first + // Turnstile script load fails; the second must create a fresh script, + // execute with the fixed action, and submit the returned token. + await client.send('Page.navigate', { url: `${new URL(target).origin}/?browser_turnstile_retry=${Date.now()}` }); + await waitFor("document.readyState === 'complete'", 'Turnstile retry fixture page'); + const turnstileRetry = await evaluateValue(`(async () => { + document.body.innerHTML = \` +
+
+ + +
+ + +
+
+

Output

Ready
+
\`; + const nativeAppend = document.head.appendChild.bind(document.head); + const nativeFetch = window.fetch; + let scriptAttempts = 0; + let renderAction = ''; + let widgetOptions = null; + document.head.appendChild = node => { + if (node.tagName === 'SCRIPT' && node.src.includes('challenges.cloudflare.com/turnstile')) { + scriptAttempts += 1; + setTimeout(() => { + if (scriptAttempts === 1) { + node.onerror?.(new Event('error')); + return; + } + window.turnstile = { + render: (_box, options) => { widgetOptions = options; renderAction = options.action; return 7; }, + execute: () => queueMicrotask(() => widgetOptions.callback('retry-token')), + remove: () => {}, + reset: () => {}, + }; + node.onload?.(new Event('load')); + }, 0); + return node; + } + return nativeAppend(node); + }; + let fetchCount = 0; + let submittedToken = ''; + window.fetch = async (_url, options) => { + fetchCount += 1; + submittedToken = options.body.get('cf-turnstile-response') || submittedToken; + const html = fetchCount < 3 + ? '
Verification required
' + : '

Output

retry succeeded
'; + return { ok: true, status: 200, text: async () => html }; + }; + await import(${JSON.stringify(runnerAsset)} + '?retry=' + Date.now()); + const form = document.querySelector('form.runner-editor'); + const output = () => document.querySelector('.output-panel code')?.textContent || ''; + const waitUntil = async predicate => { + for (let i = 0; i < 100; i++) { + if (predicate()) return true; + await new Promise(resolve => setTimeout(resolve, 10)); + } + return false; + }; + form.requestSubmit(); + await waitUntil(() => output().includes('press Run to retry')); + const firstFailure = output(); + form.requestSubmit(); + await waitUntil(() => output().includes('retry succeeded')); + const finalOutput = output(); + document.head.appendChild = nativeAppend; + window.fetch = nativeFetch; + return { scriptAttempts, fetchCount, renderAction, submittedToken, firstFailure, finalOutput }; + })()`); + const failures = []; if (interaction.result.value?.ariaLabel !== 'Editable Python example code') { failures.push('CodeMirror editor is missing its accessible name'); @@ -295,6 +474,9 @@ try { if (block.ratio > 1.25) failures.push(`block ${block.index} visual line height ratio ${block.ratio.toFixed(2)} > 1.25`); } if (!metrics.loadedShiki) failures.push('Shiki did not load'); + if (runnerOrdering.error) failures.push(runnerOrdering.error); + if (runnerOrdering.finalOutput !== 'second result') failures.push(`Stale run overwrote the latest output (${runnerOrdering.finalOutput})`); + if (runnerOrdering.resetType !== 'reset' || runnerOrdering.resetValue !== runnerOrdering.originalCode || runnerOrdering.busy) failures.push('Native Reset did not restore code and cancel runner state'); if (!share.url.includes('#code=')) failures.push('Copy link did not copy a shared-code fragment'); if (restored.notice !== 'This link included edited code. Press Run to execute it.') failures.push('Shared-code recipient notice did not render'); if (restored.editorHeight < 1000) failures.push(`Shared CodeMirror payload did not resize the editor (${restored.editorHeight}px)`); @@ -309,18 +491,34 @@ try { failures.push(`${measurement.label} search is not full width (${measurement.searchWidth}px search, ${measurement.mainWidth}px main, ${measurement.inputWidth}px input)`); } } + if (!searchFailure.status?.includes('temporarily unavailable')) failures.push('Search fetch failure was not announced'); + if (Number.parseFloat(searchFailure.outlineWidth) < 3) failures.push('Search focus indicator is too weak'); + if (searchFailure.shikiRequests !== 0) failures.push('Code-free homepage unnecessarily loaded Shiki'); + if (lightTheme.shikiColor === darkTheme.shikiColor) failures.push('Shiki did not react to the OS theme change'); + if (JSON.stringify(lightTheme.editorColors) === JSON.stringify(darkTheme.editorColors)) failures.push('CodeMirror did not react to the OS theme change'); + if (darkTheme.runContrast < 4.5) failures.push(`Dark Run-button contrast is ${darkTheme.runContrast.toFixed(2)}:1`); + if (Number.parseFloat(darkTheme.editorOutlineWidth) < 3) failures.push('CodeMirror focus indicator is too weak'); if (heldCdnRequests === 0) failures.push('CDN-pending runner test did not hold an esm.sh request'); if (offlineRunner.editorLoaded) failures.push('CDN-pending test unexpectedly loaded CodeMirror'); if (offlineRunner.fallbackHeight < 1000) failures.push(`Shared fallback textarea did not resize (${offlineRunner.fallbackHeight}px)`); + if (!fragmentBound.unchanged || !fragmentBound.notice?.includes('invalid or too large')) failures.push('Oversized shared-code fragment was decoded or not announced'); + if (turnstileRetry.scriptAttempts !== 2 || !turnstileRetry.firstFailure.includes('press Run to retry') || turnstileRetry.finalOutput !== 'retry succeeded') failures.push('Transient Turnstile CDN failure was cached instead of retried'); + if (turnstileRetry.renderAction !== 'run-example' || turnstileRetry.submittedToken !== 'retry-token') failures.push('Turnstile browser action/token contract failed'); console.log(JSON.stringify({ runnerInteraction: interaction.result.value, + runnerOrdering, share: { codeLength: share.code.length, urlLength: share.url.length, editorHeight: share.editorHeight }, restored, sourceCopy: { copiedLength: sourceCopy.copied.length, state: sourceCopy.state, status: sourceCopy.status }, arrow, searchWidths, + searchFailure, + lightTheme, + darkTheme, offlineRunner, + fragmentBound, + turnstileRetry, heldCdnRequests, }, null, 2)); client.close(); diff --git a/src/app.py b/src/app.py index 93059b9..cd3d3a7 100644 --- a/src/app.py +++ b/src/app.py @@ -109,7 +109,22 @@ def write(self, value): return len(value) def getvalue(self): - return "".join(self.parts) + # Submitted code receives sys.stdout and can mutate its attributes + # directly. Revalidate the sink after exec before joining anything, + # so appending around write() cannot bypass the byte cap. + if type(self.parts) is not list: + raise OutputLimitExceeded + checked = [] + total = 0 + for value in self.parts: + if type(value) is not str or len(value) > MAX_OUTPUT_BYTES - total: + raise OutputLimitExceeded + encoded = value.encode("utf-8") + total += len(encoded) + if total > MAX_OUTPUT_BYTES: + raise OutputLimitExceeded + checked.append(value) + return "".join(checked) class Default(WorkerEntrypoint): @@ -119,7 +134,7 @@ async def fetch(self, request): try: with contextlib.redirect_stdout(stdout): exec(EXAMPLE_CODE, namespace) - return Response(stdout.getvalue(), headers={{"Content-Type": "text/plain; charset=utf-8"}}) + return Response(BoundedStdout.getvalue(stdout), headers={{"Content-Type": "text/plain; charset=utf-8"}}) except OutputLimitExceeded: return Response(OUTPUT_LIMIT_MESSAGE, status=413, headers={{"Content-Type": "text/plain; charset=utf-8"}}) except Exception: @@ -197,9 +212,12 @@ def _structured_data_script(data: dict) -> str: return f'' -def _layout(title: str, content: str, description: str | None = None, path: str = "/", og_type: str = "website", include_editor: bool = False, include_search: bool = False, structured_data: dict | None = None, og_image: str | None = None) -> str: +def _layout(title: str, content: str, description: str | None = None, path: str | None = "/", og_type: str = "website", include_editor: bool = False, include_search: bool = False, structured_data: dict | None = None, og_image: str | None = None) -> str: description = _meta_description(description or "Learn Python with concise, editable examples that run in isolated Cloudflare Dynamic Python Workers.") - canonical_url = f"{SITE_URL}{path}" + canonical_url = f"{SITE_URL}{path}" if path is not None else "" + canonical_tag = f'' if canonical_url else "" + og_url_tag = f'' if canonical_url else "" + robots_meta = "" if canonical_url else '' page_title = title if title == "Python By Example" else f"{title} · Python By Example" # The CDN-backed editor stays in the head, but the dependency-free runner # is emitted after page content. Its async module can then attach Run, @@ -219,7 +237,9 @@ def _layout(title: str, content: str, description: str | None = None, path: str "REFERENCE_URL": html.escape(REFERENCE_URL), "PYTHON_VERSION": html.escape(PYTHON_VERSION), "META_DESCRIPTION": html.escape(description), - "CANONICAL_URL": html.escape(canonical_url), + "CANONICAL_TAG": canonical_tag, + "OG_URL_TAG": og_url_tag, + "ROBOTS_META": robots_meta, "OG_TYPE": html.escape(og_type), "SITE_CSS": html.escape(ASSET_PATHS["SITE_CSS"]), "SYNTAX_JS": html.escape(ASSET_PATHS["SYNTAX_JS"]), @@ -426,15 +446,15 @@ def render_example_not_found(slug: str) -> str: '

Try one of these nearby examples.

' f"

Recommended examples

    {recommendations}
" ) - return _layout("Not Found", content) + return _layout("Not Found", content, path=None) def render_journey_not_found() -> str: - return _layout("Not Found", "

Journey not found

") + return _layout("Not Found", "

Journey not found

", path=None) def render_not_found() -> str: - return _layout("Not Found", "

Not found

") + return _layout("Not Found", "

Not found

", path=None) def _example_neighbors(slug): @@ -692,6 +712,12 @@ def render_example_page( def route(url: str, method: str = "GET", turnstile_site_key: str | None = None) -> AppResponse: + """Legacy pure-rendering adapter retained for focused renderer tests. + + Production HTTP routing, middleware, caching, POST handling, and security + headers live in ``src.main``. New transport behavior must be tested through + that ASGI application rather than added here. + """ without_scheme = url.split("://", 1)[-1] path_part = without_scheme.split("/", 1)[1] if "/" in without_scheme else "" path = ("/" + path_part.split("?", 1)[0]).rstrip("/") or "/" @@ -719,7 +745,7 @@ def route(url: str, method: str = "GET", turnstile_site_key: str | None = None) slug = path.split("/", 2)[2] journey = JOURNEYS_BY_SLUG.get(slug) if journey is None: - return AppResponse(_layout("Not Found", "

Journey not found

"), status=404) + return AppResponse(_layout("Not Found", "

Journey not found

", path=None), status=404) return AppResponse(render_journey_page(journey), headers={"Content-Type": "text/html; charset=utf-8"}) if path.startswith("/examples/"): slug = path.split("/", 2)[2] @@ -730,9 +756,9 @@ def route(url: str, method: str = "GET", turnstile_site_key: str | None = None) for item in _recommended_examples(slug) ) body = f'

Example not found

Try one of these nearby examples.

Recommended examples

    {recommendations}
' - return AppResponse(_layout("Not Found", body), status=404) + return AppResponse(_layout("Not Found", body, path=None), status=404) return AppResponse( render_example_page(example, turnstile_site_key=turnstile_site_key), headers={"Content-Type": "text/html; charset=utf-8"}, ) - return AppResponse(_layout("Not Found", "

Not found

"), status=404) + return AppResponse(_layout("Not Found", "

Not found

", path=None), status=404) diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 18c2f38..f269ba4 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.4ad21a6ce8a3.css', 'SYNTAX_JS': '/syntax-highlight.d6178fb3eef5.js', 'EDITOR_JS': '/editor.a550bf2266bd.js', 'RUNNER_JS': '/runner.d3515b57bc3e.js', 'SEARCH_JS': '/search.158bd7afa114.js', 'SEARCH_INDEX': '/search-index.332712b2e502.json'} -HTML_CACHE_VERSION = '9ae21c5f386e' +ASSET_PATHS = {'SITE_CSS': '/site.07c59ae07f87.css', 'SYNTAX_JS': '/syntax-highlight.c405e604fc10.js', 'EDITOR_JS': '/editor.3827f6497b54.js', 'RUNNER_JS': '/runner.96b105eef218.js', 'SEARCH_JS': '/search.e5a5822d9917.js', 'SEARCH_INDEX': '/search-index.332712b2e502.json'} +HTML_CACHE_VERSION = '3f7b7d26c98f' diff --git a/src/main.py b/src/main.py index 09724b9..7a59b8c 100644 --- a/src/main.py +++ b/src/main.py @@ -12,8 +12,10 @@ from workers import WorkerEntrypoint, python_from_rpc from app import ( + DYNAMIC_OUTPUT_LIMIT_MESSAGE, FAVICON_SVG, JOURNEYS_BY_SLUG, + MAX_DYNAMIC_OUTPUT_BYTES, build_dynamic_worker_code, get_example, render_about, @@ -36,6 +38,7 @@ import worker_asgi_bridge as asgi TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" +TURNSTILE_ACTION = "run-example" SMOKE_BYPASS_HEADER = "x-pythonbyexample-smoke-secret" TURNSTILE_CLEARANCE_COOKIE = "pbe_turnstile_clearance" DEFAULT_TURNSTILE_CLEARANCE_SECONDS = 60 * 60 * 8 @@ -344,6 +347,7 @@ async def run_example(slug: str, request: Request): elif event := _wide_event(request): event["turnstile"] = {"outcome": _turnstile_not_required_outcome(request)} + request.state.dynamic_worker_status = 200 started = time.perf_counter() try: output = await _run_example(request, example["slug"], submitted) @@ -358,7 +362,8 @@ async def run_example(slug: str, request: Request): code=submitted, execution_time_ms=elapsed_ms, turnstile_site_key=_turnstile_site_key(request), - ) + ), + status=413 if request.state.dynamic_worker_status == 413 else 200, ) if turnstile_verified: _set_turnstile_clearance(response, request) @@ -411,11 +416,50 @@ async def _verify_turnstile(request: Request, token: str) -> tuple[bool, str, st raise ValueError("Turnstile Siteverify returned a non-object payload") except Exception: return False, "Turnstile verification failed. Please refresh the challenge and try again.", "fail" - if result.get("success") is True: + expected_hostname = urlparse(str(request.url)).hostname or "" + if ( + result.get("success") is True + and result.get("hostname", "").lower().rstrip(".") == expected_hostname.lower().rstrip(".") + and result.get("action") == TURNSTILE_ACTION + ): return True, "", "pass" return False, "Turnstile verification failed. Please refresh the challenge and try again.", "fail" +async def _read_dynamic_response_text(response) -> tuple[str, bool]: + """Read a Dynamic Worker response without trusting its declared status. + + Submitted code controls the child runtime and may tamper with Python-level + stdout internals. The parent therefore enforces the same byte limit while + consuming the Fetch response stream and cancels as soon as it is exceeded. + """ + body = getattr(response, "body", None) + if body is not None and hasattr(body, "getReader"): + reader = body.getReader() + chunks: list[bytes] = [] + total = 0 + while True: + result = await reader.read() + if bool(getattr(result, "done", False)): + break + value = getattr(result, "value", None) + chunk = value.to_bytes() if hasattr(value, "to_bytes") else bytes(value) + total += len(chunk) + if total > MAX_DYNAMIC_OUTPUT_BYTES: + try: + await reader.cancel("Dynamic Worker output exceeded limit") + except Exception: + pass + return DYNAMIC_OUTPUT_LIMIT_MESSAGE, True + chunks.append(chunk) + return b"".join(chunks).decode("utf-8", errors="replace"), False + + output = await response.text() + if len(output.encode("utf-8")) > MAX_DYNAMIC_OUTPUT_BYTES: + return DYNAMIC_OUTPUT_LIMIT_MESSAGE, True + return output, False + + async def _run_example(request: Request, slug, code): event = _wide_event(request) if event is None: @@ -466,8 +510,11 @@ def provide_worker_code(): ) response = python_from_rpc(await entrypoint.fetch(dynamic_request)) status_code = int(getattr(response, "status", 200) or 200) + output, output_limit_exceeded = await _read_dynamic_response_text(response) + if output_limit_exceeded: + status_code = 413 worker_event["status_code"] = status_code - output = await response.text() + request.state.dynamic_worker_status = status_code if status_code == 413: worker_event["outcome"] = "rejected" worker_event["rejected"] = "output_too_large" @@ -553,6 +600,7 @@ async def fetch(self, request): max_body_bytes=MAX_SUBMITTED_BODY_BYTES, ) _apply_security_headers(response) + observability.record_status(event, getattr(response, "status", 200)) if getattr(response, "status", 200) == 200: response.headers.set( "Cache-Control", @@ -572,6 +620,7 @@ async def fetch(self, request): max_body_bytes=MAX_SUBMITTED_BODY_BYTES, ) _apply_security_headers(response) + observability.record_status(event, getattr(response, "status", 200)) response.headers.set("Cache-Control", "no-store") return response @@ -584,6 +633,8 @@ async def fetch(self, request): max_body_bytes=MAX_SUBMITTED_BODY_BYTES, ) _apply_security_headers(response) + observability.record_status(event, getattr(response, "status", 200)) + response.headers.set("Cache-Control", "no-store") return response except Exception as exc: event["status_code"] = 500 diff --git a/src/marginalia.py b/src/marginalia.py index cdba872..187054d 100644 --- a/src/marginalia.py +++ b/src/marginalia.py @@ -1,7 +1,8 @@ """Marginalia attachments and figure registry. -Authors of example markdown never touch this file. The project owner curates -named figures here and attaches them to cells via slug + anchor. +Example prose and code live in Markdown; illustration definitions and their +slug/anchor attachments are curated here. Figure quality metadata lives in the +``[figures]`` table of ``docs/quality-registries.toml``. Anchors today: "cell-0", "cell-1", … each literate-program cell, zero-indexed. diff --git a/src/templates/about.html b/src/templates/about.html index a007c4f..5aff8d7 100644 --- a/src/templates/about.html +++ b/src/templates/about.html @@ -15,7 +15,7 @@

Every page runs

Output is verified, not promised

-

The expected output beside each fragment is not written by hand. A build step executes every teaching cell and every complete program, then fails if the recorded output differs from what actually printed. Cells the sandbox cannot execute are labelled as standard Python instead of pretending to run.

+

Expected output is authored in each example's Markdown source and rendered beside the corresponding fragment. The build executes every supported teaching cell and complete program, then fails if captured stdout differs from that authored expectation. Cells the sandbox cannot execute are labelled as standard Python instead of pretending to run.

The figures

diff --git a/src/templates/example.html b/src/templates/example.html index 87d7898..7a7973e 100644 --- a/src/templates/example.html +++ b/src/templates/example.html @@ -1,8 +1,8 @@
-
+

__SECTION__

-

__TITLE__

+

__TITLE__

__SUMMARY__

__WALKTHROUGH__
@@ -19,10 +19,10 @@

Example code

__TURNSTILE_CHALLENGE__
- +
-

__OUTPUT_HEADING__

__SHOWN_OUTPUT__

__EXECUTION_TIME__

+

__OUTPUT_HEADING__

__SHOWN_OUTPUT__

__EXECUTION_TIME__

diff --git a/src/templates/home.html b/src/templates/home.html index b0d1cd8..a6cc1c6 100644 --- a/src/templates/home.html +++ b/src/templates/home.html @@ -1,10 +1,11 @@ -
-

Python By Example

+
+

Python By Example

Learn Python with small, editable examples backed by the official Python __PYTHON_VERSION__ docs. Run each snippet in an isolated Dynamic Python Worker using the newest Python version currently supported by Cloudflare Workers/Pyodide.

__CARDS__ diff --git a/src/templates/layout.html b/src/templates/layout.html index d9abf85..4512ed2 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -5,11 +5,12 @@ __PAGE_TITLE__ - + __ROBOTS_META__ + __CANONICAL_TAG__ - + __OG_URL_TAG__ __SOCIAL_IMAGE_TAGS__ diff --git a/src/worker_asgi_bridge.py b/src/worker_asgi_bridge.py index 8d4a704..faf01ec 100644 --- a/src/worker_asgi_bridge.py +++ b/src/worker_asgi_bridge.py @@ -212,13 +212,13 @@ async def send(got): with acquire_js_buffer(body) as jsbytes: await writer.write(jsbytes.slice()) else: - # Complete body in a single chunk - px = create_proxy(body) - buf = px.getBuffer() - px.destroy() - resp = Response.new( - buf.data, headers=Object.fromEntries(headers), status=status - ) + # Complete body in a single chunk. Response construction may + # retain the source view, so copy it before releasing the + # Pyodide buffer handle just as the streaming path does. + with acquire_js_buffer(body) as jsbytes: + resp = Response.new( + jsbytes.slice(), headers=Object.fromEntries(headers), status=status + ) result.set_result(resp) finished_response.set() @@ -270,7 +270,7 @@ def onopen(evt): onopen(1) def onclose(evt): - msg = {"type": "websocket.close", "code": evt.code, "reason": evt.reason} + msg = {"type": "websocket.disconnect", "code": evt.code, "reason": evt.reason} queue.put_nowait(msg) def onmessage(evt): @@ -287,12 +287,13 @@ async def ws_send(got): s = got.get("text", None) if b: with acquire_js_buffer(b) as jsbytes: - # Unlike the `Response` constructor, server.send seems to - # eagerly copy the source buffer + # Unlike the `Response` constructor, server.send eagerly + # copies the source buffer. server.send(jsbytes) if s: server.send(s) - + elif got["type"] == "websocket.close": + server.close(got.get("code", 1000), got.get("reason", "")) else: logger.warning(" == Not implemented %s", got["type"]) diff --git a/tests/test_app.py b/tests/test_app.py index 069a3d7..fa0db64 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -376,7 +376,7 @@ def test_cf_workers_design_system_and_playground_lessons(self): self.assertNotIn('border-style: dashed', html) self.assertIn('class="playground-toolbar"', html) self.assertIn('type="submit">Run', html) - self.assertIn('type="button" data-reset', html) + self.assertIn('type="reset" data-reset', html) self.assertNotIn('data-copy', html) self.assertNotIn('data-share', html) self.assertIn('output-panel', html) @@ -430,6 +430,15 @@ def test_generated_drift_is_blocked_before_commit_and_merge(self): self.assertIn("make check-generated", hook) self.assertIn(".githooks/pre-commit", installer) self.assertIn("make verify", verify_workflow) + self.assertIn("uv sync --locked --all-groups", verify_workflow) + self.assertIn("npm ci --ignore-scripts", verify_workflow) + preview_workflow = (ROOT / ".github" / "workflows" / "preview.yml").read_text() + self.assertIn("uv sync --locked --all-groups", preview_workflow) + self.assertIn("npm exec -- wrangler whoami", preview_workflow) + package = json.loads((ROOT / "package.json").read_text()) + lock = json.loads((ROOT / "package-lock.json").read_text()) + self.assertEqual(package["devDependencies"]["wrangler"], "4.110.0") + self.assertEqual(lock["packages"][""]["devDependencies"]["wrangler"], "4.110.0") self.assertFalse((ROOT / ".github" / "workflows" / "regenerate-generated-files.yml").exists()) def test_conditionals_are_substantive_not_bare_minimum(self): @@ -567,18 +576,12 @@ def test_dark_mode_keeps_marginalia_figures_on_light_paper(self): self.assertIn(".cell-banner figure svg", dark_block) self.assertIn(".journey-section-figure svg", dark_block) - def test_shiki_highlights_with_dual_light_dark_themes(self): - js = (ROOT / "public" / "syntax-highlight.js").read_text() - self.assertIn("github-light", js) - self.assertIn("github-dark", js) - self.assertIn("themes:", js) - css = (ROOT / "public" / "site.css").read_text() - self.assertIn("--shiki-dark", css) - - def test_editor_picks_a_dark_highlight_style_in_dark_mode(self): - js = (ROOT / "public" / "editor.js").read_text() - self.assertIn("prefers-color-scheme: dark", js) - self.assertIn("oneDarkHighlightStyle", js) + def test_optional_highlighters_retain_readable_server_fallbacks(self): + page = render_example_page(get_example("values")) + self.assertIn('
', page)
+        self.assertIn('aria-label="Editable Python example code"', page)
+        self.assertIn('"))
 
+    def test_404_pages_omit_canonical_and_og_url_metadata(self):
+        for response in (
+            route("https://example.test/no-such-page"),
+            route("https://example.test/examples/no-such-example"),
+            route("https://example.test/journeys/no-such-journey"),
+        ):
+            with self.subTest(body=response.body[:80]):
+                self.assertEqual(response.status, 404)
+                self.assertNotIn('rel="canonical"', response.body)
+                self.assertNotIn('property="og:url"', response.body)
+                self.assertIn('', response.body)
+
     def test_robots_txt_references_sitemap(self):
         robots = (ROOT / "public" / "robots.txt").read_text()
         self.assertIn("Sitemap: https://www.pythonbyexample.dev/sitemap.xml", robots)
@@ -999,15 +1014,6 @@ def test_share_button_copies_a_code_fragment_link(self):
         self.assertIn(".playground-toolbar", js)
         self.assertIn("aria-live", js)
 
-    def test_share_encoder_mirrors_the_day_one_decoder(self):
-        js = (ROOT / "public" / "runner.js").read_text()
-        self.assertIn("decodeURIComponent(escape(atob(hash.slice(6))))", js)
-        self.assertIn("btoa(unescape(encodeURIComponent(code)))", js)
-
-    def test_hash_decode_syncs_the_enhanced_editor(self):
-        js = (ROOT / "public" / "runner.js").read_text()
-        self.assertIn("setCode(decodeURIComponent(escape(atob(hash.slice(6)))))", js)
-
     def test_shared_link_recipients_see_a_status_notice(self):
         js = (ROOT / "public" / "runner.js").read_text()
         self.assertIn("This link included edited code. Press Run to execute it.", js)
diff --git a/tests/test_main_observability.py b/tests/test_main_observability.py
index 2c23cea..3b39a59 100644
--- a/tests/test_main_observability.py
+++ b/tests/test_main_observability.py
@@ -139,6 +139,75 @@ def import_main(self):
         return importlib.import_module("main")
 
 
+class _Headers(dict):
+    def set(self, name, value):
+        self[name] = value
+
+
+class MainDefaultFetchTests(MainModuleHarness):
+    def test_post_and_bridge_rejections_get_security_no_store_and_status_observability(self):
+        main = self.import_main()
+        emitted = []
+        event = {"cache": "bypass"}
+        response = SimpleNamespace(status=413, headers=_Headers())
+
+        async def asgi_fetch(*args, **kwargs):
+            return response
+
+        main.caches = None
+        main.asgi.fetch = asgi_fetch
+        main.observability.event_from_worker_request = lambda *args, **kwargs: event
+        main.observability.emit = lambda value, env=None: emitted.append(value.copy())
+        worker = main.Default()
+        worker.env = SimpleNamespace()
+        request = SimpleNamespace(
+            method="POST",
+            url="https://www.pythonbyexample.dev/examples/values",
+            js_object=SimpleNamespace(),
+        )
+
+        returned = asyncio.run(worker.fetch(request))
+
+        self.assertIs(returned, response)
+        self.assertEqual(response.headers["Cache-Control"], "no-store")
+        for name, value in main.SECURITY_HEADERS.items():
+            self.assertEqual(response.headers[name], value)
+        self.assertEqual(emitted[-1]["status_code"], 413)
+        self.assertEqual(emitted[-1]["outcome"], "client_error")
+
+    def test_dynamic_response_reader_cancels_stream_above_output_cap(self):
+        main = self.import_main()
+        cancelled = []
+
+        class Chunk:
+            def __init__(self, value):
+                self.value = value
+
+            def to_bytes(self):
+                return self.value
+
+        class Reader:
+            def __init__(self):
+                self.results = [
+                    SimpleNamespace(done=False, value=Chunk(b"x" * 40_000)),
+                    SimpleNamespace(done=False, value=Chunk(b"y" * 40_000)),
+                ]
+
+            async def read(self):
+                return self.results.pop(0)
+
+            async def cancel(self, reason):
+                cancelled.append(reason)
+
+        reader = Reader()
+        response = SimpleNamespace(body=SimpleNamespace(getReader=lambda: reader))
+        text, exceeded = asyncio.run(main._read_dynamic_response_text(response))
+
+        self.assertTrue(exceeded)
+        self.assertEqual(text, main.DYNAMIC_OUTPUT_LIMIT_MESSAGE)
+        self.assertEqual(len(cancelled), 1)
+
+
 class MainObservabilityTests(MainModuleHarness):
     def test_run_example_marks_dynamic_worker_http_500_as_worker_error(self):
         main = self.import_main()
diff --git a/tests/test_main_routes.py b/tests/test_main_routes.py
index 650bb03..f83ab61 100644
--- a/tests/test_main_routes.py
+++ b/tests/test_main_routes.py
@@ -46,3 +46,6 @@ def test_unknown_paths_fall_back_to_html(self):
             main.not_found("no-such-page", self.request("https://example.test/no-such-page"))
         )
         self.assertEqual(response.status_code, 404)
+        self.assertNotIn('rel="canonical"', response.content)
+        self.assertNotIn('property="og:url"', response.content)
+        self.assertIn('', response.content)
diff --git a/tests/test_main_turnstile.py b/tests/test_main_turnstile.py
index ec8af1d..23cae9a 100644
--- a/tests/test_main_turnstile.py
+++ b/tests/test_main_turnstile.py
@@ -67,6 +67,8 @@ def make_request(*, cookie: str = "", headers: dict[str, str] | None = None, env
             "path": "/examples/values",
             "query_string": b"",
             "headers": raw_headers,
+            "scheme": "https",
+            "server": ("www.pythonbyexample.dev", 443),
             "env": env,
         }
     )
@@ -274,6 +276,18 @@ def test_off_mode_runs_without_challenge(self):
         self.assertEqual(self._ran_code, "print(42)")
         self.assertIn("ran-output", response.body.decode())
 
+    def test_dynamic_worker_output_rejection_propagates_413(self):
+        async def rejected(request, slug, code):
+            request.state.dynamic_worker_status = 413
+            return main.DYNAMIC_OUTPUT_LIMIT_MESSAGE
+
+        main._run_example = rejected
+        response = self._run(
+            post_request(b"code=print(42)", env=session_env(TURNSTILE_CHALLENGE_MODE="off"))
+        )
+        self.assertEqual(response.status_code, 413)
+        self.assertIn("output exceeded", response.body.decode())
+
     def test_invalid_raw_or_percent_encoded_utf8_is_rejected_before_execution(self):
         for body in (b"code=\xff", b"code=%FF"):
             with self.subTest(body=body):
@@ -340,11 +354,11 @@ async def failing(_request):
                 self.assertIn("verification failed", message)
                 self.assertEqual(outcome, "fail")
 
-    def test_successful_siteverify_requires_true_success_flag(self):
+    def test_successful_siteverify_requires_matching_hostname_and_action(self):
         class Response:
             status = 200
             async def text(self):
-                return '{"success": true}'
+                return '{"success": true, "hostname": "www.pythonbyexample.dev", "action": "run-example"}'
 
         main.js_fetch = lambda _request: _awaitable(Response())
         ok, message, outcome = asyncio.run(main._verify_turnstile(make_request(env=session_env()), "token"))
@@ -352,6 +366,26 @@ async def text(self):
         self.assertEqual(message, "")
         self.assertEqual(outcome, "pass")
 
+    def test_siteverify_rejects_wrong_or_missing_hostname_and_action(self):
+        payloads = [
+            '{"success": true}',
+            '{"success": true, "hostname": "other.example", "action": "run-example"}',
+            '{"success": true, "hostname": "www.pythonbyexample.dev", "action": "other-action"}',
+        ]
+        for payload in payloads:
+            with self.subTest(payload=payload):
+                class Response:
+                    status = 200
+
+                    async def text(self):
+                        return payload
+
+                main.js_fetch = lambda _request: _awaitable(Response())
+                ok, message, outcome = asyncio.run(main._verify_turnstile(make_request(env=session_env()), "token"))
+                self.assertFalse(ok)
+                self.assertIn("verification failed", message)
+                self.assertEqual(outcome, "fail")
+
 
 async def _awaitable(value):
     return value
@@ -403,6 +437,16 @@ def __init__(self, body, status=200, headers=None):
             self.assertEqual(huge_error.status, 413)
             self.assertEqual(huge_error.body, DYNAMIC_OUTPUT_LIMIT_MESSAGE)
             self.assertLessEqual(len(huge_error.body.encode()), MAX_DYNAMIC_OUTPUT_BYTES)
+            namespace = {}
+            exec(
+                build_dynamic_worker_code(
+                    f"import sys; sys.stdout.parts.append('x' * {MAX_DYNAMIC_OUTPUT_BYTES + 1})"
+                ),
+                namespace,
+            )
+            mutated_sink = asyncio.run(namespace["Default"]().fetch(None))
+            self.assertEqual(mutated_sink.status, 413)
+            self.assertEqual(mutated_sink.body, DYNAMIC_OUTPUT_LIMIT_MESSAGE)
         finally:
             if saved_workers is None:
                 sys.modules.pop("workers", None)
diff --git a/tests/test_social_cards.py b/tests/test_social_cards.py
index 7c1a9fc..2eabf28 100644
--- a/tests/test_social_cards.py
+++ b/tests/test_social_cards.py
@@ -1,8 +1,11 @@
 import json
 import tempfile
 import unittest
+from io import BytesIO
 from pathlib import Path
 
+from PIL import Image
+
 from scripts import build_social_cards as cards
 
 
@@ -23,8 +26,10 @@ def tearDown(self):
     def _write_current(self, sources):
         self.output.mkdir(exist_ok=True)
         cards.PROVENANCE_PATH.write_text(json.dumps(cards.provenance(sources)))
+        buffer = BytesIO()
+        Image.new('RGB', (1200, 630), '#fff').save(buffer, format='JPEG')
         for name in cards.provenance(sources)['inputs']:
-            (self.output / name).write_bytes(b'jpeg')
+            (self.output / name).write_bytes(buffer.getvalue())
 
     def test_current_provenance_and_exact_jpeg_set_pass(self):
         sources = {'home': 'home', 'values': 'values'}
@@ -40,6 +45,21 @@ def test_changed_input_missing_and_orphan_cards_fail(self):
         self.assertTrue(any('missing social card values.jpg' in failure for failure in failures))
         self.assertTrue(any('unexpected stale social card orphan.jpg' in failure for failure in failures))
 
+    def test_corrupt_and_wrong_size_cards_fail(self):
+        sources = {'home': 'home'}
+        self._write_current(sources)
+        valid_jpeg = (self.output / 'home.jpg').read_bytes()
+        (self.output / 'home.jpg').write_bytes(b'not-a-jpeg')
+        self.assertTrue(any('invalid JPEG' in failure for failure in cards.check_current(sources)))
+
+        (self.output / 'home.jpg').write_bytes(valid_jpeg[:-10])
+        self.assertTrue(any('invalid JPEG' in failure for failure in cards.check_current(sources)))
+
+        buffer = BytesIO()
+        Image.new('RGB', (1, 1), '#fff').save(buffer, format='JPEG')
+        (self.output / 'home.jpg').write_bytes(buffer.getvalue())
+        self.assertTrue(any('expected 1200x630' in failure for failure in cards.check_current(sources)))
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_worker_asgi_bridge_scope.py b/tests/test_worker_asgi_bridge_scope.py
index 26b5846..1bab308 100644
--- a/tests/test_worker_asgi_bridge_scope.py
+++ b/tests/test_worker_asgi_bridge_scope.py
@@ -40,36 +40,80 @@ def new(cls, value):
                     search=f"?{parsed.query}" if parsed.query else "",
                 )
 
-        # Minimal JS-runtime fakes used by process_request's non-streaming path.
+        # Minimal JS-runtime fakes used by HTTP and WebSocket bridge paths.
         class Response:
-            def __init__(self, body, headers, status):
+            def __init__(self, body, headers, status, web_socket=None):
                 self.body = body
                 self.headers = headers
                 self.status = status
+                self.webSocket = web_socket
 
             @classmethod
-            def new(cls, body, *, headers=None, status=200):
-                return cls(body, headers, status)
+            def new(cls, body, *, headers=None, status=200, webSocket=None):
+                return cls(body, headers, status, webSocket)
 
         class Object:
             @staticmethod
             def fromEntries(pairs):
                 return dict(pairs)
 
+        class WebSocketServer:
+            def __init__(self):
+                self.accepted = False
+                self.onopen = None
+                self.onclose = None
+                self.onmessage = None
+                self.sent = []
+                self.closed = None
+
+            def accept(self):
+                self.accepted = True
+
+            def send(self, value):
+                self.sent.append(value)
+
+            def close(self, code=1000, reason=""):
+                self.closed = (code, reason)
+
+        class WebSocketPair:
+            @classmethod
+            def new(cls):
+                client = SimpleNamespace(name="client")
+                server = WebSocketServer()
+                self.ws_server = server
+                return SimpleNamespace(object_values=lambda: (client, server))
+
         js.URL = URL
         js.Response = Response
         js.Object = Object
         js.TransformStream = object  # only referenced on the streaming path
+        js.WebSocketPair = WebSocketPair
         sys.modules["js"] = js
 
         ffi = types.ModuleType("pyodide.ffi")
+        self.released_buffers = 0
+        test_case = self
+
+        class _JSBytes:
+            def __init__(self, value):
+                self.value = value
+
+            def slice(self):
+                return self.value
+
+        class _Buffer:
+            def __init__(self, value):
+                self.data = _JSBytes(value)
+
+            def release(self):
+                test_case.released_buffers += 1
 
         class _Proxy:
             def __init__(self, obj):
                 self._obj = obj
 
             def getBuffer(self):
-                return SimpleNamespace(data=self._obj)
+                return _Buffer(self._obj)
 
             def destroy(self):
                 pass
@@ -146,6 +190,29 @@ async def app(scope, receive, send):
         self.assertEqual(response.body, b"echo:ping")
         self.assertEqual(response.headers.get("content-type"), "text/plain")
         self.assertEqual(response.headers.get("x-trace"), "abc")
+        self.assertEqual(self.released_buffers, 1)
+
+    def test_latin1_headers_round_trip_through_scope_and_response(self):
+        async def app(scope, receive, send):
+            self.assertIn((b"x-label", b"caf\xe9"), scope["headers"])
+            await send(
+                {
+                    "type": "http.response.start",
+                    "status": 200,
+                    "headers": [(b"x-label", b"caf\xe9")],
+                }
+            )
+            await send({"type": "http.response.body", "body": b"ok", "more_body": False})
+
+        req = self._fake_request(
+            url="https://x.dev/examples/values",
+            headers={"X-Label": "café"},
+            body_chunks=[],
+        )
+        response = asyncio.run(
+            self.bridge.process_request(app, req, SimpleNamespace(), None, state={})
+        )
+        self.assertEqual(response.headers["x-label"], "café")
 
     def test_process_request_propagates_app_exception(self):
         async def app(scope, receive, send):
@@ -202,6 +269,84 @@ class FakeRequest(self.Request):
         self.assertIs(scope["state"]["wide_event"], wide_event)
         self.assertEqual(scope["state"]["cache"], "bypass")
 
+    def test_websocket_close_callback_delivers_disconnect_event(self):
+        received = []
+
+        async def app(scope, receive, send):
+            self.assertEqual(scope["type"], "websocket")
+            received.append(await receive())
+            received.append(await receive())
+
+        req = self._fake_request(
+            method="GET",
+            url="https://x.dev/socket",
+            headers={},
+            body_chunks=[],
+        )
+
+        async def scenario():
+            response = await self.bridge.process_websocket(app, req)
+            await asyncio.sleep(0)
+            self.ws_server.onclose(SimpleNamespace(code=1001, reason="leaving"))
+            for _ in range(20):
+                if len(received) == 2:
+                    break
+                await asyncio.sleep(0)
+            return response
+
+        response = asyncio.run(scenario())
+        self.assertEqual(response.status, 101)
+        self.assertEqual(received[0], {"type": "websocket.connect"})
+        self.assertEqual(
+            received[1],
+            {"type": "websocket.disconnect", "code": 1001, "reason": "leaving"},
+        )
+
+    def test_websocket_application_close_closes_server_socket(self):
+        async def app(scope, receive, send):
+            await receive()
+            await send({"type": "websocket.close", "code": 1008, "reason": "policy"})
+
+        req = self._fake_request(
+            method="GET",
+            url="https://x.dev/socket",
+            headers={},
+            body_chunks=[],
+        )
+        response = asyncio.run(self.bridge.process_websocket(app, req))
+        self.assertEqual(response.status, 101)
+        self.assertEqual(self.ws_server.closed, (1008, "policy"))
+
+    def test_fetch_starts_lifespan_once_per_application(self):
+        calls = {"startup": 0, "request": 0}
+        app = object()
+        self.bridge._app_lifespans.clear()
+
+        async def start_application(_app):
+            self.assertIs(_app, app)
+            calls["startup"] += 1
+
+        async def process_request(_app, req, env, ctx, **kwargs):
+            self.assertIs(_app, app)
+            calls["request"] += 1
+            return SimpleNamespace(status=200)
+
+        self.bridge.start_application = start_application
+        self.bridge.process_request = process_request
+        req = self._fake_request(
+            method="GET",
+            url="https://x.dev/",
+            headers={},
+            body_chunks=[],
+        )
+
+        async def scenario():
+            await self.bridge.fetch(app, req, SimpleNamespace())
+            await self.bridge.fetch(app, req, SimpleNamespace())
+
+        asyncio.run(scenario())
+        self.assertEqual(calls, {"startup": 1, "request": 2})
+
     def test_bridge_has_pre_asgi_body_cap_hook(self):
         bridge_source = (ROOT / "src" / "worker_asgi_bridge.py").read_text()
         main_source = (ROOT / "src" / "main.py").read_text()
diff --git a/uv.lock b/uv.lock
index 5fccfdc..be8c99e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -99,6 +99,39 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
 ]
 
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
+    { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
+    { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
+    { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
+    { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
+    { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
+    { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
+    { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
+    { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
+    { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
+    { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
+]
+
 [[package]]
 name = "pydantic"
 version = "2.13.3"
@@ -209,6 +242,7 @@ dependencies = [
 
 [package.dev-dependencies]
 dev = [
+    { name = "pillow" },
     { name = "ruff" },
 ]
 workers = [
@@ -220,7 +254,10 @@ workers = [
 requires-dist = [{ name = "fastapi" }]
 
 [package.metadata.requires-dev]
-dev = [{ name = "ruff", specifier = ">=0.8" }]
+dev = [
+    { name = "pillow", specifier = ">=11.3,<12" },
+    { name = "ruff", specifier = ">=0.8" },
+]
 workers = [
     { name = "workers-py" },
     { name = "workers-runtime-sdk" },