Skip to content

Security hardening, delete-after-reading fix, WEIN & CO rebranding, Kubernetes deployment#1

Draft
predator2003 wants to merge 8 commits into
mainfrom
claude/enclosed-security-rebranding-0w619s
Draft

Security hardening, delete-after-reading fix, WEIN & CO rebranding, Kubernetes deployment#1
predator2003 wants to merge 8 commits into
mainfrom
claude/enclosed-security-rebranding-0w619s

Conversation

@predator2003

Copy link
Copy Markdown
Owner

Overview

This PR brings the fork from a clean upstream v1.16.0 to a hardened, rebranded, Kubernetes-ready instance. Upstream has been dormant since v1.16.0 (July 2025), so the security-relevant open upstream PRs were adapted here directly. There are no published CVEs against enclosed itself; the work below closes dependency advisories and hardening gaps.

1. Security

Dependency refresh (lockfile was frozen ~15 months): hono 4.7.5 → 4.12.30, @hono/node-server 1.13.8 → 1.19.14, lodash-es → 4.18.1, unstorage → 1.17.5 (pulls patched h3/defu), vite → 5.4.21. pnpm audit --prod drops from 75 findings to 22, and all remaining ones are client build-chain only — the server runtime is clear.

Hardening (adapted from unmerged upstream PRs CorentinTh#477/CorentinTh#478 and issue CorentinTh#445):

  • Strict CSP (script-src 'self', no inline scripts) + Permissions-Policy. The runtime config injection was converted from an inline executable script to a non-executable JSON block so the CSP can stay strict; connect-src includes the origin of an absolute PUBLIC_BASE_API_URL.
  • Streaming request body limit sized per request from NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH (memory-exhaustion protection, honored on env-only configs like the Cloudflare entry too).
  • The server refuses to start when authentication is required while AUTHENTICATION_JWT_SECRET is still change-me (verified: exit code 1).
  • Note ids use non-monotonic ULIDs (full 80-bit randomness per capability id).
  • The note access guard no longer leaks note existence to unauthenticated clients on auth-required instances (401 for missing and private notes alike).

2. Delete-after-reading data-loss fix (upstream issue CorentinTh#307)

Previously any fetch deleted the note — a wrong password attempt or a link-preview bot destroyed it before the recipient saw it. Now deletion is two-phase: the note is deleted only after the client confirms successful decryption via POST /api/notes/:noteId/read-confirmation (web client sends it with fetch keepalive; CLI confirms after printing). Backstop for non-confirming readers: a fetched delete-after-reading note without expiration gets a 24h fallback TTL. Trade-off documented in the commit: one-time-read is now cooperative rather than fetch-enforced, in exchange for eliminating accidental destruction.

Also: file re-upload after removal works again (CorentinTh#446), and "New note" performs a full document load so SSO reverse proxies can challenge (CorentinTh#461).

3. WEIN & CO rebranding

Palette measured from weinco.at theme files: brand red #BC0936 primary, warm greys, charcoal text, dark-mode variants; red header bar with the square logo; WEIN & CO favicons/webmanifest; German metadata (noindex); app name replaced across all 17 locales; self-hosted Mulish font (no font CDN, CSP-compatible); footer with company imprint and "Powered by Enclosed (Apache 2.0)" attribution.

4. Kubernetes

deploy/kubernetes/ reference manifests (hardened non-root single-replica Deployment with /api/ping probes, PVC, TLS ingress sized for 50 MiB payloads, kustomization) plus a new docs page Self-hosting → Kubernetes including an agent prompt for automated deployment.

Verification

  • 69 app-server / 29 app-client / 27 lib / 26 crypto tests pass; typecheck, lint and production builds green in all packages.
  • Live smoke tests against the built server: CSP/Permissions-Policy headers, JSON config block, two-phase deletion flow (note survives repeated fetches, 404 after confirmation), JWT-guard startup abort, env-configured body limit (413 at 1 KB limit), 24h fallback TTL written to storage.
  • An adversarial multi-agent review (8 angles, 39 candidates) ran over the full diff; all confirmed findings were fixed in the final commit.

Notes for reviewers

  • Old clients (published npm CLI) never call the new confirmation endpoint; their delete-after-reading reads leave the note until TTL/fallback-TTL.
  • The guard's double storage read on auth-required instances is a pre-existing upstream pattern, left unchanged.
  • PBKDF2 iterations remain at upstream's 100k: raising them would invalidate all existing note links.

🤖 Generated with Claude Code

https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5


Generated by Claude Code

claude added 8 commits July 13, 2026 05:08
Security-driven refresh of the frozen v1.16.0 lockfile:
- hono 4.7.5 -> 4.12.30 (JWT algorithm confusion GHSA-f67f-6cw9-8mq4,
  serveStatic path handling GHSA-q5qw-h33p-qvwr, JWT NumericDate
  validation GHSA-hm8q-7f3q-5f36, and further advisories)
- @hono/node-server 1.13.8 -> 1.19.14 (serve-static authorization
  bypass GHSA-wc8c-qw6v-h7f6 and middleware bypass via repeated slashes)
- lodash-es 4.17.21 -> 4.18.1 (_.template code injection)
- unstorage 1.15.0 -> 1.17.5 (pulls patched h3 1.15.11 and defu 6.1.7)
- vite 5.4.14 -> 5.4.21 (dev-server fs.deny bypasses, build-time only)

Also adds @fontsource/mulish (self-hosted font, used by the branding
commit) and enables skipLibCheck for @enclosed/lib to keep typecheck
green with vite 5.4.21 type declarations (same setting as crypto).

All package tests, typechecks and builds pass with the new versions.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
- Add a strict Content-Security-Policy (script-src 'self', no inline
  scripts), Permissions-Policy and a streaming request body limit sized
  to the configured max payload (memory-exhaustion protection), based on
  upstream PR CorentinTh#477.
- Inject the runtime public config as a non-executable JSON script block
  instead of an inline executable script so the CSP can stay strict; the
  client reads the JSON block and falls back to the legacy global.
- Refuse to start when authentication is required but
  AUTHENTICATION_JWT_SECRET is still the default value, and warn when
  users are configured with the default secret (upstream issue CorentinTh#445).
- Generate note ids with non-monotonic ULIDs so ids created in the same
  millisecond keep their full 80 bits of randomness instead of being a
  predictable increment (upstream PR CorentinTh#478).

Verified live: CSP/Permissions-Policy headers present, config block
parsed by the client, server exits with code 1 when auth is enabled
with the default secret.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
Previously the server deleted a delete-after-reading note on ANY fetch.
Since the password is verified client-side, a wrong password attempt or
a link-preview bot prefetching the URL permanently destroyed the note
before the intended reader ever saw it (upstream issue CorentinTh#307).

Now the note is only deleted once the reader confirms a successful
decryption via the new POST /api/notes/:noteId/read-confirmation
endpoint. The endpoint shares the note access guard with the GET route,
only ever deletes notes flagged deleteAfterReading, and requires the
capability note id. Web client and CLI confirm automatically after a
successful decrypt (failures are ignored: content is already delivered
and the TTL still applies).

Trade-off: a reader that fetches but never confirms leaves the note
until its TTL expires; previously it was deleted on fetch. This is the
intended direction - availability for the legitimate reader wins over
eager deletion, and the TTL remains the backstop.

Covered by new unit and e2e tests; verified live against the built
server (note survives repeated fetches, 404 after confirmation).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
The hidden file input kept its value after a file was removed from the
list, so selecting the identical file again fired no change event and
the upload silently did nothing until a page refresh (upstream issue
CorentinTh#446). Clear the input value after reading the selection.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
Rebrand the client to the WEIN & CO corporate identity (weinco.at):

- Color palette measured from the weinco.at theme: brand red #BC0936
  as primary (hover #9F082F), warm greys #F5F1F1/#EEE9E9/#D9D4D0 as
  surfaces and borders, charcoal #3C3C3B text, champagne gold #CFBB79
  available as --gold; matching dark mode variants.
- Header becomes the brand-red bar with the WEIN & CO square logo,
  mirroring the weinco.at main navigation; footer shows the company
  imprint plus 'Powered by Enclosed (Apache 2.0)' attribution.
- Self-hosted Mulish font (closest free match to the commercial Altis
  corporate font) replaces the Inter webfont from the bunny CDN - no
  external font requests, consistent with the strict CSP.
- German index.html metadata (noindex for the internal instance),
  WEIN & CO favicons/webmanifest, app name replaced in all locales.
- New-note navigation is now a full document load so an authenticating
  reverse proxy in front of the instance can challenge the user
  (upstream issue CorentinTh#461).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
- deploy/kubernetes/: namespace, configmap, example secret, PVC,
  hardened single-replica Deployment (runAsNonRoot 1000, fsGroup,
  readOnlyRootFilesystem, drop ALL, /api/ping probes), service,
  TLS ingress with proxy-body-size sized for the 50 MiB payload cap,
  and a kustomization for one-command deploys.
- docs: new Self-hosting -> Kubernetes page covering the architecture
  constraints (single-writer fs storage, mandatory HTTPS for WebCrypto,
  non-root volume ownership) plus an agent prompt for automated
  deployment; registered in the sidebar.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
Correctness and security:
- Body limit is now resolved per request from the config middleware, so
  deployments that only configure through the environment (Cloudflare
  entry passes no config object) honor NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH
  instead of a construction-time 50 MiB constant; the 413 response now
  goes through the shared error factory. Verified live with a 1 KB limit.
- The note access guard no longer acts as a note-existence oracle on
  auth-required instances: unauthenticated requests get the same 401 for
  missing and for existing private notes (the previous 'if (!note)'
  branch was dead code because getNoteById throws 404).
- Delete-after-reading backstop: fetching a delete-after-reading note
  that has no expiration assigns a 24h fallback TTL, so a reader that
  never confirms (old client, curl) cannot keep the note alive forever.
- CSP connect-src now includes the origin of an absolute
  PUBLIC_BASE_API_URL so a cross-origin API setup is not blocked.
- The web client sends the read confirmation with fetch keepalive so
  closing the tab right after decryption does not abort the deletion;
  the CLI prints the note before confirming instead of after.

Cleanups from review:
- DEFAULT_JWT_SECRET exported from the config module instead of a
  duplicated literal; startup guards restructured without overlap.
- config.provider uses safelySync and drops the unreachable legacy
  window.__CONFIG__ fallback.
- Removed dead i18n keys (footer.crafted-by/source-code/github,
  navbar.github/github-repository/settings.support) from all locales,
  fixed the Hungarian article before the new brand name, removed the
  now-dead form-reset call before the full-page new-note navigation,
  deduplicated header trigger classes and dark-theme CSS variables,
  aligned bundled Mulish weights with actual usage (300 added, unused
  800 dropped), updated robots.txt/humans.txt leftovers.
- Missing awaits added to rejects assertions in notes usecases tests;
  new tests cover the fallback-TTL behavior (69 server tests total).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
- de.json now covers all 98 keys of the English reference (was 78):
  QR-code sharing, password visibility toggles, no-expiration switch,
  loading/attachment strings, plus 'Design' and 'Anmelden' instead of
  the leftover English 'Theme'/'Login'.
- Header bar is taller (py-4) with a larger logo (h-13, rounded-md,
  subtle shadow) and a larger app title so the brand mark carries the
  header, as on weinco.at.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01McgPwoZTeJF5Tgimar97t5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants