Commit graph

1316 commits

Author SHA1 Message Date
Ullrich Schäfer
ae0e95fcd5
Merge pull request #458 from trails-cool/federation/fedify-spike
feat(journal): Fedify foundation — WebFinger + actor objects behind FEDERATION_ENABLED
2026-06-06 14:21:00 +02:00
Ullrich Schäfer
9a90b6db55 feat(journal): federation schema + per-user signing keypairs
social-federation tasks 2.1–2.5:

- users.public_key / users.private_key_encrypted (TEXT NULL): RSA 2048
  keypairs as JWK JSON; private key AES-256-GCM encrypted at rest with
  FEDERATION_KEY_ENCRYPTION_KEY.
- remote_actors table: cache of remote AP actors (display fields,
  inbox/outbox URLs, public key, software discovery field, poll cursor).
- activities.remote_origin_iri (UNIQUE) / remote_actor_iri / audience:
  provenance + audience tagging for rows ingested from remote outboxes.
  Replay-safe ingestion keys off the unique origin IRI.
- crypto.server.ts: generalized into createAesCipher(envVar, salt)
  factory; existing INTEGRATION_SECRET surface unchanged.
- federation-keys.server.ts: generate/ensure/load keypairs.
  RSASSA-PKCS1-v1_5 because that's what Mastodon interops on.
- backfill-user-keypairs pg-boss job: enqueued once per server startup
  when FEDERATION_ENABLED=true; only touches users with NULL keys, so
  re-runs are no-ops. New users get keys at registration (both passkey
  and magic-link paths), best-effort with the backfill as safety net.
- design.md: noted open question — activities.owner_id is NOT NULL but
  remote-ingested rows have no local owner; decide in task 7.2.

Schema is additive; zero behavior change while FEDERATION_ENABLED is
off. db:push verified clean against local Postgres.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:15:51 +02:00
Ullrich Schäfer
4ef86e4dc2 feat(journal): Fedify spike — WebFinger + actor objects behind FEDERATION_ENABLED
social-federation tasks 1.1–1.3:

- Add @fedify/fedify pinned to exactly 2.1.16: every 2.2.x release
  depends on @fedify/webfinger@2.2.x which was never published to npm,
  so 2.1.16 is the newest installable version.
- app/lib/federation.server.ts: Federation instance with an actor
  dispatcher serving Person objects for public users; private and
  unknown users 404 (no existence leak). MemoryKvStore for now.
- /.well-known/webfinger resource route delegating to federation.fetch.
- ActivityPub content negotiation on /users/:username via route
  middleware (future.v8_middleware — no loader uses the context arg,
  so the flag is a no-op for existing code).
- FEDERATION_ENABLED env flag (default off) gating every federation
  surface.
- Unit tests exercise the Fedify dispatcher as a remote AP client:
  WebFinger resolution, actor fetch with Mastodon's Accept header,
  private-user 404s, flag-off 404s.

Spike verdict: Fedify fits — URL dispatch, JRD/AP serialization, and
visibility gating all work through framework routes without a custom
server layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:15:27 +02:00
Ullrich Schäfer
8638c2fdfa
Merge pull request #451 from trails-cool/dependabot/npm_and_yarn/production-3c42ababf0
chore(deps): bump the production group with 28 updates
2026-06-06 13:23:10 +02:00
Ullrich Schäfer
9297d1e511
Merge branch 'main' into dependabot/npm_and_yarn/production-3c42ababf0 2026-06-06 13:19:49 +02:00
Ullrich Schäfer
52dc571374
Merge pull request #457 from trails-cool/docs/federation-unblocked
docs: fix stale route-sharing → social-federation dependency
2026-06-06 13:18:41 +02:00
Ullrich Schäfer
81ba2d5de3 docs: fix stale route-sharing → social-federation dependency
The roadmap claimed social-federation depends on route-sharing for
public routes, but that prerequisite shipped independently via
public-content-visibility (2026-04-24) and social-feed (2026-04-25):
visibility columns, public profiles, and IRI-keyed follows are all in
place. Reorder Phase 1 to reflect that federation can start now.

Also flag route-sharing's proposal as needing re-scoping: tasks
1.1-1.3 and 3.1 duplicate already-shipped visibility work, and the
proposed enum contradicts the shipped text-column design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:16:06 +02:00
dependabot[bot]
4a21f4e115 [github-actions] pnpm dedupe 2026-05-31 08:37:48 +00:00
dependabot[bot]
2e6a83b063
chore(deps): bump the production group with 28 updates
Bumps the production group with 28 updates:

