Commit Graph
1683 Commits
Author SHA1 Message Date
TheZwiss 89467d6f93 fix(server): upgrade better-sqlite3 to 12.x for Node 24 compatibility (#41)
better-sqlite3 11.x removes its environment cleanup hook from Statement::~Statement() after the Node environment is torn down. Node 24.19.0 asserts on the null environment and aborts the worker, so vitest reported 'Worker exited unexpectedly' and exited non-zero on a fully green test run — failing the required check on every PR.

11.x also ships no prebuild for Node 24, so CI compiled it from source on every run. 12.x has prebuilds for that ABI and the V8 13.9 shims 11.x lacks. drizzle-orm declares better-sqlite3 >=7, so the major bump is in range.

Supersedes #26.
2026-08-25 15:41:43 +02:00
TheZwiss 8e13aaa057 Merge pull request #27 from TheZwiss/docs/security-design-record
docs: complete security design record on main (spec + Plan A plan)
2026-07-13 11:40:37 +02:00
Jannis Braun aa5052ba8a docs(security): add scanning/hardening design spec + Plan A implementation plan
Completes the design record on main: Plan B's plan and the federation spec were
already here; this adds the umbrella security spec (source of truth for the
remaining container/web/desktop/remediation workstreams) and Plan A's plan.
2026-07-13 11:36:23 +02:00
TheZwiss 97989fbfb4 Merge pull request #18 from TheZwiss/security/container-hardening
Container hardening: non-root + image scan (report-only)
2026-07-13 11:33:54 +02:00
Jannis Braun bf836e208b Merge main into security/container-hardening (bring up to date for merge) 2026-07-13 11:27:02 +02:00
TheZwiss 53a36cd11a Merge pull request #17 from TheZwiss/security/scanning-pipeline
Security scanning pipeline (report-only)
2026-07-13 11:26:27 +02:00
Jannis Braun d2d6ce9756 Merge main into security/scanning-pipeline (bring up to date for merge) 2026-07-13 11:20:45 +02:00
Jannis Braun 7d1895308d fix(docker): make SARIF upload non-blocking; correct seed-admin/build-stage/restore ownership docs
Final whole-branch review (opus) fixes:
- docker-publish.yml: upload-sarif was if:always() but not continue-on-error, so a
  Trivy SARIF-emit flake would fail the job and SKIP the multi-arch publish. Made it
  non-blocking so a scanner hiccup never blocks a release.
- deployment.md: seed-admin-rotated.txt is root-owned (written via docker exec, which
  bypasses the gosu drop) — reverted an over-correction. Corrected the canonical
  runtime-stage build description (no toolchain; non-root gosu). First-boot chown note.
- restore.sh: comment ownership root -> uid 1000.
2026-07-13 01:21:20 +02:00
Jannis Braun 0b3aa42a09 docs(docker): add maintainer release-gate pull-test note (per review) 2026-07-13 01:13:19 +02:00
Jannis Braun 6d81b63d86 docs(docker): document non-root runtime, data-volume migration, and image scan 2026-07-13 01:09:26 +02:00
Jannis Braun c4929a8b1b ci(docker): scan the amd64 image before publish; attach SBOM + provenance 2026-07-13 01:06:58 +02:00
Jannis Braun 0ec7ddba81 docs(docker): correct stale runtime-deps comment (no build toolchain anymore) 2026-07-13 01:04:13 +02:00
Jannis Braun 3100965c30 fix(docker): run container as non-root (gosu) and drop build toolchain from runtime 2026-07-13 00:58:07 +02:00
Jannis Braun 9d2eeb0963 docs(security): build native arm64 for local Docker verification (host is Apple Silicon; arm64 = Pi target) 2026-07-13 00:54:42 +02:00
Jannis Braun fd8659a964 docs(security): lighter targeted arm64 native-module check (avoid QEMU Vite build timeout) 2026-07-13 00:53:26 +02:00
Jannis Braun 7457146846 docs(security): fold Plan B review findings (proc/1 uid check, arm64 verify, cheap idempotent chown, provenance floor, doc corrections) 2026-07-13 00:52:13 +02:00
Jannis Braun 321428ba1f docs(security): Plan B implementation plan (container hardening + image scan) 2026-07-13 00:42:02 +02:00
Jannis Braun cf4172e81a fix(security): bump trivy-action v0.28.0 -> v0.36.0
v0.28.0's composite action referenced a nested aquasecurity/setup-trivy@v0.2.1
tag that no longer exists, so the action failed to RESOLVE during job setup
(before any step ran) — continue-on-error can't catch a resolution failure, so
both Trivy jobs went red on every run. v0.36.0 pins setup-trivy to a real SHA
(v0.2.6) and still supports scan-type/scan-ref/scanners/format/output.
2026-07-12 23:54:47 +02:00
Jannis Braun e2d09c0d52 fix(security): point OSV-Scanner at runnable subpath action; doc accuracy fixes
- OSV-Scanner ref was google/osv-scanner-action@<sha> (metadata-only root
  action, no runs:) -> subpath google/osv-scanner-action/osv-scanner-action
  which carries the docker action + scan-args input. Root ref would fail to
  load and redden the job on every run (caught in final whole-branch review).
- security-scanning.md: note gitleaks findings land in job log (not SARIF);
  add scorecard branch_protection_rule trigger; mark SBOM/provenance as not-
  yet-live. CLAUDE.md row: image scan is a later plan, not current.
2026-07-12 23:51:00 +02:00
Jannis Braun cb524675cc docs(security): document the scanning pipeline + maintainer checklist 2026-07-12 23:51:00 +02:00
Jannis Braun 4758ca46fa ci(security): SHA-pin all actions and add harden-runner (audit) 2026-07-12 23:51:00 +02:00
Jannis Braun 21d783e257 ci(security): add OpenSSF Scorecard workflow 2026-07-12 23:51:00 +02:00
Jannis Braun 3f75ff6e36 ci(security): add CodeQL SAST workflow (javascript-typescript) 2026-07-12 23:51:00 +02:00
Jannis Braun 74ae929ab4 ci(security): add report-only security scan workflow (gitleaks, OSV, Trivy) 2026-07-12 23:51:00 +02:00
Jannis Braun 807afba45a ci(security): add Dependabot config (npm + actions + docker) 2026-07-12 23:51:00 +02:00
BadAtCaptchasandJannis Braun 1e6c7c6042 Enhance Windows dev support (#13)
Makes local dev work on Windows: cross-env for the server dev port, pnpm --parallel to run server+web together (replacing the POSIX-only '&'), PowerShell setup docs, engines widened to Node >=20, and a Node 20 + 24 CI matrix.

CI keeps a stable required 'Build & test' status via an aggregate gate job so the matrix rename doesn't drop the context the main ruleset requires.

Co-authored-by: BadAtCaptchas <2359196+BadAtCaptchas@users.noreply.github.com>
Co-authored-by: Jannis Braun <151788261+TheZwiss@users.noreply.github.com>
2026-07-12 13:10:43 +02:00
TheZwissandBadAtCaptchas 43cab41e60 fix(spaces): clearer 403 for non-members on invite endpoint (#14)
Non-members already can't mint invite codes (permissions resolve to zero for them since 85e1975f), but hasPermission reports it as a missing CREATE_INVITE permission, which is misleading. Return 'Space membership required' for the non-member case instead.

Message wording from #12 by BadAtCaptchas.

Co-authored-by: BadAtCaptchas <2359196+BadAtCaptchas@users.noreply.github.com>
2026-07-12 12:51:37 +02:00
TheZwiss d76e06a023 refactor(federation): consolidate inbound S2S-auth preamble into one helper (#11)
Six S2S-HMAC endpoints repeated the same inbound-auth preamble verbatim
(parse federation headers -> resolve active peer -> optional per-peer rate
limit -> verify HMAC signature -> nonce replay protection). Extract it into
authenticateS2SPeer() so the trust boundary has a single, tested definition.

Adopters (preamble only; every post-auth side effect, body validation, and
response is unchanged):
- DELETE /api/federation/identity      (no rate limiter; warns on missing nonce)
- POST   /api/federation/relay         (relay limiter; warns; keeps in-handler
                                        epoch-baseline populate + nonce ratchet)
- POST   /api/federation/sync          (no limiter; warns with the [sync] tag;
                                        keeps in-handler nonce ratchet)
- POST   /api/federation/users/lookup       (lookup limiter, Retry-After 60)
- POST   /api/federation/users/by-home-id   (same)
- POST   /api/federation/verify-attach-proof(shares lookup bucket, Retry-After 60)

Deliberate non-adopters, each keeping a load-bearing gate the helper would
flatten (documented at each site + in the helper docstring):
- POST /api/federation/epoch        gates status != 'revoked' (peer recovery),
                                    400 on missing headers, no nonce check
- POST /api/federation/peer/rotate  active-only but no nonce check
- POST /api/federation/peer/denied  awaiting_approval gate (404/409), synthetic
                                    no-grace secret verify

Behavior-preserving. The rate limiter is injected (plain { limited, retryAfter }),
so the limit still fires BEFORE signature verification. The only ordering change:
/relay's opportunistic epoch-baseline populate now runs just after the shared
preamble (i.e. after the nonce check) instead of between signature and nonce.
This is provably equivalent for every reachable honest-peer state (a duplicate
nonce means the baseline is already non-null; a valid-signature-but-no-nonce
request from a nonce-supporting peer is unreachable in transit and carries no
security/correctness consequence) and the populate is documented as not
affecting relay accept/reject.

Adds a dedicated unit test covering the full decision table (headers, peer
status, rate-limit + Retry-After, rate-limit-before-signature ordering,
signature, nonce duplicate/missing, log flag + context suffix, success). Full
server suite green (804 tests).
2026-07-10 03:08:09 +02:00
TheZwiss c79bf91398 refactor(server): dedupe federation rate limiters and response signing (#10)
Phase C cleanup follow-up to the routes/federation split (#9). Behavior-
preserving; full server suite (790 tests) green.

A) rateLimits.ts: the four near-identical sliding-window limiters
   (accept/relay/lookup/ensure) and their duplicated prune loops collapse
   into one createLimiter(windowMs, max) factory. Per-call and periodic-
   sweep semantics are preserved exactly, including that lookup buckets are
   pruned per-call but never swept (unchanged from before). 177 -> 101 lines.

B) Extract sendSignedJson(reply, payload, hmacSecret) — the single
   definition of how this instance signs an S2S JSON response — and use it
   in the /epoch and /verify-attach-proof|reattach handlers, replacing two
   copies of the build-headers-and-send boilerplate.
2026-07-10 02:23:14 +02:00
TheZwiss 94fe73522d refactor(server): split federation routes into cohesive modules (#9)
routes/federation.ts had grown to 7.6k lines, spanning HTTP route
registration, federated identity resolution, ~30 inbound relay event
processors, DM reconciliation, and rate-limiting internals — too large
to review or hold in context, and awkward to change safely.

Split the implementation into 18 focused modules under routes/federation/
(helpers, events/, handlers/) and keep routes/federation.ts as a thin
barrel that re-exports the public API and composes the HTTP registrars
into federationRoutes(). No import paths change anywhere else.

Pure move, no behavior change:
- 61/61 named functions byte-identical; only deltas are 2 dynamic-import
  paths adjusted for the new directory depth
- public export surface unchanged (barrel re-exports all 22 symbols)
- all 30 endpoints preserved (identical verb+path set)
- typecheck, build, and full server suite (790 tests) green

Docs: update federation.md source-file map; add split design doc.
2026-07-10 02:08:03 +02:00
TheZwiss 180228f2d1 ci: bump actions to Node 24 runtimes to clear deprecation warning (#8)
GitHub is deprecating the Node 20 runtime for JS actions; every run printed a
warning that actions/checkout@v4, actions/setup-node@v4 and pnpm/action-setup@v4
were being force-run on Node 24. Bump each to its first Node 24 major (v5) across
all workflows — the smallest jump that clears the warning, avoiding the extra
behavior changes in checkout v6/v7 (credential persistence, fork-PR blocking)
that don't apply here. Our checkout jobs use push/pull_request, not
pull_request_target/workflow_run, so none are affected regardless.

Also bump the GitHub Pages actions in deploy-pages.yml (configure-pages v5->v6,
upload-pages-artifact v3->v5, deploy-pages v4->v5), which were likewise on Node
20. Inputs are unchanged; pnpm still pinned to 10.34.3 via the version input and
the packageManager field.
2026-07-10 01:02:47 +02:00
TheZwiss c7d88481ad test: give federation integration suites a realistic per-test timeout (#7)
The three test/ files boot real federated instances and drive S2S over HTTP,
with several tests deliberately waiting on log matchers (e.g. logMatched(...,
1_000) per remote). Vitest's 5s default per-test timeout is meant for unit tests
and is too tight here: under CI load the multi-remote fan-out tests in
federation-identity-deletion intermittently timed out (observed on the post-merge
main run), producing a flaky red check.

Set a file-level testTimeout of 30s in each of the three harness-based suites via
vi.setConfig. Scoped per-file so unit tests keep the strict 5s default; a genuine
hang still trips the 30s ceiling well before the 90s hook budget. No test logic
changed.
2026-07-10 00:51:18 +02:00
BadAtCaptchasandJannis Braun 9b6d1b18eb Fix federated attachment downloads allowing SSRF via redirects (#1)
* Backspace — initial public release

* chore: pin Node 20 (LTS) and pnpm 10.34.3

Unpinned `pnpm@latest` in the Dockerfile made fresh builds non-reproducible:
`latest` now resolves to pnpm 11, but the committed lockfile targets pnpm 10, so
`pnpm install --frozen-lockfile` fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on
a clean host. Pin pnpm to 10.34.3 across the Dockerfile, the `packageManager`
field, and the release workflow; pin Node to 20 (LTS) via `.nvmrc` and `engines`
so Docker, CI, and from-source builds all use the same tested toolchain.

Also corrects the docs: the lockfile is v9.0 (requires pnpm 10, not "pnpm 8+"),
and "Node 20+" implied untested newer majors were supported.

* fix(compose): fail fast on missing JWT_SECRET instead of a silent restart loop

Without JWT_SECRET the server throws at boot (config.ts) and `restart:
unless-stopped` turns that into an endless crash loop that is invisible unless
you run `docker compose logs backspace`. Guard it exactly like DOMAIN already is,
so `docker compose up` stops immediately with an actionable message. install.sh
generates the secret before bringing the stack up, so this only affects the
manual `cp .env.example .env && docker compose up` path.

* docs(env): make the JWT_SECRET requirement explicit in .env.example

Spell out that JWT_SECRET is required and must be a strong 32+ char value, and
that the empty default is intentional (docker compose fails fast rather than the
server boot-looping). Pairs with the compose-level guard added in 592c23f.

* fix(desktop): don't let uiohook-napi rebuild abort the whole workspace install

The @backspace/desktop postinstall runs `electron-rebuild -f -w uiohook-napi`,
which compiles a native module from source and needs a C++ toolchain
(make/g++/python3). On a bare host without those — a typical VPS a self-hoster
clones onto — that failure took the ENTIRE `pnpm install` down (exit 255), so the
server never installed either, even though uiohook-napi is desktop-only and
irrelevant to hosting.

Make the rebuild non-fatal: on failure it now prints a clear one-line warning and
continues. Machines with build tools (CI, desktop devs) are byte-identical — the
fallback never fires; only toolless boxes (which aren't building the desktop app
anyway) skip it. Verified on a bare x86 box: full `pnpm install` now exits 0 and
the server builds and boots from source. Docs note the build-tool requirement for
desktop work and point self-hosters at the Docker installer.

* feat(deploy): three deployment modes + prebuilt multi-arch image for robust self-hosting

Make Backspace self-hostable in any homelab environment, not just a clean host
that owns ports 80/443.

install.sh is now mode-aware and auto-detects which fits:
  - allinone (default): bundled Caddy + auto-HTTPS — unchanged behavior
  - proxy: behind your own reverse proxy (nginx / Traefik / Caddy / Nginx Proxy
    Manager / SWAG) — app published on 127.0.0.1:APP_PORT, no bundled Caddy,
    prints paste-ready proxy snippets
  - tunnel: behind a tunnel (Cloudflare / Tailscale) — same, plus a 90MB upload
    cap (under Cloudflare's 100MB body limit) and voice force-disabled (WebRTC
    over UDP can't traverse a tunnel)

Port detection is Docker-aware (consults `docker ps` published ports, not just
`ss`), so a host whose proxy already owns 80/443 via iptables DNAT — with no
listening socket for `ss` to see — is correctly detected as "taken" instead of
dead-ending.

docker-compose.proxy.yml is a small overlay, layered via COMPOSE_FILE (written
into .env so no `-f` flags are ever needed), that publishes the loopback port and
parks Caddy in an inert profile. The base compose file is untouched, so All-in-One
behaves exactly as before.

Prebuilt image: .github/workflows/docker-publish.yml builds and pushes a
multi-arch (linux/amd64 + linux/arm64) image to ghcr.io/thezwiss/backspace on
release tags (and manual dispatch), so weak/ARM hosts skip the ~1.6GB local build
(the Vite build OOMs small ARM boxes). install.sh and docker-compose.yml default
to pulling it, fall back to an image already present on the host, and finally to a
from-source build — AGPL §13 commit stamping preserved on every path. Kept
deliberately separate from the desktop-installer workflow (release.yml).

Docs: README gains a "Deployment modes" section (all three modes, nginx / Caddy /
Traefik snippets, GUI-proxy field-by-field, cloudflared ingress, the update path,
and voice-per-mode caveats); docs/systems/deployment.md updated to match.

Verified live on a throwaway VM: proxy + all-in-one end-to-end through install.sh
(with a real Let's Encrypt cert), tunnel config generation, loopback-only binding,
and the local-image fallback path.

* Fix federation file download SSRF (#1)

---------

Co-authored-by: Jannis Braun <151788261+TheZwiss@users.noreply.github.com>
2026-07-10 00:27:05 +02:00
TheZwiss 628e4dec3e ci: run typecheck, build & tests on PRs and main (#6)
* ci: run typecheck, build & tests on PRs and main

Add a GitHub Actions workflow (.github/workflows/ci.yml) that installs on
Node 20 (the pinned runtime), builds shared/server/web, typechecks desktop,
and runs the full vitest suite across all packages on every pull request and
push to main. The repo already had 500+ tests but only ever ran them locally.

Also wires up the missing test/typecheck plumbing this exposed:
- web package had 55 test files but no test script — add test/test:watch/typecheck
- add root-level test and typecheck aggregate scripts
- fix a latent web type error the new typecheck surfaced: reference
  vite-plugin-pwa/react types so virtual:pwa-register/react is typed
  (useRegisterSW callback params were implicit any; the tsc half of the web
  build script has been failing, masked by vite build ignoring type errors)

* test: fix two latent failures surfaced by CI on Node 20

- server: exclude dist/** from vitest. Vitest 4's default exclude dropped
  dist/, so after `pnpm build` emits compiled .test.js files, vitest ran those
  stale copies alongside src/*.test.ts and they failed (compiled vi.mock paths
  resolve differently).
- web: polyfill Blob.prototype.stream in the jsdom test setup. jsdom's Blob has
  no .stream() on Node 20 (Node 25 provided one, masking this locally); undici's
  Response constructor calls blob.stream(), so new Response(blob) threw
  'object.stream is not a function'.
2026-07-10 00:15:41 +02:00
TheZwiss 0727d5a3b3 Add landing page and Pages deploy workflow 2026-07-08 00:45:31 +02:00
TheZwiss 3513a3dde9 docs: tighten public copy, add comparison, templates, and social preview
* docs: tighten README, CONTRIBUTING, and CLA prose

* docs: add platform comparison and README FAQ

* chore: add issue and pull request templates

* chore: add repository social preview image
2026-07-07 23:39:33 +02:00
Jannis Braun 95b545d8b2 chore(deploy): allow a gitignored local override for deploy targets
deploy.sh now sources ./.deploy.local at runtime if it exists, letting a
maintainer point the script at their own hosts without committing them — the
tracked defaults stay placeholders. .deploy.local and .deploy-local/ are
gitignored so real hostnames/IPs never land in the repo.
2026-07-07 19:45:51 +02:00
Jannis Braun 85e1975fa5 fix(permissions): deny space permissions to non-members (invite-bypass)
computePermissions() returned the space @everyone role's permissions without
verifying the caller had joined the space. Because CREATE_INVITE is in
DEFAULT_EVERYONE_PERMISSIONS, any authenticated user could mint an invite code
for a request-only space — whose id is listed by /api/spaces/explore — and then
self-join via /api/spaces/:id/join, bypassing the join-request approval flow.
The same gap let non-members read message history and search default channels.

Root cause:
- computePermissions now returns 0n for non-members (space owner and instance
  admin still short-circuit first, so they are unaffected).

Defense in depth (request-only spaces are approval-gated, never invite-joinable):
- both invite-code join endpoints reject visibility='request' (private stays
  invite-joinable — its only entry path; public too).
- POST /api/spaces/:id/invite refuses to hand out a code for request spaces.
- POST /api/dm/space-invite refuses to card a local request space, checked by
  space id against the local table so a spoofed spaceInstanceOrigin can't slip
  past it.
- InviteModal hides the invite affordances for request spaces.

Also removes the unused computeCategoryPermissions(), which duplicated the
resolution algorithm without the membership gate.

Adds unit + route + component tests covering non-member/member/owner/admin
resolution and the request/private/public visibility matrix.

Reported-by: BadAtCaptchas (#2)
2026-07-07 19:45:51 +02:00
github-actions[bot] 26bb0be7af @BadAtCaptchas has signed the CLA in TheZwiss/backspace#1 2026-07-07 14:36:01 +00:00
github-actions[bot] e27e52f267 Creating file for storing CLA Signatures 2026-07-07 14:35:11 +00:00
Jannis Braun 531b496618 feat(deploy): three deployment modes + prebuilt multi-arch image for robust self-hosting
Make Backspace self-hostable in any homelab environment, not just a clean host
that owns ports 80/443.

install.sh is now mode-aware and auto-detects which fits:
  - allinone (default): bundled Caddy + auto-HTTPS — unchanged behavior
  - proxy: behind your own reverse proxy (nginx / Traefik / Caddy / Nginx Proxy
    Manager / SWAG) — app published on 127.0.0.1:APP_PORT, no bundled Caddy,
    prints paste-ready proxy snippets
  - tunnel: behind a tunnel (Cloudflare / Tailscale) — same, plus a 90MB upload
    cap (under Cloudflare's 100MB body limit) and voice force-disabled (WebRTC
    over UDP can't traverse a tunnel)

Port detection is Docker-aware (consults `docker ps` published ports, not just
`ss`), so a host whose proxy already owns 80/443 via iptables DNAT — with no
listening socket for `ss` to see — is correctly detected as "taken" instead of
dead-ending.

docker-compose.proxy.yml is a small overlay, layered via COMPOSE_FILE (written
into .env so no `-f` flags are ever needed), that publishes the loopback port and
parks Caddy in an inert profile. The base compose file is untouched, so All-in-One
behaves exactly as before.

Prebuilt image: .github/workflows/docker-publish.yml builds and pushes a
multi-arch (linux/amd64 + linux/arm64) image to ghcr.io/thezwiss/backspace on
release tags (and manual dispatch), so weak/ARM hosts skip the ~1.6GB local build
(the Vite build OOMs small ARM boxes). install.sh and docker-compose.yml default
to pulling it, fall back to an image already present on the host, and finally to a
from-source build — AGPL §13 commit stamping preserved on every path. Kept
deliberately separate from the desktop-installer workflow (release.yml).

Docs: README gains a "Deployment modes" section (all three modes, nginx / Caddy /
Traefik snippets, GUI-proxy field-by-field, cloudflared ingress, the update path,
and voice-per-mode caveats); docs/systems/deployment.md updated to match.

Verified live on a throwaway VM: proxy + all-in-one end-to-end through install.sh
(with a real Let's Encrypt cert), tunnel config generation, loopback-only binding,
and the local-image fallback path.
2026-07-06 13:36:29 +02:00
Jannis Braun e7f41b5609 fix(desktop): don't let uiohook-napi rebuild abort the whole workspace install
The @backspace/desktop postinstall runs `electron-rebuild -f -w uiohook-napi`,
which compiles a native module from source and needs a C++ toolchain
(make/g++/python3). On a bare host without those — a typical VPS a self-hoster
clones onto — that failure took the ENTIRE `pnpm install` down (exit 255), so the
server never installed either, even though uiohook-napi is desktop-only and
irrelevant to hosting.

Make the rebuild non-fatal: on failure it now prints a clear one-line warning and
continues. Machines with build tools (CI, desktop devs) are byte-identical — the
fallback never fires; only toolless boxes (which aren't building the desktop app
anyway) skip it. Verified on a bare x86 box: full `pnpm install` now exits 0 and
the server builds and boots from source. Docs note the build-tool requirement for
desktop work and point self-hosters at the Docker installer.
2026-07-06 01:52:06 +02:00
Jannis Braun 24accc3647 docs(env): make the JWT_SECRET requirement explicit in .env.example
Spell out that JWT_SECRET is required and must be a strong 32+ char value, and
that the empty default is intentional (docker compose fails fast rather than the
server boot-looping). Pairs with the compose-level guard added in 028005d.
2026-07-06 01:20:08 +02:00
Jannis Braun 028005d487 fix(compose): fail fast on missing JWT_SECRET instead of a silent restart loop
Without JWT_SECRET the server throws at boot (config.ts) and `restart:
unless-stopped` turns that into an endless crash loop that is invisible unless
you run `docker compose logs backspace`. Guard it exactly like DOMAIN already is,
so `docker compose up` stops immediately with an actionable message. install.sh
generates the secret before bringing the stack up, so this only affects the
manual `cp .env.example .env && docker compose up` path.
2026-07-06 01:12:14 +02:00
Jannis Braun 9d3f72be75 chore: pin Node 20 (LTS) and pnpm 10.34.3
Unpinned `pnpm@latest` in the Dockerfile made fresh builds non-reproducible:
`latest` now resolves to pnpm 11, but the committed lockfile targets pnpm 10, so
`pnpm install --frozen-lockfile` fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on
a clean host. Pin pnpm to 10.34.3 across the Dockerfile, the `packageManager`
field, and the release workflow; pin Node to 20 (LTS) via `.nvmrc` and `engines`
so Docker, CI, and from-source builds all use the same tested toolchain.

Also corrects the docs: the lockfile is v9.0 (requires pnpm 10, not "pnpm 8+"),
and "Node 20+" implied untested newer majors were supported.
2026-07-06 01:04:59 +02:00
Jannis Braun ef020601eb ci(release): build arm64 .deb with host-native fpm (USE_SYSTEM_FPM)
Release Desktop / build (--linux --arm64, ubuntu-24.04-arm) (push) Waiting to run
Release Desktop / build (--linux --x64, ubuntu-latest) (push) Waiting to run
Release Desktop / build (--mac --arm64 --x64, macos-latest) (push) Waiting to run
Release Desktop / build (--win --x64 --arm64, windows-2022) (push) Waiting to run
CI run 2: mac/win/linux-x64 green, but linux-arm64 failed packaging .deb —
electron-builder's bundled fpm is x86_64-only and can't execute on the arm64
runner (Exec format error). The arm64 AppImage built fine; only fpm/.deb broke.
Install fpm natively on the Linux runners and set USE_SYSTEM_FPM=true so both
arches package .deb with a host-native fpm. Preserves arm64 .deb (the reason
the arm64 runner was added — Raspberry Pi users).
v1.0.0
2026-07-03 13:54:56 +02:00
Jannis Braun 9c293b9731 ci(release): fix native uiohook-napi build on Windows + Linux
First real CI run failed on 3 of 4 platforms in postinstall (electron-rebuild
of uiohook-napi):
- Linux (x64+arm64): missing X11 dev headers — 'Xrandr.h: No such file'. Add the
  full libuiohook header set (libxrandr-dev, libxinerama-dev, libx11-xcb-dev,
  libxkbfile-dev, libxkbcommon-x11-dev) derived from its #include list.
- Windows: node-gyp on the windows-latest image can't detect VS 18
  ('unknown version undefined'). Pin to windows-2022 (VS 2022 / v17).
macOS built cleanly and is unaffected.
2026-07-03 13:47:29 +02:00
Jannis Braun 90fe262676 docs(desktop): document unsigned-build auto-update status and CI release pipeline 2026-07-03 13:28:37 +02:00
Jannis Braun 45cee93e7d docs(readme): add desktop download section with per-platform install notes 2026-07-03 13:28:37 +02:00
Jannis Braun 70a648d209 ci(release): build Linux arm64 on native arm runner; pin unsigned mac builds 2026-07-03 13:17:13 +02:00