See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Switch to @alpinejs/csp to remove unsafe-eval from Content Security Policy

Replaces standard alpinejs with @alpinejs/csp build, which avoids
new Function() and allows dropping 'unsafe-eval' from script-src.
Moves Math.min logic from inline x-bind expression into a progressWidth
getter on the backfillProgress component since globals aren't available
in the CSP build. Documents the CSP build constraint in CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+25 -17
+5 -1
AGENTS.md
··· 12 12 - **ClickHouse** — append-only store for engagement events + post snapshots 13 13 - **SQLite** (via Lucid) — metadata: tracked users, jetstream cursor, backfill jobs 14 14 - **Node 24**, **pnpm 10** (pinned in `mise.toml`; the repo migrated from npm in c46cdbb) 15 - - **Edge.js** templates, **Alpine.js** + **Vite** on the frontend 15 + - **Edge.js** templates, **Alpine.js (CSP build)** + **Vite** on the frontend 16 16 - Ships as one Docker image with three process entrypoints. 17 17 18 18 ## Commands ··· 71 71 ## Edge templates gotcha 72 72 73 73 Edge uses `{{ handle }}` for interpolation, **not** `{{{ handle }}}`. There was a recent bug (ac30713) where `@{{ handle }}` rendered as literal `@<handle>` because of Edge's `@` tag parsing — if you're rendering a handle preceded by `@`, escape the `@` or use `{{ '@' + handle }}`. 74 + 75 + ## Alpine.js CSP build 76 + 77 + We use `@alpinejs/csp` (not standard `alpinejs`). This build avoids `unsafe-eval` in our Content Security Policy. Before doing any Alpine work, read https://alpinejs.dev/advanced/csp — it documents what inline expressions are supported and what isn't (no arrow functions, no globals like `Math`/`console`/`JSON`, no template literals, no destructuring). Move complex logic into `Alpine.data()` components instead of inline expressions. 74 78 75 79 ## Testing notes 76 80
+1 -3
config/shield.ts
··· 14 14 enabled: true, 15 15 directives: { 16 16 defaultSrc: [`'self'`], 17 - // Alpine.js uses new Function() internally, requiring 'unsafe-eval' 18 - scriptSrc: [`'self'`, `'unsafe-eval'`], 19 - // Alpine x-bind:style sets inline styles, requiring 'unsafe-inline' 17 + scriptSrc: [`'self'`], 20 18 styleSrc: [`'self'`, `'unsafe-inline'`], 21 19 imgSrc: [`'self'`, 'data:', 'https://cdn.bsky.app', 'https://video.bsky.app'], 22 20 connectSrc: [`'self'`],
+1 -1
package.json
··· 58 58 "@types/alpinejs": "^3.13.11", 59 59 "@types/luxon": "^3.7.1", 60 60 "@types/node": "~25.5.0", 61 - "alpinejs": "^3.15.9", 62 61 "eslint": "^10.1.0", 63 62 "execa": "^9.6.1", 64 63 "hot-hook": "^1.0.0", ··· 76 75 "@adonisjs/shield": "^9.0.0", 77 76 "@adonisjs/static": "^2.0.1", 78 77 "@adonisjs/vite": "^5.1.0", 78 + "@alpinejs/csp": "^3.15.11", 79 79 "@atproto/api": "^0.19.8", 80 80 "@clickhouse/client": "^1.18.2", 81 81 "@tailwindcss/vite": "^4.2.2",
+10 -10
pnpm-lock.yaml
··· 29 29 '@adonisjs/vite': 30 30 specifier: ^5.1.0 31 31 version: 5.1.0(36f2255f94509bfb806708f28e3d804e) 32 + '@alpinejs/csp': 33 + specifier: ^3.15.11 34 + version: 3.15.11 32 35 '@atproto/api': 33 36 specifier: ^0.19.8 34 37 version: 0.19.8 ··· 96 99 '@types/node': 97 100 specifier: ~25.5.0 98 101 version: 25.5.2 99 - alpinejs: 100 - specifier: ^3.15.9 101 - version: 3.15.11 102 102 eslint: 103 103 specifier: ^10.1.0 104 104 version: 10.2.0(jiti@2.6.1) ··· 395 395 optional: true 396 396 edge.js: 397 397 optional: true 398 + 399 + '@alpinejs/csp@3.15.11': 400 + resolution: {integrity: sha512-7DTQ86/unHMztj5qsjtZ1B9YKLgZ5zxSynq8kBQ1zaMHEXomrGpD2X/rVluIu1AHRnVrjfUt9ji8ZLfXxgbqIg==} 398 401 399 402 '@antfu/install-pkg@1.1.0': 400 403 resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} ··· 1432 1435 ajv@6.14.0: 1433 1436 resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} 1434 1437 1435 - alpinejs@3.15.11: 1436 - resolution: {integrity: sha512-m26gkTg/MId8O+F4jHKK3vB3SjbFxxk/JHP+qzmw1H6aQrZuPAg4CUoAefnASzzp/eNroBjrRQe7950bNeaBJw==} 1437 - 1438 1438 ansi-colors@4.1.3: 1439 1439 resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} 1440 1440 engines: {node: '>=6'} ··· 3609 3609 '@adonisjs/shield': 9.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.2))(@adonisjs/core@7.3.1(@adonisjs/assembler@8.4.0(typescript@6.0.2))(@vinejs/vine@4.3.1)(edge.js@6.5.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(6ec8878f6288127aeb8665fba21971dc))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.1(@adonisjs/assembler@8.4.0(typescript@6.0.2))(@vinejs/vine@4.3.1)(edge.js@6.5.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/browser-client@2.3.0(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.59.1))(@japa/runner@5.3.0)(playwright@1.59.1))(edge.js@6.5.0) 3610 3610 edge.js: 6.5.0 3611 3611 3612 + '@alpinejs/csp@3.15.11': 3613 + dependencies: 3614 + '@vue/reactivity': 3.1.5 3615 + 3612 3616 '@antfu/install-pkg@1.1.0': 3613 3617 dependencies: 3614 3618 package-manager-detector: 1.6.0 ··· 4484 4488 fast-json-stable-stringify: 2.1.0 4485 4489 json-schema-traverse: 0.4.1 4486 4490 uri-js: 4.4.1 4487 - 4488 - alpinejs@3.15.11: 4489 - dependencies: 4490 - '@vue/reactivity': 3.1.5 4491 4491 4492 4492 ansi-colors@4.1.3: {} 4493 4493
+1 -1
resources/js/app.js
··· 1 - import Alpine from 'alpinejs' 1 + import Alpine from '@alpinejs/csp' 2 2 import { createBackfillProgress } from './backfill_progress.ts' 3 3 4 4 Alpine.data('alert', function () {
+6
resources/js/backfill_progress.ts
··· 23 23 total: number 24 24 state: 'running' | 'failed' 25 25 error: string | null 26 + readonly progressWidth: string 26 27 init(): void 27 28 destroy(): void 28 29 } ··· 39 40 total: initial.total, 40 41 state: 'running', 41 42 error: null, 43 + 44 + get progressWidth(): string { 45 + const pct = this.total > 0 ? Math.min((this.fetched / this.total) * 100, 100) : 0 46 + return `width: ${pct}%` 47 + }, 42 48 43 49 init(): void { 44 50 source = new deps.EventSource(`/profile/${this.handle}/backfill/stream`)
+1 -1
resources/views/pages/profile/loading.edge
··· 32 32 > 33 33 <div 34 34 class="h-full rounded-full bg-blue-600 transition-[width] duration-500 ease-out" 35 - x-bind:style="'width: ' + (total > 0 ? Math.min(fetched / total * 100, 100) : 0) + '%'" 35 + x-bind:style="progressWidth" 36 36 ></div> 37 37 </div> 38 38 <p