| Package | From | To |
| --- | --- | --- |
| [@expo/fingerprint](https://github.com/expo/expo/tree/HEAD/packages/@expo/fingerprint) | `0.19.2` | `0.19.3` |
| [eslint](https://github.com/eslint/eslint) | `10.4.0` | `10.4.1` |
| [fit-file-parser](https://github.com/jimmykane/fit-parser) | `3.0.0` | `3.0.1` |
| [i18next](https://github.com/i18next/i18next) | `26.2.0` | `26.3.0` |
| [turbo](https://github.com/vercel/turborepo) | `2.9.14` | `2.9.16` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.59.4` | `8.60.0` |
| [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) | `13.3.0` | `13.3.1` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `8.0.8` | `8.0.10` |
| [@expo/metro-runtime](https://github.com/expo/expo) | `56.0.12` | `56.0.13` |
| [@maplibre/maplibre-react-native](https://github.com/maplibre/maplibre-react-native) | `11.2.1` | `11.3.0` |
| [@sentry/react-native](https://github.com/getsentry/sentry-react-native) | `8.12.0` | `8.13.0` |
| [expo-constants](https://github.com/expo/expo/tree/HEAD/packages/expo-constants) | `56.0.15` | `56.0.16` |
| [expo-crypto](https://github.com/expo/expo/tree/HEAD/packages/expo-crypto) | `56.0.3` | `56.0.4` |
| [expo-dev-client](https://github.com/expo/expo/tree/HEAD/packages/expo-dev-client) | `56.0.15` | `56.0.18` |
| [expo-linking](https://github.com/expo/expo/tree/HEAD/packages/expo-linking) | `56.0.11` | `56.0.13` |
| [expo-location](https://github.com/expo/expo/tree/HEAD/packages/expo-location) | `56.0.13` | `56.0.15` |
| [expo-notifications](https://github.com/expo/expo/tree/HEAD/packages/expo-notifications) | `56.0.13` | `56.0.15` |
| [expo-router](https://github.com/expo/expo/tree/HEAD/packages/expo-router) | `56.2.6` | `56.2.8` |
| [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated/tree/HEAD/packages/react-native-reanimated) | `4.3.1` | `4.4.0` |
| [react-native-safe-area-context](https://github.com/AppAndFlow/react-native-safe-area-context) | `5.7.0` | `5.8.0` |
| [react-native-worklets](https://github.com/software-mansion/react-native-reanimated/tree/HEAD/packages/react-native-worklets) | `0.8.3` | `0.9.1` |
| [yjs](https://github.com/yjs/yjs) | `13.6.30` | `13.6.31` |
| [@react-router/dev](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dev) | `7.15.1` | `7.16.0` |
| [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) | `7.15.1` | `7.16.0` |
| [@react-router/serve](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-serve) | `7.15.1` | `7.16.0` |
| [@sentry/node](https://github.com/getsentry/sentry-javascript) | `10.53.1` | `10.55.0` |
| [@sentry/react](https://github.com/getsentry/sentry-javascript) | `10.53.1` | `10.55.0` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.15.1` | `7.16.0` |


Updates `@expo/fingerprint` from 0.19.2 to 0.19.3
- [Changelog](https://github.com/expo/expo/blob/main/packages/@expo/fingerprint/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/@expo/fingerprint)

Updates `eslint` from 10.4.0 to 10.4.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.4.1)

Updates `fit-file-parser` from 3.0.0 to 3.0.1
- [Changelog](https://github.com/jimmykane/fit-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jimmykane/fit-parser/commits)

Updates `i18next` from 26.2.0 to 26.3.0
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.2.0...v26.3.0)

Updates `turbo` from 2.9.14 to 2.9.16
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md)
- [Commits](https://github.com/vercel/turborepo/compare/v2.9.14...v2.9.16)

Updates `typescript-eslint` from 8.59.4 to 8.60.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.0/packages/typescript-eslint)

Updates `@simplewebauthn/server` from 13.3.0 to 13.3.1
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.3.1/packages/server)

Updates `nodemailer` from 8.0.8 to 8.0.10
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.8...v8.0.10)

Updates `@expo/metro-runtime` from 56.0.12 to 56.0.13
- [Changelog](https://github.com/expo/expo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits)

Updates `@maplibre/maplibre-react-native` from 11.2.1 to 11.3.0
- [Release notes](https://github.com/maplibre/maplibre-react-native/releases)
- [Changelog](https://github.com/maplibre/maplibre-react-native/blob/main/CHANGELOG.md)
- [Commits](https://github.com/maplibre/maplibre-react-native/compare/v11.2.1...v11.3.0)

Updates `@sentry/react-native` from 8.12.0 to 8.13.0
- [Release notes](https://github.com/getsentry/sentry-react-native/releases)
- [Changelog](https://github.com/getsentry/sentry-react-native/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-react-native/compare/8.12.0...8.13.0)

Updates `expo-constants` from 56.0.15 to 56.0.16
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-constants/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-constants)

Updates `expo-crypto` from 56.0.3 to 56.0.4
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-crypto/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-crypto)

Updates `expo-dev-client` from 56.0.15 to 56.0.18
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-dev-client/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-dev-client)

Updates `expo-linking` from 56.0.11 to 56.0.13
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-linking/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-linking)

Updates `expo-location` from 56.0.13 to 56.0.15
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-location/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-location)

Updates `expo-notifications` from 56.0.13 to 56.0.15
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-notifications/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-notifications)

Updates `expo-router` from 56.2.6 to 56.2.8
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-router/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-router)

Updates `react-native-reanimated` from 4.3.1 to 4.4.0
- [Release notes](https://github.com/software-mansion/react-native-reanimated/releases)
- [Changelog](https://github.com/software-mansion/react-native-reanimated/blob/main/packages/react-native-reanimated/RELEASE.md)
- [Commits](https://github.com/software-mansion/react-native-reanimated/commits/4.4.0/packages/react-native-reanimated)

Updates `react-native-safe-area-context` from 5.7.0 to 5.8.0
- [Release notes](https://github.com/AppAndFlow/react-native-safe-area-context/releases)
- [Commits](https://github.com/AppAndFlow/react-native-safe-area-context/compare/v5.7.0...v5.8.0)

Updates `react-native-worklets` from 0.8.3 to 0.9.1
- [Release notes](https://github.com/software-mansion/react-native-reanimated/releases)
- [Commits](https://github.com/software-mansion/react-native-reanimated/commits/worklets-0.9.1/packages/react-native-worklets)

Updates `yjs` from 13.6.30 to 13.6.31
- [Release notes](https://github.com/yjs/yjs/releases)
- [Commits](https://github.com/yjs/yjs/compare/v13.6.30...v13.6.31)

Updates `@react-router/dev` from 7.15.1 to 7.16.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/dev@7.16.0/packages/react-router-dev)

Updates `@react-router/node` from 7.15.1 to 7.16.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.16.0/packages/react-router-node)

Updates `@react-router/serve` from 7.15.1 to 7.16.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/serve@7.16.0/packages/react-router-serve)

Updates `@sentry/node` from 10.53.1 to 10.55.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.53.1...10.55.0)

Updates `@sentry/react` from 10.53.1 to 10.55.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.53.1...10.55.0)

Updates `react-router` from 7.15.1 to 7.16.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.16.0/packages/react-router)

---
updated-dependencies:
- dependency-name: "@expo/fingerprint"
  dependency-version: 0.19.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: eslint
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: fit-file-parser
  dependency-version: 3.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: i18next
  dependency-version: 26.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: turbo
  dependency-version: 2.9.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: typescript-eslint
  dependency-version: 8.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: nodemailer
  dependency-version: 8.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: "@expo/metro-runtime"
  dependency-version: 56.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: "@maplibre/maplibre-react-native"
  dependency-version: 11.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@sentry/react-native"
  dependency-version: 8.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: expo-constants
  dependency-version: 56.0.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: expo-crypto
  dependency-version: 56.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: expo-dev-client
  dependency-version: 56.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: expo-linking
  dependency-version: 56.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: expo-location
  dependency-version: 56.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: expo-notifications
  dependency-version: 56.0.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: expo-router
  dependency-version: 56.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: react-native-reanimated
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: react-native-safe-area-context
  dependency-version: 5.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: react-native-worklets
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: yjs
  dependency-version: 13.6.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
- dependency-name: "@react-router/dev"
  dependency-version: 7.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@react-router/node"
  dependency-version: 7.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@react-router/serve"
  dependency-version: 7.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@sentry/node"
  dependency-version: 10.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: "@sentry/react"
  dependency-version: 10.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: react-router
  dependency-version: 7.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-31 08:37:04 +00:00
Ullrich Schäfer
f7002be0bd
Merge pull request #450 from trails-cool/fix/extensionless-server-imports
fix: extensionless server-side imports (Grafana app-crash-log smoking gun)
2026-05-26 08:12:41 +02:00
Ullrich Schäfer
10deae88c9
fix: extensionless server-side imports + lint rule to catch them
## Symptom

Grafana's \`app-crash-log\` alert fires on every deploy. The Loki query
that backs it (\`ERR_|FATAL|uncaughtException|…\`) matches:

  Error [ERR_MODULE_NOT_FOUND]: Cannot find module
  '/app/apps/journal/app/lib/logger.server' imported from
  '/app/apps/journal/app/lib/email.server.ts'

A real bug, not a false positive: \`email.server.ts\` line 2 was
\`import { logger } from \"./logger.server\"\` — no extension.

## Why typecheck didn't catch it

\`tsconfig.base.json\` sets \`moduleResolution: \"bundler\"\`. The bundler
resolver accepts extensionless relative imports because Vite / esbuild
/ webpack resolve them at build time. TypeScript was happy.

Production runs \`node --experimental-strip-types server.ts\`, which
uses Node's NodeNext ESM resolver — strict about extensions. The two
resolvers disagree silently for files Node loads directly (server.ts,
\`.server.ts\` modules dynamically imported from it, and jobs).

## Fix

1. **Added \`.ts\` extensions to the 4 broken imports** I could find:
   - \`apps/journal/app/lib/email.server.ts\` → \`./logger.server.ts\`
     (the actual deploy-blocker)
   - \`apps/planner/app/lib/brouter.ts\` → \`./route-merge.ts\`, \`./http.server.ts\`
   - \`apps/planner/app/lib/use-nearby-pois.ts\` → \`./overpass.ts\`
   - \`packages/map/src/MapView.tsx\` → \`./layers.ts\` (caught by the rule)

2. **Added \`eslint-plugin-import-x\` with \`import-x/extensions: always\`**,
   scoped to files Node actually executes raw:
   - \`apps/*/server.ts\`
   - \`apps/*/app/lib/**/*.server.ts\`
   - \`apps/*/app/jobs/**/*.ts\`
   - \`packages/*/src/**/*.{ts,tsx}\`

   Ignored: route-scoped \`*.server.ts\` files (bundled by Vite into the
   React Router build, never loaded by Node), and test files (Vitest's
   own resolver).

## What's still possible

- Non-\`.server.ts\` files in \`app/lib\` (e.g. \`legal.ts\`, \`actor-iri.ts\`)
  could still ship extensionless imports. They're not in the lint
  scope. If one breaks at runtime we can extend the glob — for now
  they tend to be tiny utility modules that don't import other
  relatives.
- The deeper fix would be \`moduleResolution: nodenext\` for server-side
  tsconfig, or bundling the server code so Node never sees raw \`.ts\`.
  Bigger surgery; the lint rule covers the failure mode for now.

Full repo: pnpm typecheck / lint / test all green after \`pnpm install\`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:08:19 +02:00
Ullrich Schäfer
c54f0361cf
Merge pull request #449 from trails-cool/chore/remove-eslint-disables
chore(ts): drop the last 2 `eslint-disable` comments in prod code
2026-05-26 07:38:34 +02:00
Ullrich Schäfer
7918ba052a
chore(ts): drop the last 2 \eslint-disable\ comments in prod code
After this, \`grep -rn 'eslint-disable' apps/ packages/\` returns 0
(excluding tests and node_modules).

**\`apps/planner/app/lib/brouter.ts\`** — \`while (true)\` in
\`readBodyWithCap\` tripped \`no-constant-condition\`. Replaced with
\`for (;;)\` which the rule explicitly allows. No behavior change.

**\`packages/gpx/src/parse.ts\`** — the sync \`parseGpx\` exported a
fallback path that did \`require(\"linkedom\")\` for non-browser sync
use, with an \`eslint-disable\` for \`no-require-imports\`. It was dead
code: \`packages/gpx/src/index.ts\` only re-exports \`parseGpxAsync\`,
and grepping the monorepo finds no caller of the sync version. Deleted
the function entirely; \`getDOMParser\` (the async helper) still serves
the async path with a clean ESM \`await import(\"linkedom\")\`.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:34:09 +02:00
Ullrich Schäfer
5bbb8bfbaa
Merge pull request #448 from trails-cool/chore/jobs-no-payload-generic
chore(ts): eliminate the remaining 4 `as any` casts in production code
2026-05-26 07:14:37 +02:00
Ullrich Schäfer
b0b58d36fb
chore(ts): eliminate the remaining 4 \as any\ casts in production code
Followup to #447. The audit ran on \`as unknown as\` first; this PR
closes out \`as any\` separately. After this, \`grep -rn ' as any\\b'
apps/ packages/\` returns 0 (excluding tests and node_modules).

## Sites fixed

**\`apps/journal/server.ts\`** + **\`packages/jobs/src/types.ts\`** —
\`komootBulkImportJob as any\`. The job had a typed payload
(\`JobDefinition<KomootBulkImportData>\`) but the worker's
\`JobDefinition[]\` array forced a contravariance cast at every site
that mixed typed and untyped jobs. Dropped the generic from
\`JobDefinition\` entirely; handlers narrow their own \`job.data\`. Only
one job (komoot-bulk-import) used the generic, so the surface is tiny.

**\`apps/journal/app/lib/connected-services/fit.ts\`** + same pattern in
\`routes/sync.import.\$provider.server.ts\` — \`parser.parse(buffer as any,
(err: unknown, d: any) => ...)\`. fit-file-parser's TypeScript types
require \`Buffer<ArrayBuffer>\` specifically; a generic Node \`Buffer\`
is structurally \`Buffer<ArrayBufferLike>\` (which includes
SharedArrayBuffer). The runtime accepts either, so narrowed the cast
to \`as Buffer<ArrayBuffer>\` — still a cast, but precise about what
we're asserting and why. Removed the \`(err, d: any) => …\` ad-hoc
callback typings; the library exports a proper \`FitParserCallback\`.

**\`apps/mobile/lib/editor/RouteMap.tsx\`** — \`onLongPress\` handler
took \`(event: any)\`. maplibre-react-native v11 generates the event
type via React Native codegen but doesn't re-export it as a public
TypeScript type. Replaced with a local structural slice of the parts
we actually read.

Net: 4 \`as any\` sites in prod code → 0. \`pnpm typecheck\` / \`lint\` /
\`test\` all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:11:02 +02:00
Ullrich Schäfer
2fc09165da
Merge pull request #447 from trails-cool/chore/tighten-typescript-coercions
chore(ts): replace 7 `as unknown as` shims with proper types
2026-05-26 01:06:11 +02:00
Ullrich Schäfer
a724f862b8
chore(ts): drop two more redundant casts
- RouteMapThumbnail.client.tsx: `{ type: 'Feature', ... } as unknown
  as GeoJsonObject` → `as GeoJsonObject`. The literal already
  satisfies the type; the unknown bridge was unnecessary.

- use-yjs.ts:102: keep the coercion (y-websocket's `.on()` signature
  doesn't include the legacy `"synced"` event even though the
  runtime still fires it), but document why with a comment so future
  readers don't try to drop it.
2026-05-26 01:02:49 +02:00
Ullrich Schäfer
985ec54023
chore(ts): replace 7 \as unknown as\ shims with proper types
Most of the existing coercions are legitimate (Drizzle raw-SQL row
shapes, linkedom DOMParser interop, fit-file-parser's loose \`any\`
callback API) — leaving those is honest. The ones cleaned up here
were shims around APIs that have real types we just hadn't wired:

- **\`window.__leafletMap\`** (4 sites: MapHelpers.tsx, SessionView.tsx ×3)
  Added \`Window\` declaration in \`apps/planner/app/types/global.d.ts\`.
  Callers now use plain \`window.__leafletMap\` typed as
  \`L.Map | undefined\`.

- **\`L.markerClusterGroup\`** (PoiPanel.tsx) — leaflet.markercluster
  augments the global \`L\` namespace at runtime but ships no types.
  Added a \`declare module \"leaflet\"\` block in the same global.d.ts
  with a minimal signature for what we actually use.

- **\`L.DomEvent.preventDefault / .stop\`** taking a Leaflet event
  rather than a native one (PlannerMap.tsx, RouteInteraction.tsx,
  NoGoAreaLayer.tsx). Leaflet events carry the native event under
  \`.originalEvent\`; using that drops the cast entirely.

- **\`_creds as unknown as OAuthCredentials\`** orphan in
  wahoo/webhook.ts — \`_creds\` was unused inside the callback (the
  void-cast was a no-op preserving the type for documentation).
  Replaced with \`async ()\`, dropped the unused import.

Net: 26 \`as unknown as\` / \`as any\` sites → 19, all remaining ones
gated by external lib interop or Drizzle raw-SQL.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:02:25 +02:00
Ullrich Schäfer
ec86f722d9
Merge pull request #444 from trails-cool/test/e2e-journal-planner-handoff
test(e2e): journal↔planner save handoff covers Phase A + Phase B
2026-05-26 01:01:27 +02:00
Ullrich Schäfer
8160393e55
Merge branch 'main' into test/e2e-journal-planner-handoff 2026-05-26 00:57:57 +02:00
Ullrich Schäfer
ce9da71f10
Merge pull request #446 from trails-cool/fix/planner-brouter-response-size
fix(planner/brouter): cap upstream response size to 10MB
2026-05-26 00:57:00 +02:00
Ullrich Schäfer
89a6c5f900
Merge branch 'main' into test/e2e-journal-planner-handoff 2026-05-26 00:56:42 +02:00
Ullrich Schäfer
e1c696d598
Merge branch 'main' into fix/planner-brouter-response-size 2026-05-26 00:53:52 +02:00
Ullrich Schäfer
206f58b1b9
test(e2e/journal-planner-save): pass waypoints in URL + mock BRouter
The first revision opened the planner session URL without waypoints,
so the in-browser Yjs doc was empty, no route was computed, and the
Save button shipped an empty GPX → journal callback returned 400 →
\"Saved!\" never appeared and the test timed out.

The real journal→planner handoff (in
\`apps/journal/app/routes/api.routes.\$id.edit-in-planner.ts\`) encodes
the planner's session-creation response (initialWaypoints / noGoAreas /
notes) as URL query params on the redirect. The test now mirrors that,
passing a 2-waypoint encoded blob via the \`?waypoints=\` param.

Also mocks BRouter via the existing \`mockBRouter(page)\` fixture so the
route compute is deterministic — \`planner-coloring.test.ts\` uses the
same pattern. Otherwise the test would race against a real BRouter
cold-start on CI.

Asserts canvas is visible before clicking Save (proxy for \"routeData
is populated\" — the same condition that gates a non-empty GPX in
\`SaveToJournalButton\`).
2026-05-26 00:53:16 +02:00
Ullrich Schäfer
6c5d4e6510
Merge pull request #445 from trails-cool/fix/planner-api-sessions-limit
fix(planner): bound /api/sessions listing (default 50, max 200)
2026-05-26 00:50:35 +02:00
Ullrich Schäfer
6f2b4450df
fix(planner/brouter): cap upstream response size to 10MB
Planner-audit #9. \`fetchSegment\` previously \`await response.json()\`
on any body BRouter returned. A misbehaving or compromised upstream
could OOM the planner process by returning gigabytes of JSON.

New \`readBodyWithCap()\`:
- Reject upfront when \`content-length\` declares over the cap.
- Stream the body and abort the reader once received bytes exceed
  the cap (handles the case where upstream lies about content-length
  or omits it).

Cap chosen at 10 MB — real per-segment GeoJSON is <100 KB; even the
longest realistic multi-day route stays well under 2 MB. Above 10 MB
is upstream bug or abuse; we'd rather error.

Applied to both \`fetchSegment\` (GeoJSON path) and \`computeSegmentGpx\`
(GPX path).

Tests: 3 cases (within cap, content-length over cap, streamed body
mid-cap abort).
2026-05-26 00:48:45 +02:00
Ullrich Schäfer
6f18ce8099
fix(planner): bound /api/sessions listing (default 50, max 200)
Addresses planner-audit #8. `listSessions()` had no LIMIT — every call
to `GET /api/sessions` returned every non-closed session and did a
full table scan ordered by last_activity. On a long-running planner
host that's both a memory cliff and a query-plan footgun.

Now: defaults to 50 rows, clamps to [1, 200], accepts `?limit=` for
pagination-light scenarios. `sessions_last_activity_idx` (added in
#438) backs the ORDER BY + LIMIT efficiently.
2026-05-26 00:46:38 +02:00
Ullrich Schäfer
be64e2df5c
test(e2e): journal↔planner save handoff covers Phase A + Phase B
New \`e2e/journal-planner-save.test.ts\` exercises the full save flow:

1. Seed a routeId + JWT via \`/api/e2e/seed\` (journal).
2. POST to \`/api/sessions\` on the planner with \`callbackUrl\` +
   \`callbackToken\` — mirrors what the journal's \`edit-in-planner\`
   action does server-to-server.
3. Open the planner session in a real browser.
4. Click \"Save to Journal\". The button POSTs sessionId+GPX to the
   planner's \`/api/save-to-journal\` action (Phase A); the action
   forwards to the journal callback with the Bearer.

Test 1 asserts:
- The journal's route ends up with geometry (round-trip works).
- The exact JWT string is **never** present in any browser-issued
  request body or \`Authorization\` header — i.e. Phase A correctly
  keeps the token server-side.

Test 2 asserts:
- A second click on Save reuses the same stored JWT, the journal's
  jti consumer rejects it, and the planner UI surfaces the error.
  This is the Phase B replay guard exercised end-to-end through the
  UI rather than just the API.

Added \`journal-planner-save\` Playwright project (no baseURL — the
test navigates both apps using absolute URLs).

Full repo: pnpm typecheck / lint / test all green (cached).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:43:56 +02:00
Ullrich Schäfer
11edcbccb3
Merge pull request #443 from trails-cool/fix/journal-jwt-jti-single-use
fix(journal): single-use JWT for route callback tokens (#2 Phase B)
2026-05-26 00:41:49 +02:00
Ullrich Schäfer
b4c64a40e7
fix(journal): single-use JWT enforcement for route callback tokens (#2 Phase B)
After Phase A (#442) moved the journal callback token off the browser,
the token was still replayable on the wire until \`exp\` (7 days). This
PR makes each token strictly single-use.

Changes:

- **\`journal.consumed_jwt_jti\` table** — \`jti TEXT PRIMARY KEY,
  consumed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ\`. Picked up by
  drizzle-kit push on deploy.
- **\`createRouteToken\` now sets a \`jti\` claim** (\`randomUUID()\`).
- **\`verifyRouteToken\` atomically consumes the jti** via
  \`INSERT … ON CONFLICT DO NOTHING RETURNING jti\`. Postgres
  serializes the insert, so exactly one concurrent caller wins; the
  rest see an empty result and throw \`TokenAlreadyConsumedError\`.
  Tokens without a \`jti\` claim (i.e. minted before this PR) are
  also rejected — the right call: any in-flight legacy token sitting
  in a planner session is replayable, and we'd rather fail-loud than
  silently grandfather them in.
- **\`consumed-jti-sweep\` job** — daily 03:45 UTC cron that
  \`DELETE WHERE expires_at < now()\`. Keeps the table tiny; offset
  from the other purge jobs to spread load.
- **e2e replay test** — \`integration.test.ts\` now exercises a
  same-token double-submit and asserts the second returns 401 with
  \`/consumed|already/i\`.

UX implication worth flagging: a user who clicks \"Save\" twice (or whose
network retries a failed POST) sees an error on the second attempt.
They go back to the journal for a fresh \"Edit in Planner\" link.

Full repo: pnpm typecheck / lint / test all green (177 + 31 integration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:38:08 +02:00
Ullrich Schäfer
c525240d0e
Merge pull request #442 from trails-cool/fix/planner-save-token-server-side
fix(planner): keep journal callback token off the client (#2 Phase A)
2026-05-26 00:32:51 +02:00
Ullrich Schäfer
0917de6080
fix(planner): keep journal callback token off the client (#2 Phase A)
The Save-to-Journal flow had the browser fetch the journal with a
\`Bearer \${callbackToken}\` header. The JWT was visible in DevTools,
exfiltratable via any XSS or browser extension, and the planner's
\`loader\` shipped it down to the client as part of the page payload.

Now:

- **New action**: \`POST /api/save-to-journal\` (\`routes/api.save-to-journal.ts\`).
  Body: \`{ sessionId, gpx }\`. The action loads \`callbackUrl\` +
  \`callbackToken\` from \`planner.sessions\` (set at /new time when the
  user came from the journal), POSTs to the journal server-to-server
  with the Bearer, and forwards the response.

- **\`SaveToJournalButton\`**: drops the \`callbackUrl\` + \`callbackToken\`
  props. Takes \`sessionId\` only and POSTs to the planner action.

- **\`session.\$id.tsx\` loader**: stops returning \`callbackUrl\` /
  \`callbackToken\` to the client. Returns a single \`hasJournalCallback\`
  boolean so the button still knows whether to render.

- **\`SessionView\`**: same prop simplification.

Trust model is unchanged: the same \`sessionId\` that grants Yjs
membership grants save authority. Knowing the URL = ability to act.
The action only adds a server-side hop so the JWT never reaches
browser JS.

Phase B (jti single-use enforcement on the journal side) follows in
a separate PR — needs a journal DB column + verifier change.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:29:24 +02:00
Ullrich Schäfer
3dcc17152b
Merge pull request #441 from trails-cool/fix/planner-ws-session-existence-check
fix(planner): require session row before accepting Yjs WebSocket upgrade
2026-05-26 00:27:35 +02:00
Ullrich Schäfer
4e2c5f7d6b
fix(planner): require session row before accepting Yjs WebSocket upgrade
Addresses planner-audit #1 (CRITICAL — WebSocket joins unauthenticated)
with the narrow fix. The upgrade handler used to immediately
\`handleUpgrade\` for any \`/sync/<id>\` path, then \`getOrLoadDoc\` *created*
a Y.Doc on demand. An attacker connecting to random sessionIds could:

  - exhaust process memory by spinning up Y.Doc objects for sessions
    that don't exist (no DB row backed them), and
  - resurrect closed/expired sessions by reconnecting (closed rows
    were still cacheable to \`docs\`).

Now: the upgrade handler calls \`getSession(sessionId)\` first
(\`SELECT \\* FROM planner.sessions WHERE id = ? AND closed = false\`).
Missing or closed → \`socket.destroy()\` before handshake. DB unreachable
also closes — fail closed; the client reconnects after backoff.

Costs one DB roundtrip per upgrade. Upgrades are rare vs message
volume, so the overhead is negligible.

Note: we are NOT adding a join token. The planner is anonymous-by-design
(see CLAUDE.md \"sessions are anonymous and ephemeral\") — knowing the
URL still equals membership. The check here only guards against
attacker-supplied sessionIds with no backing row.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:24:16 +02:00
Ullrich Schäfer
643266e619
Merge pull request #440 from trails-cool/fix/planner-yjs-doc-size-cap
fix(planner): cap per-WS-frame + per-session Yjs doc size
2026-05-26 00:16:40 +02:00
Ullrich Schäfer
2b0717fe21
fix(planner): cap per-WS-frame + per-session Yjs doc size
Addresses planner audit #5 — a connected client could feed unlimited
waypoints / no-go / notes into the Yjs doc, growing the in-memory map
and the persisted \`sessions.yjs_state\` blob until OOM or DB blowout.

Two guards:

1. **Per-frame**: \`MAX_MESSAGE_BYTES = 256 KB\`. Anything bigger arrives
   from a buggy or hostile client — a single waypoint add is hundreds
   of bytes. Oversized frame → \`ws.close(1008)\` and drop the message.

2. **Per-doc**: \`MAX_DOC_BYTES = 5 MB\`. After a sync-message apply
   succeeds, recompute \`Y.encodeStateAsUpdate(doc).byteLength\`. If it
   crossed the cap, mark the session \"quarantined\": close every
   connected client (\`1008 policy violation\`), and short-circuit
   subsequent handleMessage calls so no further bytes can be applied
   and the debounced save can't write an oversized blob to Postgres.

The 5 MB limit is generous — a typical multi-day route's serialized
state is well under 100 KB. The cap exists to make abuse expensive,
not to constrain real use.

Exports \`MAX_MESSAGE_BYTES\`, \`MAX_DOC_BYTES\`, and \`docByteSize\`
for testing. Tests cover:
- constants are positive and ordered
- docByteSize is 0 for unknown sessions
- a realistic 500-waypoint route stays well under the cap
- ~6MB of garbage in a Y.Text trips the cap (smoke test for the guard)

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:13:01 +02:00
Ullrich Schäfer
078e68feaf
Merge pull request #439 from trails-cool/fix/planner-url-validation
fix(planner): validate callback/returnUrl + cap session URL-param payloads
2026-05-26 00:09:39 +02:00
Ullrich Schäfer
51e6b8a0d7
fix(planner): validate callback/returnUrl + cap session URL-param payloads
Addresses planner audit #3 (SSRF via callbackUrl) and #7 (URL-param
size). Two attack surfaces hardened:

1. /new loader — \`callback\`, \`token\`, \`returnUrl\`, \`gpx\` query
   params now validated:
   - callbackUrl: must be a valid absolute http(s) URL ≤ 2048 chars.
     If \`PLANNER_CALLBACK_ALLOWED_HOSTS\` is set (comma-separated),
     the host must match — defense-in-depth SSRF guard for self-
     hosted instances. Unset = no allowlist (dev / open self-host).
   - token: max 2048 chars.
   - returnUrl: must be a same-origin path or absolute http(s) URL
     ≤ 2048 chars. Rejects \`javascript:\`, \`data:\`, and
     protocol-relative \`//host\` (which would resolve to a remote
     origin on HTTPS pages).
   - gpx: ≤ 2 MB encoded.
   Invalid input throws 400 from the loader.

2. /session/:id default-export component — \`waypoints\`, \`noGoAreas\`,
   \`notes\`, \`returnUrl\` URL params now bounded before
   \`JSON.parse\` / use:
   - waypoints / noGoAreas: ≤ 50KB each; over-cap returns undefined
     (component starts with empty initial state, same as malformed).
   - notes: ≤ 10KB.
   - returnUrl: ≤ 2KB + same scheme rules as #1.

Pulled the URL validation into \`lib/url-validation.server.ts\` so
both routes (and any future caller) share the same rules.

Tests: \`url-validation.server.test.ts\` (14 cases — schemes,
allowlist, length caps, protocol-relative guards, env parsing).

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:05:47 +02:00
Ullrich Schäfer
e35a8e27c8
Merge pull request #438 from trails-cool/fix/planner-sessions-lastactivity-idx
fix(planner): add index on sessions.last_activity for the expire job
2026-05-26 00:03:04 +02:00
Ullrich Schäfer
d2b28a1164
fix(planner): add index on sessions.last_activity for the expire job
The hourly `expireSessions()` cron runs `DELETE FROM planner.sessions
WHERE last_activity < cutoff`. Without an index on `last_activity`,
that's a full table scan growing linearly with the total sessions
ever created (planner sessions are never user-deleted; expiry is the
only churn path).

`drizzle-kit push` picks this up on next deploy.
2026-05-25 23:59:10 +02:00
Ullrich Schäfer
d07207464e
Merge pull request #437 from trails-cool/fix/planner-brouter-timeout
fix(planner/brouter): add 30s timeout to outbound fetch
2026-05-25 23:57:04 +02:00
Ullrich Schäfer
d70d6ee8a8
fix(planner/brouter): add 30s timeout to outbound fetch
Mirrors journal PR #5. `fetchSegment()` and `computeSegmentGpx()` in
lib/brouter.ts called `fetch()` with no AbortSignal — a hung or slow
BRouter would stall the request handler indefinitely.

New `lib/http.server.ts::fetchWithTimeout()` (same as the journal's
helper) wraps fetch with `AbortSignal.timeout(30_000)`, composable
with a caller-supplied signal via `AbortSignal.any()`.

Tests: lib/http.server.test.ts (2 cases — timeout abort, happy path).
2026-05-25 23:53:19 +02:00
Ullrich Schäfer
e647a29633
Merge pull request #436 from trails-cool/fix/e2e-planner-coloring-flake
test(e2e/planner-coloring): bump canvas timeout 10s→20s to absorb cold-CI lazy load
2026-05-25 23:48:38 +02:00
Ullrich Schäfer
d38b808a11
test(e2e/planner-coloring): bump canvas timeout 10s→20s
The three planner-coloring tests wait for the elevation chart's
\`<canvas>\` after seeing the Yjs \"Connected\" text. \`ElevationChart\`
returns \`null\` until \`points.length >= 2\` — i.e. until the BRouter
response (even mocked) has flowed through the Yjs round-trip and
updated \`yjs.routeData\`. The chart module is also lazy-imported
(\`Suspense\`) — cold CI runs need to fetch the chunk before mounting.

Saw the failure on PR #424's CI run: canvas \"element(s) not found\"
after the 10s window, both initial run and retry. Other planner
suites that wait on \`.leaflet-container\` (which renders
unconditionally and isn't behind a lazy boundary) don't see this.

Bumped \`CANVAS_TIMEOUT\` to 20s. Not masking a real regression — the
lazy chunk + Yjs apply is a real serial cost that the test budget
didn't reflect.
2026-05-25 23:44:53 +02:00
Ullrich Schäfer
f02edd346e
Merge pull request #435 from trails-cool/fix/ci-run-integration-tests
ci: wire integration tests into the CI E2E job
2026-05-25 23:31:48 +02:00
Ullrich Schäfer
ded70a5404
ci: wire integration tests into the CI E2E job
The five \`*.integration.test.ts\` files in apps/journal (explore, follow,
demo-bot, notifications, notifications-fanout — 31 tests total) were
gated behind \`EXPLORE_INTEGRATION=1\` / \`FOLLOW_INTEGRATION=1\` /
\`DEMO_BOT_INTEGRATION=1\` / \`NOTIFICATIONS_INTEGRATION=1\`. The unit-test
job doesn't have Postgres, so they correctly skipped there — but no
CI job set the env vars, so they were effectively dead code.

Added a step in the E2E job (which already has Postgres + schema
pushed) that flips all four gates and runs the integration files
serially (\`--no-file-parallelism\` — they share the schema and trip FK
constraints if run in parallel; ~2.5s sequential anyway).

Wiring them up surfaced two real issues, both fixed here:

1. **\`createRoute\` silently dropped \`input.visibility\`** — every
   caller passing \`visibility: \"public\"\` (including the demo-bot)
   was getting the column's \`private\` default. Spread now mirrors
   \`createActivity\`'s pattern. Demo-bot routes have actually been
   private in production all this time — they were rendering on the
   home feed only via the \`activity_published\` fan-out from the
   *activity*, not as visible *routes*.

2. **The demo-bot test's fetch stub was incomplete** — it stubbed
   \`.text()\` but the planner-session preflight calls \`.json()\`. The
   stub now branches on URL: \`/api/sessions\` returns
   \`{ sessionId: 'test-session' }\` JSON, the BRouter call returns the
   stub GPX text.

Full repo: pnpm typecheck, pnpm lint, pnpm test all green
(177 unit-test pass + 31 integration pass = 208 total, no skips).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:27:44 +02:00
Ullrich Schäfer
1263372eef
Merge pull request #434 from trails-cool/fix/sentry-dsn-no-hardcoded-fallback
fix(sentry): remove hardcoded DSN fallbacks; supply via env in CI
2026-05-25 23:12:55 +02:00
Ullrich Schäfer
edb618fe40
fix(sentry): remove hardcoded DSN fallbacks; supply via env in CI
Self-hosted instances no longer inherit the trails.cool flagship Sentry
DSNs. Code paths now read DSN strictly from env — unset = no Sentry
init, no events sent.

Flagship continues to report unchanged: cd-apps.yml and cd-staging.yml
inject the public DSNs as workflow env vars, which feed into:
- runtime via \`infrastructure/app.env\` / \`staging.env\`
  (\`SENTRY_DSN_JOURNAL\` / \`SENTRY_DSN_PLANNER\` → compose env per service)
- build time via docker build-arg \`VITE_SENTRY_DSN\` (journal only;
  planner has no client Sentry init).

Sentry DSNs are public-by-design (transmitted unencrypted from the
client JS bundle), so embedding them as plaintext workflow env vars
is no worse than the runtime exposure. Forks should replace these with
their own DSNs or remove the workflow env lines to ship Sentry-free.

Files changed:
- apps/journal/server.ts: \`process.env.SENTRY_DSN\` only, no fallback
- apps/planner/server.ts: same
- apps/journal/app/lib/sentry.client.ts: \`import.meta.env.VITE_SENTRY_DSN\`
  only; falsy = skip init
- apps/journal/Dockerfile: new \`ARG VITE_SENTRY_DSN\` baked into build
- infrastructure/docker-compose.yml: pass \`SENTRY_DSN\` per service
- infrastructure/docker-compose.staging.yml: same
- .github/workflows/cd-apps.yml: workflow env + build-arg + app.env echo
- .github/workflows/cd-staging.yml: same

Full repo: pnpm typecheck, pnpm lint, pnpm test, journal build all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:07:07 +02:00
Ullrich Schäfer
f239b4adee
Merge pull request #433 from trails-cool/fix/infra-compose-secret-defaults
fix(infra): refuse compose up when production secrets are missing
2026-05-25 23:03:06 +02:00
Ullrich Schäfer
e2bf3ddb94
fix(infra): refuse compose up when production secrets are missing
infrastructure/docker-compose.yml had \`\${VAR:-default}\` fallbacks for
JWT_SECRET, SESSION_SECRET, and POSTGRES_PASSWORD that silently
substituted known-weak values (\`change-me-in-production\`, \`trails\`)
when the env wasn't set. A misconfigured prod deploy would happily come
up with these defaults — JWT/session forgery + a guessable Postgres
password.

Switched the four critical substitutions to \`\${VAR:?message}\` so
\`docker compose up\` fails loud instead. Mirrors the existing pattern
already used for BROUTER_URL/BROUTER_AUTH_TOKEN and (in staging) for
JWT_SECRET/SESSION_SECRET.

Sites fixed:
- journal:   DATABASE_URL (POSTGRES_PASSWORD), JWT_SECRET, SESSION_SECRET
- planner:   DATABASE_URL (POSTGRES_PASSWORD)
- postgres:  POSTGRES_PASSWORD (container itself)
- exporter:  DATA_SOURCE_NAME (POSTGRES_PASSWORD)
- staging:   both DATABASE_URLs

Production already provides all three via SOPS-encrypted
secrets.app.env, so this is a defense-in-depth change with no behavior
change on a properly-configured deploy.

Updated infrastructure/.env.example to make SESSION_SECRET explicit
(was missing) and call out the trio as required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:59:09 +02:00