Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.
Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)
Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
sources with clear labelling
- Bokmål UI
Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
a visually-unambiguous alphabet
Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped
Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.
Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00
|
|
|
{
|
|
|
|
|
"name": "vinterliste",
|
|
|
|
|
"version": "0.1.0",
|
|
|
|
|
"description": "End-to-end encrypted winter activity list",
|
|
|
|
|
"private": true,
|
|
|
|
|
"type": "module",
|
|
|
|
|
"scripts": {
|
|
|
|
|
"dev:server": "bun run --hot server/index.ts",
|
|
|
|
|
"dev:frontend": "vite --config frontend/vite.config.ts",
|
|
|
|
|
"build:frontend": "vite build --config frontend/vite.config.ts",
|
|
|
|
|
"start": "NODE_ENV=production bun run server/index.ts",
|
|
|
|
|
"test": "bun test",
|
2026-05-25 20:04:57 +02:00
|
|
|
"typecheck": "tsc --noEmit && tsc --noEmit -p frontend/tsconfig.json",
|
|
|
|
|
"reset-password": "bun run server/reset-password.ts"
|
Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.
Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)
Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
sources with clear labelling
- Bokmål UI
Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
a visually-unambiguous alphabet
Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped
Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.
Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00
|
|
|
},
|
|
|
|
|
"dependencies": {
|
feat(activity): Markdown rendering for descriptions
Activity descriptions now render through a small marked + DOMPurify
pipeline. Client-side only — the server keeps storing raw markdown
source, private descriptions stay inside the encrypted payload.
frontend/src/lib/markdown.ts exposes a single renderMarkdown(src)
helper. Allowlist: p / br / hr / strong / em / del / s / code / pre /
ul / ol / li / blockquote / a / h3–h6. URL scheme allowlist:
http(s) and mailto. Images are deliberately stripped — external
image URLs leak the viewer's IP to the linker's host. Raw HTML
pass-through is off. A DOMPurify afterSanitizeAttributes hook forces
target="_blank" rel="noopener noreferrer ugc" on every <a>, matching
the existing external-link pattern in PublicList. h1/h2 in the
source get downshifted to h3 via walkTokens so the description's
heading hierarchy doesn't collide with the SPA's own <h1>/<h2>.
Render sites: the two description spots in ActivityRow.svelte (one
for the decrypted private branch, one for non-private). New
.md class in styles.css gives the rendered block tight spacing,
discreet code/pre/blockquote treatment, accent-coloured links.
UX: a "Du kan bruke Markdown — **fet**, *kursiv*, [lenke](https://…),
lister med -" hint under the textarea in ActivityForm. No preview
pane to keep scope contained.
Tests: 14 cases in tests/markdown.test.ts covering the allowlist
(bold/italic/strike/lists/links/mailto), the sanitisation surface
(javascript: / data: / script / iframe / on* handlers / images),
and the h1/h2 → h3 downshift. happy-dom is registered locally in
beforeAll (and the markdown module dynamic-imported) rather than as
a project-wide preload — the latter overrides fetch/Request/Response
and breaks Bun-fetch-based API tests in other files.
Bundle impact: marked + dompurify add ~60KB to the SPA bundle.
2026-05-25 21:15:31 +02:00
|
|
|
"dompurify": "^3.4.5",
|
Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.
Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)
Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
sources with clear labelling
- Bokmål UI
Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
a visually-unambiguous alphabet
Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped
Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.
Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00
|
|
|
"hono": "^4.6.0",
|
2026-05-25 16:59:43 +02:00
|
|
|
"libsodium-wrappers-sumo": "^0.7.15",
|
feat(activity): Markdown rendering for descriptions
Activity descriptions now render through a small marked + DOMPurify
pipeline. Client-side only — the server keeps storing raw markdown
source, private descriptions stay inside the encrypted payload.
frontend/src/lib/markdown.ts exposes a single renderMarkdown(src)
helper. Allowlist: p / br / hr / strong / em / del / s / code / pre /
ul / ol / li / blockquote / a / h3–h6. URL scheme allowlist:
http(s) and mailto. Images are deliberately stripped — external
image URLs leak the viewer's IP to the linker's host. Raw HTML
pass-through is off. A DOMPurify afterSanitizeAttributes hook forces
target="_blank" rel="noopener noreferrer ugc" on every <a>, matching
the existing external-link pattern in PublicList. h1/h2 in the
source get downshifted to h3 via walkTokens so the description's
heading hierarchy doesn't collide with the SPA's own <h1>/<h2>.
Render sites: the two description spots in ActivityRow.svelte (one
for the decrypted private branch, one for non-private). New
.md class in styles.css gives the rendered block tight spacing,
discreet code/pre/blockquote treatment, accent-coloured links.
UX: a "Du kan bruke Markdown — **fet**, *kursiv*, [lenke](https://…),
lister med -" hint under the textarea in ActivityForm. No preview
pane to keep scope contained.
Tests: 14 cases in tests/markdown.test.ts covering the allowlist
(bold/italic/strike/lists/links/mailto), the sanitisation surface
(javascript: / data: / script / iframe / on* handlers / images),
and the h1/h2 → h3 downshift. happy-dom is registered locally in
beforeAll (and the markdown module dynamic-imported) rather than as
a project-wide preload — the latter overrides fetch/Request/Response
and breaks Bun-fetch-based API tests in other files.
Bundle impact: marked + dompurify add ~60KB to the SPA bundle.
2026-05-25 21:15:31 +02:00
|
|
|
"marked": "^18.0.4",
|
2026-05-25 16:59:43 +02:00
|
|
|
"svelte-dnd-action": "^0.9.69"
|
Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.
Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)
Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
sources with clear labelling
- Bokmål UI
Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
a visually-unambiguous alphabet
Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped
Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.
Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00
|
|
|
},
|
|
|
|
|
"devDependencies": {
|
feat(activity): Markdown rendering for descriptions
Activity descriptions now render through a small marked + DOMPurify
pipeline. Client-side only — the server keeps storing raw markdown
source, private descriptions stay inside the encrypted payload.
frontend/src/lib/markdown.ts exposes a single renderMarkdown(src)
helper. Allowlist: p / br / hr / strong / em / del / s / code / pre /
ul / ol / li / blockquote / a / h3–h6. URL scheme allowlist:
http(s) and mailto. Images are deliberately stripped — external
image URLs leak the viewer's IP to the linker's host. Raw HTML
pass-through is off. A DOMPurify afterSanitizeAttributes hook forces
target="_blank" rel="noopener noreferrer ugc" on every <a>, matching
the existing external-link pattern in PublicList. h1/h2 in the
source get downshifted to h3 via walkTokens so the description's
heading hierarchy doesn't collide with the SPA's own <h1>/<h2>.
Render sites: the two description spots in ActivityRow.svelte (one
for the decrypted private branch, one for non-private). New
.md class in styles.css gives the rendered block tight spacing,
discreet code/pre/blockquote treatment, accent-coloured links.
UX: a "Du kan bruke Markdown — **fet**, *kursiv*, [lenke](https://…),
lister med -" hint under the textarea in ActivityForm. No preview
pane to keep scope contained.
Tests: 14 cases in tests/markdown.test.ts covering the allowlist
(bold/italic/strike/lists/links/mailto), the sanitisation surface
(javascript: / data: / script / iframe / on* handlers / images),
and the h1/h2 → h3 downshift. happy-dom is registered locally in
beforeAll (and the markdown module dynamic-imported) rather than as
a project-wide preload — the latter overrides fetch/Request/Response
and breaks Bun-fetch-based API tests in other files.
Bundle impact: marked + dompurify add ~60KB to the SPA bundle.
2026-05-25 21:15:31 +02:00
|
|
|
"@happy-dom/global-registrator": "^20.9.0",
|
Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.
Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)
Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
sources with clear labelling
- Bokmål UI
Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
a visually-unambiguous alphabet
Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped
Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.
Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00
|
|
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
|
|
|
"@tsconfig/svelte": "^5.0.4",
|
|
|
|
|
"@types/bun": "^1.1.0",
|
feat(activity): Markdown rendering for descriptions
Activity descriptions now render through a small marked + DOMPurify
pipeline. Client-side only — the server keeps storing raw markdown
source, private descriptions stay inside the encrypted payload.
frontend/src/lib/markdown.ts exposes a single renderMarkdown(src)
helper. Allowlist: p / br / hr / strong / em / del / s / code / pre /
ul / ol / li / blockquote / a / h3–h6. URL scheme allowlist:
http(s) and mailto. Images are deliberately stripped — external
image URLs leak the viewer's IP to the linker's host. Raw HTML
pass-through is off. A DOMPurify afterSanitizeAttributes hook forces
target="_blank" rel="noopener noreferrer ugc" on every <a>, matching
the existing external-link pattern in PublicList. h1/h2 in the
source get downshifted to h3 via walkTokens so the description's
heading hierarchy doesn't collide with the SPA's own <h1>/<h2>.
Render sites: the two description spots in ActivityRow.svelte (one
for the decrypted private branch, one for non-private). New
.md class in styles.css gives the rendered block tight spacing,
discreet code/pre/blockquote treatment, accent-coloured links.
UX: a "Du kan bruke Markdown — **fet**, *kursiv*, [lenke](https://…),
lister med -" hint under the textarea in ActivityForm. No preview
pane to keep scope contained.
Tests: 14 cases in tests/markdown.test.ts covering the allowlist
(bold/italic/strike/lists/links/mailto), the sanitisation surface
(javascript: / data: / script / iframe / on* handlers / images),
and the h1/h2 → h3 downshift. happy-dom is registered locally in
beforeAll (and the markdown module dynamic-imported) rather than as
a project-wide preload — the latter overrides fetch/Request/Response
and breaks Bun-fetch-based API tests in other files.
Bundle impact: marked + dompurify add ~60KB to the SPA bundle.
2026-05-25 21:15:31 +02:00
|
|
|
"@types/dompurify": "^3.2.0",
|
Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.
Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)
Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
sources with clear labelling
- Bokmål UI
Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
a visually-unambiguous alphabet
Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped
Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.
Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00
|
|
|
"@types/libsodium-wrappers-sumo": "^0.7.8",
|
|
|
|
|
"svelte": "^5.0.0",
|
|
|
|
|
"svelte-check": "^4.0.0",
|
|
|
|
|
"typescript": "^5.6.0",
|
|
|
|
|
"vite": "^6.0.0"
|
|
|
|
|
}
|
|
|
|
|
}
|