mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
e5f1f573c5551f0001404536e06252829562da3c
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
724c88c201 |
style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them. |
||
|
|
9aab9e7513 |
feat(auth): track sign-in sessions so a session can end before it expires
A session token was a signed JWT and nothing else. Logout only cleared the cookie, a 30-day "remember me" token stayed valid wherever a copy of it had ended up, and nobody could sign out one device short of changing the password or rotating the JWT secret for everyone. Every session token now carries an id (jti) naming a row in the new user_sessions table (id, user_id, created_at, last_seen_at, expires_at, revoked_at, user_agent cut to 256 characters; no address is kept). The rows live behind UserSessionsRepository and the new sessions domain, whose SessionsService is the one issuer: AuthService.generateToken and the OIDC sign-in both go through it, and sliding renewal re-signs a tracked token under the same id and moves its expiry. verifyJwtAndLoadUser takes the repository and refuses a token whose session is revoked, expired or gone, refreshing last_seen_at at most every five minutes. A token without a jti was issued before this change; it passes until its own expiry (or a password change), so the upgrade signs nobody out, and renewal turns it into a tracked session. Sessions end on logout (the one the request names, verified even when expired), on a password change (all, the device gets a new one), a password reset, an admin setting a password, reset-admin.js, an MFA disable (all but the current one), an admin clearing a user's MFA, and with the account. GET /api/auth/sessions lists the active sessions, DELETE /api/auth/sessions/:id ends one and POST /api/auth/sessions/revoke-others ends all but the current, with their contracts in @trek/shared and two new audit actions. There is no UI for them yet. MCP exposes no account or session tools, so there is no parallel tool to change; an MCP call made with a session JWT as bearer goes through the same check. The token in the login, register, demo-login, MFA and passkey bodies stays for API clients but is marked deprecated in the API docs and in the client type, which never read it. A nightly session-purge job on CronRegistrarService deletes the expired and revoked rows. The migration is 042500. The sessions domain gets its own coverage entry at the 80 floor until a full run measures it. |
||
|
|
3e1234112d |
fix(demo): save the first baseline after a reopen that seeded too
The demo seed runs in the schema bootstrap, at boot and again after every reopen. Since the baseline save moved out of the seed into DemoResetJob, only the boot read the seed's mark. A restore onto a demo instance with no baseline, whose archive held no admin trips, seeded the example trips during the reopen and then saved nothing, so the hourly reset kept logging "No baseline found"; the mark also stayed set for the rest of the process. Before the move, that path saved the baseline from inside the seed. DatabaseLifecycle now runs listeners after a reopen's schema bootstrap, and DemoResetJob registers one in demo mode that does the same check as at boot: save through the backup port when the seed just ran and no baseline exists. Each check clears the mark. A failed save is logged, never turned into a failed reopen. AdminModule imports DatabaseLifecycleModule from the shared database kernel for it. |
||
|
|
b658b5e68d |
docs(server): say why the admin domain does not import DatabaseBackupModule
DatabaseBackupModule stays @Global, but its comment gave a vague reason. The real one is concrete: the backup domain already reaches admin through auth and settings, so importing the module from AdminModule would add a domain cycle that lint:boundaries rejects. The module comment now says that and points at the e2e harness that imports it by hand, and AdminModule notes where its DATABASE_BACKUP comes from. |
||
|
|
de8caa81a2 |
Feat/orm : mikroorm (#2416)
* base entities
* update deps
* fix date as string
* rename entities
* migrations + seeders
* feat(db): DbTimestampType keeps DATETIME columns as the CURRENT_TIMESTAMP text
* feat(db): discover entities from an explicit list instead of a glob
* test(db): createTestOrm binds an ORM to a suite's own SQLite handle
* feat(db): dialect helper is the one place SQL functions are spelled
* test(db): dialect helper refuses malformed column references and day counts
* feat(db): snake_case entity shape and the first three repositories (Days, DayNotes, Trips)
* fix(db): repositories mirror the statement's column set and guard their row types
* feat(db): request-context and unit-of-work helpers for the ORM, with concurrency tests
* test(server): integration suites boot on the migrated schema snapshot
* test(server): snapshot harness keeps the legacy id semantics
* fix(test): route MikroORM's dynamic imports through vitest from the setup file, not the ORM config
* test(db): coverage ratchet entries for the ORM foundation
* test(server): websocket suites boot on the migrated schema snapshot
* test(server): coverage's uncovered-file pass goes through the SWC transform
* fix(db): dialect helpers dispatch on the platform; lint gate for the repository layer
* test(server): ORM suites open the schema snapshot; RESET_TABLES derived from the schema
* chore(server): promise safety net and sweep tooling for the async migration
* refactor(realtime, systemNotices, addons, airports, audit, categories, permissions, query-helpers, tags, transit, trip-membership): async DB-touching methods; transactions through UnitOfWork
* fix(server): observe every promise the async sweep left unobserved (timer, handshake, plugin-runtime test)
* test(doc-sync): unit suites await the methods the async sweep made async
* refactor(atlas, school-holidays, settings): async DB-touching methods; transactions through UnitOfWork
* chore(server): lint rule refusing a Promise used as a plain value, and the misses it found
* test(server): suites await the methods Task 1's forced edits made async
* refactor(budget, common, day-notes, days): async DB-touching methods; transactions through UnitOfWork
* test(oauth): controller suite awaits the async service
* chore(server): db-call-graph --unawaited lists calls to async methods whose result is used without await
* refactor(llm-parse, mcp-shared, oauth, place-shadow, route-usage, storage, todo, trip-invite): async DB-touching methods; transactions through UnitOfWork
* refactor(plugins): async DB-touching methods; transactions through UnitOfWork
* chore(server): db-call-graph --sync stops its transitive walk at an async callee
* chore(plugins): drop gate-only asyncs; observe the scheduler's rejection; async event-sink stub
* refactor(notifications, place-photos, backup): async DB-touching methods; transactions through UnitOfWork
* refactor(auth, files): async DB-touching methods; transactions through UnitOfWork
* test(server): suites await the methods earlier sweep groups made async
* refactor(tokens): async DB-touching methods; the auth token handlers await them
* test(files): upload filters fail closed on an excluded or unresolvable type
* refactor(collab, collections, maps): async DB-touching methods; transactions through UnitOfWork
* refactor(memories, photos, packing, share): async DB-touching methods; transactions through UnitOfWork
* chore(server): lint rule also refuses a Promise handed to expect() without resolves/rejects
* refactor(vacay, admin, doc-sync): async DB-touching methods; transactions through UnitOfWork
* fix(vacay): type the loop accumulators the async sweep introduced
* refactor(journey, oidc, place-enrichment, tokens, trip-members): async DB-touching methods; transactions through UnitOfWork
* fix(server): the journey hooks are awaited by their callers, not detached
* refactor(assignments, public-api, accommodations, places): async DB-touching methods; transactions through UnitOfWork
* refactor(trips): rewrite AccommodationsService.createAccommodation .bind alias
Recipe R4 trap named in the Task 12 brief: a .bind alias of an
AssignmentsService/AccommodationsService method is typed any and hides a
missing await from tsc and the promise lint rules. createAccommodation
belongs to this task's group, so rewritten as an arrow forwarding to the
method instead of a bound reference.
* refactor(assignments): drop the unused Trip type alias
* refactor(reservations, booking-import, calendar, integrations, roadtrip): async DB-touching methods; transactions through UnitOfWork
* refactor(roadtrip, reservations): return await on transactional; document the single-flight promises
* refactor(trip-read-model, trips, feeds): async DB-touching methods; transactions through UnitOfWork
* refactor(server): remove DatabaseService.transaction — every transaction runs through UnitOfWork
* refactor(trips, feeds): drop added return annotations; no double await on the feed guard
* fix(server): phase 1 review follow-ups — awaited returns, logged upload-filter rejections, covered handshake/notice branches, call-graph gate in CI, dead test imports
* fix(server): upload filters answer multer once on a throwing callback; positive call-graph gate test
* test(db): entity metadata must match the migrated schema; fix the four drifts it found
* feat(db): primary-relation entities proven (BudgetCategoryOrder, VacayUserSettings)
* test(db): parity gate also holds declared pivot entities; per-field column types; missing-set ratchets both ways
* test(db): numeric ids for the primary-relation spike
* feat(db): re-runnable entity generator encoding the snake_case/DbTimestampType rules
* fix(db): generator guard rails — validation cannot mask hidden()/index loss; typed JSON; loud failures
* fix(db): generator keeps NOT NULL boolean defaults; fails if a JSON interface import survives
* refactor(db): every entity in the snake_case shape; document-sync entities added; one repository per entity
* fix(db): no NaN class-field initialisers; entity generator --check gate in CI; parity normaliser keeps literal case
* chore(db): entity --check reports stray files; class-field census is a ratchet; migrate-encryption typechecked
* feat(db): AppSettings and Users repositories; forFeature wiring pattern
* refactor(permissions): permission flags through AppSettingsRepository
* fix(server): boot-time settings read runs inside an explicit ORM request context
* refactor(audit): audit log writes through AuditLogRepository
* fix(db): PK reads refresh past the identity map; typed owned-lookup generics; e2e ORM module mirrors production config
* docs(db): owned-lookup call-site convention; refresh side effect; e2e ORM module spread notes
* fix(audit): audit rows are native inserts, not a unit-of-work flush; closed test ORMs
* fix(server): non-HTTP entrypoints run inside an ORM request context; permission loads fail closed on context errors
* refactor(categories, tags): repositories replace raw SQL
* refactor(addons): addon catalog and photo providers through repositories
* fix(db): generator captures inline UNIQUE constraints
SqliteSchemaHelper.getIndexes() drops every unnamed sqlite_autoindex_
entry before entity-generator ever sees it, so a table-level
UNIQUE(...) constraint with no separate named CREATE UNIQUE INDEX
(settings' UNIQUE(user_id, key) among ~40 others) never reached the
generated entity's uniques: metadata. Adds a general
RULE9_addImplicitUniqueConstraints (skips a column set identical to
the table's own primary key — AppSettings.key/Addons.id noise) fed by
a PRAGMA index_list/index_info scan, and regenerates every entity.
* chore(addons): well-known gate wraps only the addon read in a request context; drop unused test factories
* fix(platform): well-known gate tolerates a synchronous addon read from the context helper
* fix(db): PK lookups inside patch refresh past the identity map; corrected identity-map notes
* refactor(settings): user and instance settings through repositories
SettingsRepository (getForUser/upsertForUser/getOne/deleteForUser) replaces
the per-user settings table's raw SQL; SettingsService drops DatabaseService
entirely. instance-api-keys.ts's readInstanceApiKey/writeInstanceApiKey/
resolveApiKey take an explicit AppSettingsRepository/UsersRepository instead
of a raw connection.
An earlier version resolved those two repositories itself from the active
MikroORM request context, so none of its six callers needed to change. That
worked in production and e2e (always inside a real request) but broke
service construction in plain unit tests across every calling domain
(addons, auth x2, maps, transit, unsplash - 185 failures on a full run),
since none of them wrap a request context. Each of those six services now
injects its own AppSettingsRepository/UsersRepository and passes them
through explicitly, matching how every other converted repository in this
migration is consumed; their own SQL is untouched (outside Plan 3a).
* test(server): harness constructors follow the Task 5 repository signatures; one test ORM per handle in the plugin host
* fix(db): generator names relation properties in captured UNIQUE constraints
* fix(settings): drop dead wiring and the speculative deleteForUser; strict row assertions
* fix(server): plan 3a review follow-ups — fail closed without an ORM at every choke point; seam and D5 ratchet tests; dead method and DDL copies removed
* fix(server): plan 3a re-review follow-ups — plugin boot activation and the airports backfill inside a request context; the supervisor answers the child before failing closed; BOOT-SWEEP-001 drives the real boot hooks; ws adapter seam pinned
* feat(db): generated entities declare their repository type; test casts dropped
* fix(server): plan 3a re-review 2 follow-ups — absent-ORM boot branch pinned; airports boot hook never rejects; BOOT-SWEEP-001 asserts the seven routes; duplicate comment removed
* feat(server): pre-init photo route and WS handshake run in a request context; InviteTokensRepository
* refactor(auth): users table through UsersRepository; JWT and MFA lookups repository-backed
* refactor(tokens): MCP and API tokens through McpTokensRepository
* refactor(auth): passkeys and registration invites through repositories
* fix(db): row-out repository reads bypass the identity map — stale write-back over nativeUpdate at flush; insertUser via em.insert; settings round-trip pinned
* fix(server): non-numeric route ids answer the legacy not-found, never NaN (tokens, categories, tags); mint path back to two statements; identity-map regression pinned
* refactor(oauth): OAuth server tables through repositories
OauthClientsRepository/OauthTokensRepository/OauthConsentsRepository
replace OauthService's raw SQL (OA1-OA33). revokeChain's recursive CTE
and the oauth_clients join (its FK actually targets client_id, not the
row id the generated relation assumes) go through em.getKysely();
OA19's chain revoke is one nativeUpdate($in); OA12's consent grant is
em.upsert on the (client, user) unique key. revokeAllForUser built now
for Task 5. Every row-out read is disableIdentityMap: true; inserts go
through em.insert.
* fix(auth): non-numeric passkey and invite ids answer the legacy 404, never NaN; invite join type pinned
* fix(db): generator emits the referenced column for non-PK foreign keys; OAuth client joins through the relation; FK parity un-skipped
* refactor(db): TrekRepository base — every repository path validates the request context and reads bypass the identity map by default
* refactor(auth): AuthService and account cleanup through repositories
* refactor(oidc): OIDC identities and settings through repositories
* fix(auth): invite validation keeps expires_at null on the wire; password transaction scopes and reset-token burn pinned
* fix(server): plan 3b close-out — case folding on both sides for identifier lookups (non-ASCII login regression); coverage ratchet restored; repository scaffold and instanceof ratchet on TrekRepository; D-shape tests load-bearing
* refactor(server): trip-access primitives async at every caller
* refactor(trips): trip access through TripsRepository; demo seed in a request context; dialect helpers for the trips cluster
* refactor(days): days and day notes through repositories
* refactor(assignments): day assignments through repositories
* refactor(server): trip-membership, place-shadow, place-enrichment, place-photos and query helpers through repositories
* refactor(trip-members): membership, guests and the trip read model through repositories
* refactor(places): places, tags, ratings and place images through repositories
* fix(server): place-shadow source counts ordered in SQL; identity-map ratchets load-bearing; photo-cache race pinned
* refactor(places): import paths through repositories
* refactor(trips): trips through repositories
* fix(server): trip ids parsed once at the gate for places and assignments; NUL-safe value quoting on the SQLite platform; places parity tests completed
* refactor(trips): trip copy through repositories for the owned tables
* fix(server): plan 3c close-out — non-finite ids bound as NULL, trip id parsed once at every places/days/ws gate
Platform: NulSafeSqlitePlatform now renders a bound NaN as NULL and
+/-Infinity as 9e999/-9e999, matching better-sqlite3's own legacy binding
(rule 22 extension) — the program-wide fail-safe for any caller that still
passes an unguarded Number(untrusted) into a converted statement.
PlacesService.verifyTripAccess and RealtimeGateway.handleJoin stop doing
that: verifyTripAccess parses once with toRowId and threads the parsed id
through create/import/bulk/rating/export.gpx (deleting the seven
toRowId(tripId) ?? -1 sites), and handleJoin gets a Number.isFinite guard.
DaysService gets the same treatment across list/create/getDay/reorder/
insert/resyncAccommodationDays, closing the gap where a hex-spelled trip id
could renumber a real trip's days while the raw-bind inverted-accommodation
guard silently matched nothing.
DatabaseService.getTripTitle is deleted; PlacesService injects
TripsRepository directly and calls getTitle itself.
Template/cheap-item cleanup: DayAssignmentsRepository.listForTimeSort uses
$castTo instead of an unknown bridge; the duplicated TagRow interface in
Places.repository.ts is now imported from Tags.repository.ts;
AssignmentWithPlaceRow extends the guarded DayAssignmentRow instead of
restating its columns; PlaceShadowPicksRepository.totals drops its
unreachable ??-fallback branch; Places.repository.ts's updatePlace types
the NOT NULL name column as string; trip-members.service.ts's
transferOwnership threads one parsed trip id through all three writes and
corrects two stale "raw-bind seam" docstrings that were actually Number()
seams; a dead canAccessTrip mock is removed from reservations.e2e.test.ts.
Tests: the 23 new no-explicit-any warnings in TRIP-SVC-069 are typed
(lint back to 2416); HTTP and WS regression coverage for the non-finite
and non-canonical id shapes across places/days routes, mutation-proved;
TRIP-JOIN-005 asserts the exact concurrent-join settlement instead of
>= 1; a new tests/unit/db/transaction-yield.test.ts pins rule 24's
measured transaction-yield/dirty-read/nested-BEGIN behaviour.
* test(server): plan 3c close-out — copy-source full-row parity, Kysely rollback proofs, identity-map relabels, trips e2e on the snapshot DB
* fix(server): plan 3c close-out — days create/insert answer the guard 404 for non-canonical trip ids instead of 500; exact settlement and cross-request yield ratchets
* feat(server): plan 3d groundwork — dialect helpers, visibility predicate, feed and cron context ratchets
* refactor(roadtrip): roadtrip tables through repositories
* refactor(reservations): reservations, endpoints and travelers through repositories
* refactor(feeds): ICS feed tokens and anonymous feeds through repositories
* refactor(accommodations): stays through repositories inside the existing transactions
* test(server): plan 3d — MCP test helper constructors follow the accommodations and days services
* refactor(trips): trip copy through repositories for the roadtrip, accommodation and reservation tables
* refactor(calendar): calendar reads and reservation visibility through one predicate
* feat(server): plan 3e groundwork — packing visibility predicate, memories access-gate audit
* fix(server): plan 3d close-out — legacy text shape for stored accommodation ids restores the date-change restamp; reservation ids parsed once at the gate; parity tests for the reservation repositories
* refactor(photos): trek photos, memories access and cache sweeps through repositories
* refactor(files): trip files and file links through repositories, with transactions added
* test(server): plan 3e — hand-built FilesService, FilesRpc and AllowedFileTypesService calls follow the repository constructors
* refactor(memories): unified, immich and synology photo sync through repositories
* refactor(collab): notes, polls, messages and links through repositories
* refactor(budget): budget items, members, payers and settlements through repositories
* refactor(todo): todo items through repositories
* refactor(packing): packing items, bags and templates through repositories
* feat(server): plan 3f groundwork — systemNotices DI shape, TRAVELER_OWNS predicate, ntfy/storage-health specs, composite-PK pins
* refactor(school-holidays): country and region catalog through repositories
* refactor(notifications): in-app notifications and channel preferences through repositories
* refactor(notifications): reminder crons, mailer, ntfy and webhook transports through repositories
* fix(server): plan 3e close-out — repository registrations restore the boot; packing assignee and bag updates parse and no-op as legacy; coverage and scoping ratchets for the budget, packing and collab repositories
* refactor(vacay): vacation plans, entries, shares and holiday calendars through repositories
* refactor(system-notices): notice dismissals through a Nest provider and repository
* refactor(atlas): visited countries, regions and bucket list through repositories
* feat(server): plan 3g groundwork — GALLERY_CHRONOLOGICAL_ORDER Kysely ordering, unixEpochToIsoKysely helper, composite-PK DDL pins, JB5/R7 confirmations
* refactor(journey): journey and trip access control, CRUD and the trip-sync engine through repositories
* feat(server): plan 3h groundwork — date-offset dialect helpers, entity/repository fitness confirmations, pre-init and encryption pattern confirmations, survivor re-grep
* fix(server): plan 3f close-out — ntfy operator-scope test on the live send path; vacay date-offset and shift-order parity; todo reminder window; coverage and parity ratchets for the atlas and vacay repositories
* refactor(journey): journey stats, entries, photos, contributors and suggestions through repositories
* refactor(journey): public share links, TREK Studio books and the journal facades through repositories
* refactor(collections): access model, hydration, export/import and list CRUD through repositories
* refactor(server): journey-table survivors in trips, photos, memories, auth-cleanup and plugin-contributions through repositories
* refactor(integrations): dawarich connection, sync, tracks and review-flow through repositories
* refactor(collections): saved places, copy-to-trip, labels and invitations through repositories
* refactor(integrations,booking-import,airports,maps,route-usage): airtrail, booking import, airport backfill, maps cache and route-usage counters through repositories
* refactor(doc-sync): connections, reconciler and controller-SQL relocation through repositories
* test(integrations): add AirTrail saveSettings tx-asymmetry ratchet (Plan 3h Task 4 addendum)
* fix(server): plan 3g close-out — transactional book first-save, skeleton reconcile and share-link create; unknown-key entry patches are no-ops again; parity and authorization pins for the journey repositories
* feat(server): plan 3i groundwork — AuditLogRepository's reserved methods, the demo connection-swap spike, repository-fitness confirmations
* feat(server): plan 3j groundwork — scheduler onto CronRegistrarService, SDK-contract confirmation, repository fitness
* refactor(storage): backend/category config and usage stats through AppSettingsRepository; fix the seed-file test race and the shared-replica migration-flip gap
* refactor(share): public share links, the pre-init photo fallback, and collection-places survivors through repositories
* feat(server): plan 3j — the withCamelCase utility, unwired, pending a contract decision
* refactor(admin): user CRUD, stats, audit log, version check and addon config through repositories
* test(admin): pin createUser's pre-existing non-transactional uniqueness-check race (AD2-4)
* refactor(plugins): the trip-write role gate through a repository, in isolation
* refactor(demo,backup): seed and reset through DemoRepository, live-connection backup statements through MaintenanceRepository, isDemoUserId through an injected DemoService
* fix(server): plan 3h close-out — new-value-wins COALESCE restores stay/reservation propagation; doc-sync state filter truthiness; request-context and parity ratchets for share, doc-sync and the integrations
* refactor(plugins): runtime lifecycle, config CRUD and the uninstall cascade through repositories
* fix(server): plan 3i close-out — DemoModule imported where its service is consumed (e2e restored); admin addons list selects the provider relation; saveDemoBaseline awaited; backup tests on the maintenance repository
* refactor(plugins): outbound OAuth tokens and per-user plugin settings through repositories
* refactor(plugins): install-time gates, the capability-audit hash chain and the registry through repositories
* refactor(plugins): contribution-hook controllers, the small plugin controllers and the plugins-table survivors through repositories
* test(server): plan 4 — collapse mcp tool test harnesses onto the ORM snapshot
* test(server): plan 4 — collapse nest service test harnesses onto the ORM snapshot
* fix(server): plan 3j close-out — serialized audit-chain appends, atomic metadata and scheduler limits, rollback and race proofs for the plugin repositories
* refactor(server): plan 4 — convert the program's orphaned raw-SQL callers
* fix(server): plan 4 — un-skip PARITY-009 (FK deleteRule and index-name parity)
* refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (trip-domain services)
* refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (plugins + MCP surfaces)
* fix(server): plan 4 — RULE12 paren-count scanner ignores string literals
The entity generator's RULE12_fixGarbledCheckExpressions CHECK-expression
paren-balance scanner counted every `(`/`)` character regardless of
whether it fell inside a single-quoted SQL string literal. A CHECK
expression like `status IN ('confirmed)', 'tentative')` has a literal `)`
inside its first string value; the scanner would hit depth 0 at that
in-string character and truncate the expression, losing its own real
closing paren. The scanner now tracks single-quote string state (handling
the doubled `''` SQLite escape) and skips parens while inside one.
Latent-bug fix — no shipped entity's CHECK expression has hit this shape
yet (3d's ledger: "nothing affected today").
* refactor(server): plan 4 — unify the trip-access predicate's Kysely copies
TripsRepository.findForViewer/listForUser/activeTrip each hand-wrote the
identical trip-access boolean (t.user_id = ? OR m.user_id IS NOT NULL)
through the Kysely expression builder. Task 7's security review (M1)
evaluated a shared helper and set it aside for time, falling back to the
TRIPREPO-041 cross-method parity test as a regression guard instead.
Extracted the boolean into one generic helper, tripAccessExpr
(_shared/trip-access.ts), following the reservation-visibility.ts
publicReservationExpr precedent: fully generic over <DB, TB> since the
predicate has no correlated subquery, so it composes across
TripSelectKyselyDB and ActiveTripKyselyDB without needing a narrower
fixed-alias shape. The LEFT JOIN itself stays hand-written at each call
site — tripSelectQuery's own docstring already explains why a shared join
helper can't be typed across these differently-joined outer queries.
feeds.service.ts and memories-access.service.ts no longer duplicate this
predicate at all (Task 2's facade-inline sweep already converted both onto
TripsRepository.findAccessible/getFeedTokenIfReachable/
listReachableActiveTrips, which read through the canonical MikroORM
QueryBuilder form, accessibleTripsQuery) — the remaining duplication this
task closes is the three Kysely copies inside this file.
TRIPREPO-041 (64/64 in the file) stays green as the unification's own
regression guard.
* fix(server): plan 4 — trek_photo_cache_meta.cache_key NOT NULL
The column was a bare TEXT PRIMARY KEY, which SQLite does not implicitly
make NOT NULL (unlike an INTEGER PRIMARY KEY rowid alias). Every writer
(TrekPhotoCacheMetaRepository.upsertMeta, the table's only INSERT path)
has always bound a real string — 3e's R5 confirmed this. Appends a new
migration rebuilding the table with cache_key NOT NULL (SQLite has no
ALTER COLUMN, so this follows the same rebuild recipe
Migration20200101020000 uses in the opposite direction) and updates the
entity/generated metadata to match. Schema tightening, not a behaviour
change.
* refactor(server): plan 4 — retire the safely-convertible string-form where conditions (rule 23)
Places.repository.ts's existsInTrip/findInTrip/findRaw/reclaimInputs/
listByTripAndIds and Trips.repository.ts's findPublicForShare all took a
raw `.where('col = ?', [v])` string condition even though their id/trip_id
parameters were already plain `number` (or, for findPublicForShare,
narrowed to `number` here after confirming its one caller,
share.service.ts#getSharedTripData, only ever passes a number sourced
from a Kysely-typed share_tokens row, never an unconverted URL param) —
converted all six to MikroORM's typed `.where({...})` filter object.
The remaining raw string-form conditions in src/db/repositories/** are
NOT converted here: they are the `number | string` raw-bind seam
findAccessible/isOwner's own docstrings describe (trip_id/id threaded
through unconverted from req.params), which structurally requires the
deferred guard-level single toRowId parse to land first — narrowing any
of those methods' signatures now would either break the '0x10'/'007' id
parity those docstrings warn about, or duplicate the deferred item's own
work ahead of it landing.
* refactor(server): plan 4 — trek-photos.repository.ts finishes its rename
3e Task 6 renamed the CLASS (TrekPhotosRepository -> TrekPhotoRegistrationService,
since the name collided with the generated ORM TrekPhotos.repository.ts) but
left the FILE and its test file's names unchanged. Renames both to
trek-photo-registration.service.ts / .test.ts.
The old path is kept as a thin re-export stub (the src/websocket.ts shape)
rather than repointed at every one of its ~20 importers in this pass: several
of those files sit in two sibling tasks' exclusively-owned windows on this
shared branch (Task 3's budget/packing/todo/accommodations/places ripple,
reached through tests/helpers/plugin-host.ts and mcp-test-controllers.ts;
Task 5c's held-back tests/unit/services/{trekPhotoMedia,conflictUpdate}.test.ts)
and are off limits here. Flagged for a follow-up once those tasks land: delete
the stub and repoint every remaining importer at ./trek-photo-registration.service.
* fix(server): plan 4 — plugin discovery's delete+reinsert pairs are atomic
install/discovery.ts#upsert refreshed a plugin's actions and settings-field
descriptors as two independent delete-then-reinsert pairs (DI5-DI8) with no
transaction spanning either pair, let alone both: a crash between a delete
and its own reinsert left that plugin with an EMPTY actions or
settings-field set until the next discovery run, instead of its previous
still-valid rows. Wraps both pairs in one uow.transactional, so a discovery
refresh for a plugin's descriptors is now all-or-nothing.
Deliberate behaviour change, named for the user per the task brief.
DiscoveryRepos grows a UnitOfWork; PluginRegistryService (the other
production caller, registry.service.ts) picks it up the same @Optional()
way PluginRuntimeService already does, with the same guard-throw. Updates
the two hand-constructed test call sites that actually exercise discovery
(discovery.test.ts, registry.test.ts) to pass a real UnitOfWork.
* refactor(server): plan 4 — facade inline sweep part B (rosterUserIds/getPlaceWithTags)
Inline DatabaseService.rosterUserIds onto TripMembersRepository.rosterUserIds
directly in budget.service.ts, budget.mcp.ts and todo.service.ts, and
DatabaseService.getPlaceWithTags onto PlacesRepository.findWithTagsAndRatings
in accommodations.service.ts (placesRepo was already injected there).
BudgetService/BudgetMcp/TodoService collapse their constructors, dropping
the now-fully-unused DatabaseService param (budget.module.ts/todo.module.ts
register TripMembers on forFeature). PackingService keeps DatabaseService as
a vestigial param behind an optional TripMembersRepository with a fallback
in tripRosterIds: an in-flight Task 5c file
(tests/unit/services/conflictUpdate.test.ts) hand-constructs PackingService
positionally without the new repository, so its arity cannot change this
task; the fallback keeps that file's DatabaseService.rosterUserIds spy
working until Task 4 drops the param with the class itself.
* test(server): plan 4 — constructor ripple for the rosterUserIds/getPlaceWithTags inline
Hand-built BudgetService/BudgetMcp/TodoService call sites drop the removed
DatabaseService constructor arg (budgetRepoArgs() now appends a
TripMembersRepository so most spread call sites need no other change);
budget.service.test.ts's rosterHas() helper and svc() factory move from a
spied DatabaseService to a stubbed TripMembersRepository. The
AccommodationsService helper (tests/helpers/accommodations-service.ts) and
its two direct callers drop the same arg. PackingService call sites are
untouched (its new param is optional, so every existing hand-built instance
still resolves rosterUserIds through the DatabaseService fallback).
* fix(server): plan 4 — thread the discovery UnitOfWork through PluginRuntimeService too
Complements the just-landed discovery atomicity fix (DiscoveryRepos.uow):
PluginRuntimeService's own discoveryRepos getter still built the bundle
without a uow field. Threads its already-injected, @Optional() uow through
so its own onApplicationBootstrap/reload/dev-link discovery calls also get
the transactional DI5-DI8 refresh, not just PluginRegistryService's.
No throwing guard here, unlike PluginRegistryService's: several hand-built
partial-DI-graph test instances of this class (boot-registry-order.test.ts,
in a sibling task's held file window) construct it with uow omitted and
exercise onApplicationBootstrap's discovery call directly. Discovery must
never refuse outright just because a test built a partial graph, so it
degrades to the pre-existing untransacted sequence when uow is absent
(DiscoveryRepos.uow's own docstring covers this), the same "boot must
never block app init" defensiveness this file already applies elsewhere.
* fix(server): plan 4 — guard-level single parse: verify + correct the overclaiming verifyTripAccess docstring (L-2)
TripAccessGuard/TripOwnerGuard already parse :tripId exactly once
(Number(), guarded by Number.isFinite) and hand the resolved TripAccess
row to the handler via request[TRIP_REQUEST_KEY], TripOwnerGuard reusing
TripAccessGuard's own lookup when both are on the same route rather than
re-querying — verified by reading both guards directly, no change needed
there.
PlacesService.verifyTripAccess's own docstring separately claimed its
returned `tid` gets "returned... for every downstream call to reuse
(never re-parsing)" (3c's Task 9 review, L-2, carried to Plan 4). Reading
every call site (not assumed) shows this is false: `.tid` has exactly one
reader (PlacesController.requireTrip itself), which discards it — none of
requireTrip's 13 callers read `.tid` off the trip it returns; each passes
the original raw tripId string on to the write/read method it calls next,
which toRowId-parses it again itself. That second parse cannot be
eliminated by this gate alone: the same service methods are called
directly by places.mcp.ts too, an entry point with no requireTrip gate at
all (its own numeric tripId is re-stringified, String(tripId), to call
them), so each method owns its own id validation regardless of entry
point. Threading `tid` all the way through would mean giving every one of
~15 methods a second, number-only call shape for the REST path alone to
use — a real refactor, out of this docstring fix's own scope. Corrected
the docstring to say so plainly instead of repeating the false claim.
* refactor(server): plan 4 — relocate JS7/10/13/14 off the journey-share fallback stub
JourneyShareTokensRepository's own JS7/JS10/JS13/JS14 Kysely reads were a
3g Task 3 fallback (Task 2's JourneyPhotosRepository/JourneyEntriesRepository/
JourneyEntryPhotosRepository were still mid-flight). Now that those
repositories are stable, relocate: JS7/JS10 (photo/asset validation joins)
onto JourneyPhotosRepository (already injected into JourneyShareService as
photosRepo, no constructor change); JS13 (public entry list) onto
JourneyEntriesRepository.listPublicEntries; JS14 (public per-entry photo
read) onto JourneyEntryPhotosRepository.listForPublicJourney, deduped
against that repository's own JP_COLUMNS select list instead of a
hand-duplicated copy. JourneyShareService gains two new @InjectRepository
params (both already registered in journey-domain.module.ts's forFeature),
rippled through mcp-test-controllers.ts and journey-share.service.test.ts
(P04/P05 parity tests now pin the new homes).
* refactor(server): plan 4 — document unreachable ?? default arms after single-row aggregates
3f Task 7's fix wave flagged a cluster of `row?.x ?? default` arms after
unqualified, ungrouped aggregate reads (MAX/COUNT/COALESCE(SUM,0)) as
"same class as OauthClients:147" but left them unexamined per-site. Decided
per site: document, don't delete — TypeScript still types
executeTakeFirst()'s result as `| undefined` even though SQL guarantees
exactly one row for these unqualified aggregates, so the optional chain
can't be removed without a non-null assertion (worse, not safer). Six
sites: JourneyEntries.repository.ts (maxSortOrderForDate JG42,
countStatsPlaces JG67), JourneyEntryPhotos.repository.ts
(maxSortOrderForEntry JG91), JourneyPhotos.repository.ts (maxSortOrder
JG88/JG99), VacayEntries.repository.ts (sumFraction VC1, sumCompFraction
VC2). No behavior change — comments only.
* refactor(server): plan 4 — unify the two user-data-erasure enqueue paths
UserCleanupService.erasePluginUserData (the account-deletion transaction's
own UC2/UC3 enqueue) and PluginRuntimeService.enqueueUserErasure (the
emitUserDeleted sink's post-commit enqueue) independently walked the
identical SELECT id, permissions FROM plugins + JSON.parse + hook:user-data
filter + INSERT OR IGNORE — and both genuinely run for every account
deletion, since every emitUserDeleted call site (auth.service.ts,
admin.service.ts, trip-members.service.ts's guest removal) fires
immediately after deleteUserCompletely.
Extracted the shared filter into enqueueHookUserDataErasures
(nest/plugins/user-erasure-enqueue.ts) and pointed both call sites at it.
UC2/UC3 also come off DatabaseService onto PluginsRepository/
PluginUserErasureQueueRepository in the same change (they were the last
raw statements standing in the way of sharing one implementation) — this
narrows the Plan 3b Task 5 "stays raw, owned by Plan 3j" ruling to UC1
only (plugin_user_config/plugin_oauth_tokens/plugin_oauth_state), which
still stays raw, unaffected. UserCleanupService's orphan-data-directory
scan (a plugin uninstalled with data retained) has no equivalent on the
enqueueUserErasure side and stays there, now via the same repository.
Both enqueue calls still run per deletion (INSERT OR IGNORE keeps this
idempotent) — this unifies the IMPLEMENTATION, not the call count; not
touching the emitUserDeleted event-sink wiring itself was a deliberate,
smaller-blast-radius choice for this task.
* refactor(server): plan 4 — narrow TripMembersRepository.remove/TripsRepository.setOwner to number
Both accepted trip_id: number | string per 3c's carry note (their "stays
loose for other callers" reasoning), pending the guard-level single parse
landing first. Re-checked every caller directly: setOwner's one production
caller (trip-members.service.ts#transferOwnership) already passes trip.id,
a real number resolved earlier in the same transaction; remove's callers
are trips.rpc.ts (num(), already a validated number), trips.mcp.ts (Zod-
typed tool input, already a number) and TripMembersController.removeMember
(the one genuine raw-string caller, :id off the route with no class-level
TripAccessGuard on this controller by design).
TripMembersController.removeMember now parses :id once (toRowId, not
Number() — rule 15's NaN-into-SQL trap) and threads the parsed number to
both its own access check and removeMember, instead of passing the raw
string through twice. TripMembersService.removeMember and both repository
methods narrow to number accordingly; setOwner's WHERE also moves off its
raw-bind escape hatch onto nativeUpdate now that trip_id is typed, and
remove's trip_id half moves onto a typed .where({ trip: trip_id }) filter
(user_id keeps its own raw bind — a different, still-live NaN-safety seam,
unrelated to this narrowing).
TRIPREPO-018 and TMEMREPO-023, which pinned the retired string-bind seam
on setOwner/remove, no longer typecheck against the narrowed signatures
and are removed with an explanatory comment in their place.
* refactor(server): plan 4 — U6 gate-level id parsing for todo item ids
Rule 21's carry from 3e Task 8's review: TodoController.update/.remove
passed the raw :id route string all the way to TodoItemsRepository,
relying on SQLite's column-affinity CAST to match it instead of a typed
filter. Parse :id ONCE at the controller (toRowId, matching Task 8a's
TripAccessGuard/removeMember shape), 404 on a non-canonical id, and thread
the number down through TodoService.updateItem/deleteItem into
TodoItemsRepository.findInTrip/existsInTrip/update/deleteById, all
narrowed from number | string to number. todo.rpc.ts's num()-derived
todoId no longer needs its String() wrapper. tripId stays string | number
— a separate, already-accepted carry (
|
||
|
|
94914295d1 |
fix(llm): let a model refuse temperature, and say which extractor is installed
Two reports, both about an import that fails without saying why. #2262 — temperature: 0 sat in the shared request body, so it went out on every attempt including both retries. Reasoning models (the gpt-5 family) reject any explicit temperature, and the json_object retry fires on any 400, so the import spent two doomed requests and could not succeed at all. The parameter is now dropped once a server has explicitly said it will not take it. Not as a third link in the chain, though. A gpt-5 rejects max_tokens AND temperature, the API names one parameter per response, and it may name either first — a chain of one-shot ifs converges on only one of the two orders. The retries are a loop over what the server actually said instead: each remedy applies at most once and they compose. Detection is a phrase, not the bare word, because dropping temperature costs the deterministic sampling that small local models depend on, and an unfamiliar wording still falls through to the json_object retry exactly as before. NuExtract and Ollama are untouched: the first attempt always carries temperature, and it is the only one they ever see. #2261 — the bundled extractor is Debian trixie's 24.12.3, roughly eighteen months behind upstream, and the reporter is right that this is invisible: a stale extractor, a mistyped KITINERARY_EXTRACTOR_PATH and a genuinely unsupported provider all look the same from the UI, which is nothing imported and no reason given. The version now appears in the startup log and in a new admin-only GET /api/admin/system-info, and LOG_LEVEL=debug passes the extractor's raw stderr through — the JS ERROR and Invalid-result-type lines the filter drops are the only evidence that a vendor script is missing or failing. Capped at 200 lines, and opt-in because those lines can carry document fragments. Not bumping the package: the filter the reporter needs first ships in KDE Gear 26.08 and no Debian suite carries it, and sid's 26.04.3 wants KF6 6.22 and Qt6 6.10.2 against trixie's 6.6 and 6.8.2 — a partial upgrade of Qt6, KF6 and libc6 from unstable into the production image, on two architectures, that still would not fix the reported carrier. It comes for free with the base-image move after Debian 14. |
||
|
|
3a0604b3f5 |
feat(config): put the operator-owned surfaces behind TREK_MANAGED (#1946)
* feat(config): put the operator-owned surfaces behind TREK_MANAGED The switch landed last release with nothing behind it. This is what it turns off, plus the four fixes that came out of walking every one of these surfaces. Nothing here changes anything for an install that does not set the flag. That is not a hope, it is what managed-inert.test.ts asserts: the mode stays off for any unparseable value, no other variable can turn it on, every settings key stays writable, and splitManagedKeys hands back the same object rather than a copy so no write path can quietly diverge on self-hosted installs only. WHAT THE MODE WITHHOLDS Fourteen routes carry @ManagedForbidden: key validation, static MCP tokens, JWT rotation, the OIDC admin pair, restore and auto-backup, plugin sideload and reload, the Ollama model list and pull, and the SMTP test send. Each names the reason it is not the instance admin's, and the reason is mandatory. Eighteen settings keys are filtered instead of refused. SMTP, the OIDC toggles and the WebAuthn pair share one handler with keys the admin does own, and the llm_* family is reachable through per-user settings, the instance defaults and the addon config alike, so a route-level refusal would be both too coarse and incomplete. A locked key is skipped and reported as managed_keys; the rest of the save goes through. Refusing the request would take the registration toggles down with the SMTP block. GET /api/auth/me/settings stops returning the three API keys in the clear. It is the only endpoint that decrypts a stored credential back to the caller, which is fine when the admin pasted it in and wrong when the operator supplied it. Two multipart routes check in the handler rather than the guard: a guard throws before the parser drains the body and the client gets an ECONNRESET instead of the 403 (PROFILE-015). They still carry the marker so the boot gate sees them. WHAT KEEPS IT FROM DRIFTING validate-managed-routes.ts refuses to boot on a marking that is not on the reviewed list, a list entry whose route lost its marking, or a marking with no reason. The third case has no equivalent in the guard gate next door and is the one that matters most: a blank reason destroys the only record of why a control was taken away, and nothing recovers that later. It found two wrong entries in my own list while I was writing this. managed-keys.test.ts asserts that every key in ADMIN_SETTINGS_KEYS and DEFAULTABLE_USER_SETTING_KEYS belongs to exactly one of two sets. A key added to either list belongs to neither until somebody decides, and that is the failure this prevents: the lists grow, the mode does not. PLACES_API_BASE AND PLACES_API_KEY The nine Places endpoints spell out googleapis.com, so an install that wants those calls to leave through something of its own has no way to say so. One variable, substituted where every call already funnels through. A second one supplies the key, and when it is set the users table is not consulted at all — a per-user key would route around whatever the endpoint counts, and those columns are readable by any admin of the instance. FOUR FIXES THAT ARE NOT ABOUT THE MODE An admin setting somebody's password now bumps password_version and clears their MCP tokens. An admin sets a password for one reason, and until now verifyJwtAndLoadUser kept accepting every cookie the intruder already had: the one action taken to lock them out was the one action that did not. The four OIDC provider calls go through the SSRF guard. Three are worse than they look — the token, userinfo and JWKS URLs come out of the discovery response, so they are chosen by the other end, not by an admin. safeFetchLlm is renamed safeFetchAdminConfigured and keeps its old name as an alias, because it is the variant that allows loopback and LAN: a self-hoster with Authentik on localhost:9000 must keep working. The token exchange follows no redirect at all, since one would hand client_secret to a second host. DELETE /api/admin/users/:id/mfa clears another account's TOTP. The passkey half has existed since passkeys landed; the TOTP half never did, which left "somebody on the trip lost their phone" with no answer short of an operator in the database. Never for the caller themselves. TREK_PLUGIN_PERMISSIONS=off refuses to boot when the install is centrally administered. A warning is right for a machine its owner chose to trust; where the person who set the flag and the people whose data is in the database are not the same person, fail closed. TEXTS Four strings lose the phrase "self-hosted" because they describe somebody else's software — an Ollama endpoint, an AirTrail instance, a service a plugin talks to — and read better without it in both modes. The one string that describes TREK itself keeps it, and the mode picks a second key instead, so self-hosters lose nothing. A grep gate in shared/ keeps the canonical locale honest, because the parity check compares the file set and the top-level keys and never looks at a value: re-wording a string is invisible to CI today, and so is putting it back. * feat(maps): supply the map credential with the instance when centrally administered MAPBOX_ACCESS_TOKEN joins PLACES_API_KEY as an operator-supplied value. SettingsService injects it when the settings are read, and picks GL as the provider if the reader is still on Leaflet, since a token is always present. The key moves from the customer set to the locked set. It is the one locked key whose value is public by design — a pk.* token has to reach the browser to be useful — so this is not about secrecy: a per-user save landing on top of the instance token would only break the map for that user. Both settings fields for it go away with it. * fix(auth): count the operator key when reporting whether Google features work has_maps_key only looked at the users table, but getMapsKey prefers the PLACES_API_KEY env over anything stored there. On an install that supplies the key that way and has an empty column, every lookup worked while the client was told there was no key and hid the search, the autocomplete and the place photos. Found by walking the running instance rather than the code: the map came up, the search box did not. * feat(admin): hide the settings a centrally administered install cannot act on A control that answers 403 is worse than no control: the AI section rendered the refusal as "Request failed with status code 403", which tells an admin they broke something rather than that the decision is not theirs. So the surfaces go rather than being disabled in place: - AI parsing and AirTrail leave the addon list, and the AI addon route now refuses outright. The endpoint, the model and what a document costs come with the instance, so there is no instance decision left — not even whether it runs. - Immich and Synology Photos leave it too. Both connect to a server the reader runs at home, and there is no route from here to one. - The SMTP block, the admin webhook and ntfy targets, and the passkey RP ID and origins follow the same rule: they configure something the operator owns, and a wrong RP ID on a shared parent domain reaches past this instance entirely. - The About tab goes. It is about the project — what TREK is, where to file a bug, where to support it — and is aimed at the person running the software. The version and the link to the source move to the sidebar footer, on both the desktop page and the mobile twin, because AGPL 13 wants that offer wherever people reach the software over a network. - The update check stops asking GitHub and reports nothing available. Separately, the version footers lose the "self-hosted" suffix everywhere, not just here. It read as a label on the software rather than on the install, and the version alone says what it needs to. * feat(config): refuse to boot centrally administered without an at-rest key Measured on a running instance rather than assumed: with ENCRYPTION_KEY unset, the at-rest key falls back to a file in the data volume, and backupService bundles that file into every archive it builds. The archive is downloadable by any instance admin, so the encryption ships with its own key in the same zip. The skip in backupService already handles this — it only omits the file when the key came from the env — but nothing made sure the env actually carried one. Provisioning sets it; forgetting it was silent. validateEnvAtBoot now carries a second kind of check next to the schema: cross- field rules that only apply with the flag set, for combinations where every value is individually valid and the result is wrong. Empty without the flag, so an install that never asked to be centrally administered cannot meet it. * feat(client): add an attachment point for install-specific screens Two empty exports on the client, an empty module on the server, and the four places that read them. Nothing here adds a route, a nav entry or an endpoint — the arrays are empty and the module contributes nothing, which managed-ext.test pins so it stays that way. Read unconditionally, never behind a flag. On the server a conditional import would tie the module graph to an environment variable, and the graph is built once per app while the flag is read live; on the client an empty array costs nothing to spread, so every build walks the same path instead of one build walking a path nobody tests. Anything attached here follows the same rules as the rest of src/nest and src/components: a real module with a controller and a provider, bodies through the Zod pipe, routes under the global guards, tests. * test(shared): move the wording gate out of src so shared typechecks again CI installs shared with its own dependencies only (npm ci --workspace shared), so there is no @types/node and the `node:fs` import inside src/ failed to compile. The tests passed the whole time — vitest supplies the module — which is why this only showed up in the typecheck step. The walk moves to scripts/, next to i18n-parity.mjs, and the spec imports it the same way that one does. It also runs on its own now (`node scripts/i18n-selfhost-wording.mjs`), which is what you want when CI names a line you have to go find. * test(server): repair the call sites typecheck:tests caught tests/ sits outside the build tsconfig, so none of this showed up in a green test run — which is exactly the gap `npm run typecheck:tests` exists to close. Three kinds, all mine: - Seven `import type` paths for RuntimeEnvService carried one `../` too many. Type-only imports are erased, so vitest never resolved them and nothing failed. - VersionCheckJob and PluginsController grew a constructor parameter and eleven hand-wired `new` calls kept passing the old count. The argument arrived as undefined and no assertion happened to read it. - ConditionContext grew `managed`, and the shared context object in the conditions suite did not. The condition also had no test of its own, so it has one now: both directions, since the registry uses `is: false` to keep a notice away and a notice that only belongs on a centrally administered install would use `is: true`. |
||
|
|
78b63972d0 | refactor(server): move the version check and demo reset into the admin domain | ||
|
|
9895c7112a |
Give the admin OAuth-session panel back to the oauth domain
The last of AdminService's borrowed SQL. oauth_tokens belongs to oauth/, which already had a user-facing view of the same table; AdminService carried a second one only because the panel route is under /api/admin. Both survive, under names that say which is which. They are not duplicates: the user-facing listOAuthSessions is scoped to one user, the admin one spans all of them and joins the owner username, and the admin revoke kills the client session for whoever owned the token. OauthModule gains its first export. Its header said nothing inside the container injects the domain, which stops being true here. |
||
|
|
03fbced20b |
Move tokens out of AuthService into a domain of their own
AuthService carried identity, profile, settings and tokens at once. Tokens are the cleanest of those to cut loose: they touch one table plus the ephemeral-token store, and nothing in them needs to know how a password is hashed or how a session is established. TokenService takes the six user-facing methods plus verifyMcpToken, and the two admin-side ones that only lived on AdminService because the admin route did. The pairs are genuinely different queries rather than duplicates, so both survive under names that say which is which: the user-facing ones scope every statement by user_id and treat the session sweep as best-effort, the admin ones do neither. Left behind on purpose: verifyJwtToken, which is login identity and belongs next to the cookie logic, and isDemoUser, which is a demo gate that happens to read the users table. TokensModule is a leaf with nothing but DatabaseService, because auth, admin and the MCP transport all import it and anything added here lands in three graphs at once. Both bridges and both controller test harnesses construct these classes positionally and had to be pulled along; the new argument goes in an optional slot so untouched call sites stay untouched. |
||
|
|
6680bc7c74 |
Split PluginsModule into four, and gate coverage per domain
PluginsModule was 21 controllers, 24 domain imports and 10 providers in one class, which made it the hub most of the Nest graph ran through: AdminModule imported it for a single cascade-disable call and inherited all of it. It is now PluginsRuntimeModule (supervisor, capability router, hook contracts), PluginContributionsModule (the 14 read-only hook controllers, moved to contributions/), PluginOAuthModule (the credential leaf, moved to oauth/) and a composition root that keeps the install and delivery controllers. AdminModule imports the runtime alone. The three plugin files that reached for the db singleton alongside the injected DatabaseService no longer do. plugin-host-state and signature-status take the connection from the caller, and the three per-user settings functions that were bare exports 'so the RPC host wiring can call them directly' are now PluginUserSettingsService - the host wiring is providers, so it injects them. Coverage moves from one repo-wide 80% to a per-domain ratchet. The single number let a well-covered domain subsidise a thin one: booking-import sits at 50% and integrations at 20%, and the aggregate still cleared the bar, so either could have lost another ten points unnoticed. Each domain now carries its own measured floor, regenerated by scripts/coverage-thresholds.mjs. Two test-side findings on the way: expect(undefined).toContain(SomeClass) PASSES in this vitest version - it only throws for a string needle. Every '<X> is listed in its module providers' guard was written that way, so all twelve of them silently stopped guarding the moment their class moved. They go through expectRegisteredProvider now, which asserts the array shape first. The atlas region test paid for the one-off decompression of the multi-MB admin1 bundle inside its own 5s budget, which is why it failed under load and passed alone. The bundle is warmed in beforeAll instead. |
||
|
|
2ac6c76086 |
chore: move admin backend to proper DI
The 851-line services/adminService folds into the wrapper AdminService over DatabaseService plus injected Settings/Addons/Passkey/Packing/Auth/Permissions/ Notifications services; the auth, notifications and permissions bridge imports all become injections, PERMISSION_ACTIONS stays a plain const, and the mcp/sessionManager deep import keeps its anti-cycle comment. Ahead of the fold the 11 packing-template functions move to PackingService, which already owned all three template tables — resolving the admin-2 residual with no bridge. The pure and module-scoped half moves to admin.helpers.ts, the version cache staying module-scoped so admin.bridge's instance and the container singleton share one GitHub fetch. A 1-export admin.bridge.ts serves scheduler.ts's cron, the only out-of-container consumer. Recipe steps 3 and 4 are no-ops: no src/mcp/tools/ admin.ts has ever existed and the plugin host never imported the domain. Four lines are non-verbatim, all path re-anchoring one directory deeper, both resolved paths verified against the emitted dist layout. admin.dto.ts clears all twenty AdminController allow-list entries, retiring the three 'enabled must be a boolean' checks plus 'permissions object required' and 'Object body required' for the pipe envelope. The module e2e goes DI-native: the 3-method whole-module mock dies, 6 cases become 15 over real SQL. Quirk fixes on top, each pinned: the three places toggles go fail-closed with an append-only backfill migration; the template item routes honour :templateId; updateUser rejects an empty username/email; createInvite 404s an unresolvable trip_id; listOAuthSessions guards its scopes parse; GitHub fetches gain a timeout and size cap and failures cache briefly; updateOidcSettings/updateAddon/updateUser become transactional; admin.oauth_session.revoke is renamed to match its siblings; two mutations gain their missing audit rows; getAuditLog drops the _parse_error sentinel. Tests move with IDs preserved (ADMIN-SVC-001..069 incl. the pre-existing 029/030 gap and duplicated 069, VNOTIF-001..007), plus ADMIN-BR-001 and ADMIN-SVC-070..076. |
||
|
|
d203d8cccd |
chore: move notifications backend to proper DI
The send() dispatcher (services/notificationService.ts) and the in-app store (services/inAppNotifications.ts) fold together into the DI-native NotificationsService over DatabaseService + RealtimeService; the prefs matrix, transports, channel registry and inAppNotificationActions stay plain infra modules. A 1-export notifications.bridge covers scheduler, legacy adminService/memories and the six lazy fire-and-forget sends; AdminController and the plugin RPC host (24th factory dep) inject. The 5-tool registrar + notifications-in-app resource move to notifications.mcp.ts. Tests move with case IDs preserved (NSVC-*, INOTIF-*, + NSVC-020 bridge pin); ~18 suites repoint path mocks/warm-ups; the module e2e goes DI-native. Also carries the verified-quirk fixes that would normally trail as a separate fix(server) commit (file-overlap with the fold): toUtcIso on all three created_at paths (INOTIF-013/014), respond claims the response CAS before running the action handler with release-on-failure (INOTIF-011/012), synology's fire-and-forget send gains .catch, the dispatch-failure log unwraps Error messages (NSVC-021), and the MCP resource description drops its false 'unread first' claim. |
||
|
|
e65f851446 | chore: move passkey backend to proper DI | ||
|
|
ee2ea1d065 | refactor(server): extract addon enablement into the addons domain | ||
|
|
6ff936c613 | chore: move auditLog backend to proper DI | ||
|
|
cece6509b9 |
chore: move settings backend to proper DI
Folds the legacy services/settingsService SQL into the Nest SettingsService (byte-identical statements; BEGIN/COMMIT blocks become db.transaction()). AdminService and ShareService inject it; llm-config.resolver becomes the injectable LlmConfigResolver (SettingsService + DatabaseService), injected by LlmParseService and PluginHostDepsFactory. No MCP surface, no bridge. Whole-file commit, so this also carries the post-migration quirk fixes that live in the same files: null serializes as '' (a stored "null" leaked out of getDecryptedUserSetting as the literal string), bulk upserts skip the masked sentinel (mirroring the single-upsert no-op) and return the written count. Unit suite moved to tests/unit/nest with SET-SVC-001..026 preserved, 027..030 pin the fixes; settings/admin e2e converted to DI-native. |
||
|
|
91025683bb |
3.2.1 (#1433)
* fix(plugins): prevent arbitrary api access * fix(planner): disable drag & drop on mobile so the places list scrolls (#1432) On touch devices the draggable rows hijack the scroll gesture, so dragging to scroll started an HTML5 drag and popped up the file-import overlay instead of scrolling. Gate the draggable rows and the sidebar file-drop handlers on !isMobile across the places sidebar and the day plan (places, transports, notes), and hide the grip handle — the arrow reorder buttons take over there. * fix(inspector): make the remove-from-day button icon-only on mobile * fix(collections): show the save picker above the mobile place detail * fix(plugins): seal IPC parent/child for good * test(inspector): match the icon-only remove-from-day button * feat(sdk): switch plain ts for clack/prompts interactive session * feat(plugins): force-refresh the registry from the rescan button The registry is cached for 30 min server-side and GitHub serves it with a 5-min CDN cache, so a freshly published plugin could take up to ~35 min to appear. The rescan/reload button now force-pulls the registry: it bypasses the in-memory cache and appends a cache-buster + no-cache headers to beat the CDN, and refreshes the browse grid immediately. * feat(sdk): bump plugin version * fix(stored settings): prevent local storage drop when update not successful * feat(plugins): sideload plugins by uploading a .zip Adds an admin "Upload plugin" button + drag-and-drop to the plugins panel for installing a plugin archive directly — handy for testing a build before it goes to the registry. It reuses the registry install pipeline (slip/bomb-safe extract, strict manifest validation, native-binary scan) via a new POST /admin/plugins/upload, and only skips the registry sha256/signature checks that a sideload can't have. Sideloaded plugins are flagged (source "local:upload", a "Sideloaded" badge, no GitHub link, no auto-update) and always land INACTIVE — replacing a running or active plugin stops it and clears the active flag first, so new code never runs without a fresh activation + permission consent. * fix(planner): keep the day-plan collapse state after fully closing the page The expanded/collapsed days were stored in sessionStorage, which survives a reload but is wiped when the tab or window is closed — so every fresh open re-expanded all days, which is tedious to re-collapse on long (10+ day) trips. Store it in localStorage instead so a collapsed layout sticks until it's changed. * fix(i18n): correct Vietnamese translation of 'Disabled' (#1438) 'Tàn tật' means physically handicapped/disabled-person, not the off/disabled state of a toggle. Replace with 'Tắt' (off), matching the existing 'admin.plugins.stateOff' translation. Affects admin.notifications.none and admin.addons.disabled. * add the code of conduct * fix(plugins): let widgets follow the in-app dark-mode toggle The plugin frame is sandboxed at an opaque origin (no parent DOM access) and we only sent the context — including the theme — once, on trek:ready. So toggling dark mode in TREK left already-mounted widgets on the old theme until a reload. Watch the <html> `dark` class and re-post the context when it flips; plugins already re-apply the theme on trek:context. * fix(plugins): deliver widget context on load so the theme is right on first paint * fix(plugins): give widget cards the native glassy look and auto-height Widget plugins rendered in a plain card with a fixed 180px body, so they looked foreign next to the glassy dashboard tools and taller widgets had their controls clipped. Mirror the native `.tool` surface (glass background/border/blur, uppercase title) and let the body grow to the height the widget reports over trek:resize. * feat(plugins): add read/rwite costs * feat(plugin): better readme/index.js * feat(plugin): better readme/index.js * feat(plugins): hand widgets TREK's theme tokens, formats and display identity Extends trek:context with a non-secret `tokens` map (TREK's resolved CSS design tokens for the current theme), `formats` (currency/date/units/timezone) and a `user` display object (name/avatar/isAdmin — never the email, role only as a boolean). Re-sent on every theme toggle. A widget can now apply the tokens and match the host exactly, in both themes and under a custom appearance, instead of hard-coding a palette that drifts — so plugins feel native, not bolted-on. * feat(plugins): hand plugins the full palette and appearance state The theme context only carried a ~19-token subset read off <html> and only followed the dark-mode toggle. Widen it to the whole global (:root/.dark) palette — surfaces, text, borders, the accent family, semantic + soft fills, shadows, radii and fonts — so a plugin tracks the user's chosen accent scheme, custom accent and high-contrast live, not just light/dark. Also send an `appearance` block (scheme, density, reduced-motion, no-transparency) mirrored from the attributes applyAppearance writes on <html>, and re-post the context whenever any of those actually change (a small signature dedupes unrelated mutations) so plugins restyle in step with the app. * feat(plugins): ship a design kit so plugin UIs look native A plugin's UI is a sandboxed, opaque-origin iframe that can't load TREK's stylesheet — so authors had to re-derive the whole look by hand, and most didn't. Ship it instead: a token-driven stylesheet (glass, hover, buttons, inputs, chips, rows) that consumes the tokens the host already sends and swaps light/dark, plus a small bootstrap that applies those tokens, mirrors the appearance flags, auto-reports the frame height and exposes a `window.trek` helper over the existing bridge. Both are plain strings meant to be inlined (the CSP forbids external assets for an opaque frame); `injectTrekUi` expands a `<!-- trek:ui -->` marker. No new capability — only a native look. * feat(plugins): deliver the design kit — native scaffold + inline on dev/pack A new page/widget scaffolds a native, glassy starter that talks over window.trek. The source keeps a single `<!-- trek:ui -->` line; `dev` (when it serves /ui) and `pack` (as the file enters the archive) expand it into the inlined kit — so the file stays a one-line opt-in and a rebuild always ships the current kit. Existing plugins opt in the same way, by dropping the marker. * feat(plugins): faithful themed host preview in dev `dev` served the plugin UI raw at /ui — top-level, with no host — so the theme, context and bridge never fired and authors couldn't see the design kit render. Add /preview: it embeds /ui in a sandboxed opaque-origin iframe (exactly TREK's isolation) and plays the host — posts trek:context with a theme/accent/appearance toggle, proxies trek:invoke to your /api routes as the dev user, and surfaces resize/notify/navigate. /ui stays as the raw doc for debugging. * docs(plugins): document the design kit, window.trek and the token contract Rewrite the client section of the Plugin Development wiki kit-first: the `<!-- trek:ui -->` marker, the component classes, the `window.trek` bridge, the `/preview` host preview, the full `trek:context` payload (now the whole palette plus an `appearance` block) and how to apply tokens by hand. Add a "Build a native UI" section + the new exports to the SDK README. * feat(budget): add 'Outstanding amount' card * fix(translations): finish translating new keys * feat(plugins): trip-page plugins — a plugin tab inside every trip Adds a `trip-page` plugin type whose sandboxed iframe mounts as a tab in the trip planner (Plan / Transports / … / <plugin>), scoped to the open trip, with no dashboard nav entry. This is the most-asked planner-extension request from discussion #1429 (a plugin that lives in the trip, e.g. SimMesg20's budget planner). It reuses PluginFrame and the existing tab system — the frame already receives the current tripId over trek:context — so there is no bridge or security change: only the manifest type enum (server + SDK), the client feed classification (pluginStore.tripPages), and one render branch in the planner. The SDK scaffolds it with `create --type trip-page`. * fix(apple wallet): support for .pkpasses * feat(plugins): permission-gated write APIs for the planner (#1429) Plugins can now WRITE core planner data, not just read it, through curated, membership-checked methods — so downstream features can live in plugins instead of long-lived core patches. Four new scopes: db:write:places (create/update/delete places), db:write:days (days), db:write:itinerary (assign/unassign a place on a day) and db:write:trips (update trip fields). Each ctx method mirrors costs.create: it validates the input against the SAME @trek/shared schema the web app uses, binds the acting user host-side (a job/onLoad has none, so its writes are refused), checks trip access AND the app's edit permission (place_edit / day_edit / trip_edit), delegates to the real services, broadcasts the same events so open sessions update live, and records the write in the tamper-evident capability audit. No new route, no sandbox or CSP change — the isolation boundary is unchanged; a plugin can only change what its user could change by hand. Consent UI + permission labels in all 22 locales, SDK types + mock host, and the wikis are updated. * docs(plugin): ensure wiki correctness * feat(plugins): plugin metadata on core entities — db:meta (#1429) Plugins can now attach their OWN namespaced key/value data to a trip, place or day without forking the core schema (#1429, request 2). New `db:meta` scope + `ctx.meta.get/set/list/delete`. Storage is one plugin_entity_metadata table (migration 161) keyed (plugin_id, entity_type, entity_id, key) — a plugin only ever sees its own rows. Every call is membership-checked: the entity must belong to a trip the host-bound acting user can access. Quotas guard the shared volume (≤64KB per value, ≤100 keys per entity); rows are purged on uninstall-with-delete-data and recorded in the capability audit. SDK types + mock host, a consent chip + labels in all 22 locales, and the wikis. No new route, no sandbox change. * feat(plugins): place-detail plugin slot in the trip planner (#1429) A widget plugin can declare `capabilities.widget.slot: 'place-detail'` to mount its sandboxed frame inside the trip planner's place-detail panel, scoped to the open place — the frame receives the `placeId` in trek:context alongside the tripId. This is the UI half of the place-detail-providers ask (reviews/ratings/popular times shown on a place). It reuses the existing widget mechanism: PluginFrame gains an optional placeId, the feed/store learn the new slot, and PlaceInspector renders the slot at the foot of its body in trip mode. Admin chip + label in all 22 locales, wiki updated. No sandbox or permission change. * fix(plugins): green the server tests + harden the new capability surface The in-memory uninstall fixture was missing the new plugin_entity_metadata table, so uninstall's DELETE threw "no such table" and failed the server test job. Add the table to the fixture schema. Self-review hardening of the write/metadata surface: - trips.update now reproduces the web UI's per-field gate: is_archived needs trip_archive and cover_image needs trip_cover_upload, not just trip_edit — so a member who may only edit can't archive or re-cover a trip. - Plugin metadata WRITES now also require the entity's edit permission (place_edit/day_edit/trip_edit), not just trip access, so a read-only member can't overwrite or delete metadata another user created. Reads stay access-gated. - Cap the metadata key length (<=256 chars) alongside the value/count quotas — the key was attacker-controlled and uncapped, defeating the disk-DoS guard. * test(plugins): cover the new write/metadata deps to hold the coverage gate The new create-rpc-host write + metadata deps were untested, dropping the src/nest branch coverage below the 80% gate. Add a seeded in-memory core db plus mocked core services to exercise every dep end-to-end: places/days/itinerary create/update/delete + not-found paths, trips.update with the archive/cover per-field gates and the Validation/NotFound/unknown-error mapping, metadata CRUD + key/value/count caps + access checks, the costs deps, and users.getById scoping. Plus rpc-host cases for meta writes on place/day and a no-acting-user refusal. Tests only — no production code change. * fix(costs): freeze FX on every cost + settlement write path (#1445) Settled foreign-currency costs kept re-opening with a few-cent residual when live rates drifted. The #1335 freeze only ran on the REST create/ update path, so two gaps remained: - Foreign-currency items created via MCP create_budget_item or booking- import bypassed the freeze and stored exchange_rate = 1, so settlement re-converted them with live rates. Promote freezeForeignRate into the shared budgetService and call it from every write path. - Settle-up transfers were stored currency-less and re-converted with live rates on each recompute. Add currency + exchange_rate to budget_settlements (migration), freeze the display-currency rate at settle time, and convert with it in calculateSettlement. Legacy rows (currency = NULL / rate = 1) keep live-rate behaviour until re-edited. Also expose guarded cost update/delete to plugins: costs.update and costs.delete under db:write:costs, gated exactly like costs.create (addon + trip access + the acting user's budget_edit permission). updateCost reuses BudgetService.update so a plugin write re-freezes the FX rate too; both broadcast the same budget:updated / budget:deleted events the REST controller emits. Wired through the host, the runtime SDK context and the published trek-plugin-sdk (types + mock host). * feat(plugins): provider hooks — placeDetailProvider, wired (#1429) Turn "hooks" from a declared-but-dead surface into a real host→plugin capability. Add an invoke.hook branch to the child + a supervisor hook registry (providersOf) + PluginRuntimeService.invokeHook, reusing the existing invoke transport and its timeout (a short 5s deadline so a slow provider can't delay a response; a job/onLoad has no user, host-bound as ever). Also fixes a real bug: the in-repo runtime SDK copy was missing the `hooks` field entirely and could not even parse a plugin that declared one — synced it with the published SDK. The first wired hook is placeDetailProvider: a plugin returns extra rows ({label,value?,url?}) for a place, and TREK renders them natively at the foot of the place-detail panel. Consumer is a new, additive, fail-safe endpoint GET /api/place-details/:placeId (membership-checked; any provider that errors or times out is simply skipped — it never breaks the panel). New hook:place-detail- provider scope + consent chip in all 22 locales. SDK types (both copies), a controller test, and the wiki. photoProvider/calendarSource stay reserved but the transport now exists for them. No sandbox or CSP change. * fix(files): handle pkpass in booking uploads and files-tab open (#1447, #1448) Both bugs were client-only; the server already allows .pkpass and serves it as application/vnd.apple.pkpass. #1448: the reservation/transport attachment inputs hard-coded an accept list that omitted pkpass, so macOS grayed it out. Add .pkpass/.pkpasses (+ wallet MIME types) to the accept attribute in both modals. #1447: the files-tab open path routed every non-media/non-markdown file into the in-app PDF preview object. Add isWalletPass() and route wallet passes through the shared blob openFile helper (as bookings already do), which downloads them so the OS hands them to Apple Wallet. * feat(plugins): validation/warning contributions via warningProvider hook (#1429) Second wired provider hook, reusing the invoke.hook infra from the last commit. A plugin implements warningProvider.getWarnings(tripId, ctx) → {level, message, dayId?, placeId?}[] to flag problems on a trip (overpacked day, place closed on its planned date, missing booking, …). TREK surfaces them as a non-blocking overlay banner at the top of the trip planner (the wrapper ignores pointer events so it never covers the map/panels; only the pills are interactive). Consumer is a new additive, fail-safe endpoint GET /api/trip-warnings/:tripId (membership-checked; a provider that errors or times out contributes nothing and never blocks the planner). New hook:trip-warning-provider scope + consent chip in all 22 locales, SDK types (both copies), a controller test, and the wiki. This is the validation half of the scheduling+validation block; feeding durations/travel times back into core recalculation stays out (it would touch core planner computation — deliberately deferred to keep the no-breaking-changes guarantee). * fix(plugins): enforce the hook:* grant on provider dispatch (#1429 audit) The adversarial audit of the #1429 additions found one real (medium) gap: the hook:* permission was never enforced at runtime. providersOf() selected provider plugins purely by the hooks their CODE declares (sup.hooks, reported by the child as Object.keys(def.hooks)) and never intersected that with sup.granted — so a plugin that merely implemented placeDetailProvider/warningProvider got wired in as a provider even when the admin never consented to hook:place-detail-provider / hook:trip-warning-provider. The downstream capability router still held (the hook's ctx can only do what the plugin's OTHER grants allow), but a plugin could obtain an auto-triggered, user-bound execution context on a passive UI browse without the hook being consented — a consent-integrity gap that contradicts the documented invariant. Gate it host-side: a hookName→permission map, and providersOf now returns a plugin only if it is active, implements the hook, AND holds the matching hook:* grant. An unmapped hook resolves to nobody. invokeHook additionally re-checks membership in providersOf (defense-in-depth against a direct caller). Unit test proves the grant/implements/active intersection. * docs(plugins): add the Plugin Cookbook + a trip-doctor example (#1429 eco) Fosters plugin authoring by turning the new #1429 capabilities into copy-paste recipes. New wiki page Plugin-Cookbook (read a trip, write to the itinerary, tag an entity with metadata, contribute native place details, raise trip warnings, broadcast, match the TREK look) linked in the sidebar, plus a complete runnable example — trip-doctor — a hooks-only plugin that showcases warningProvider + placeDetailProvider + ctx.meta with zero UI of its own. Manifest validates against the SDK. Docs/example only; no product code. * fix(collections): don't reset saved-place status to 'idea' on edit (#1437) The update schema reused collectionStatusSchema, whose .default('idea') survives .optional() — so a PATCH that omits status had 'idea' injected by the validation pipe and written to the DB, clobbering 'want'/'visited'. Strip the default on the update field with .removeDefault(), keeping the .catch guard. Add a shared schema regression test and an e2e round-trip. * docs(plugin): ensure plugin scopes are the same everywhere * feat(plugins): read scopes for packing + files (#1429 eco) Extend the read side of the capability model beyond trips/costs: db:read:packing → ctx.packing.list(tripId) and db:read:files → ctx.files.list(tripId). Both mirror the existing trip reads exactly — the host membership-checks the trip against the invocation's user (tripRead) before delegating to the same packingService/ fileService the REST paths use (so bags/assignees hydrate and trash is excluded), and each is a separate scope (packing doesn't unlock files). ctx types in both SDK copies + mock-host, consent labels + cap chips in all 22 locales, rpc-host + create-rpc-host tests, and the wiki (perm table + cookbook recipe). * feat(plugins): core event subscriptions (#1429 eco) A plugin can react to core activity by declaring events: [{ on, handler }] + the events:subscribe grant. websocket.broadcast announces every CORE trip event (name + tripId ONLY, never the payload) through a tiny dependency-free relay (plugin-event-sink); the runtime registers a sink in onModuleInit and the supervisor fans each event out to subscribed, granted, active plugins via a fire-and-forget invoke.event on a short timeout — so a slow subscriber can never block a core write. Safety by construction: handlers run with NO user (like a job) so trip reads are refused — they react to the fact, using the plugin's own ctx.db/ws/outbound; the grant is enforced host-side (deliverEvent checks events:subscribe); plugin:* re- broadcasts are never delivered back, so handlers can't loop; and only the event name + tripId cross the boundary. SDK types (both copies), consent label + cap chip in all 22 locales, supervisor gating + broadcast-tap tests, and the wiki + cookbook. The relay lives in its own module (not websocket) so it doesn't drag `ws` into the runtime and tests that mock ./websocket don't strip the sink. * feat(plugin-sdk): typed ctx returns + native trek.ui DOM helpers (#1429 eco) Two author-DX wins, SDK-only. Typed reads/writes: ctx.trips.getById/getPlaces/getReservations, packing.list, files.list, costs.*, places/days/itinerary writes and users.getById now return proper entity types (Trip, Place, Day, Reservation, PackingItem, TripFile, BudgetItem, Assignment, User) instead of unknown — real autocomplete for authors. Only `id` is guaranteed and every shape keeps an index signature, so it mirrors the raw DB row honestly (no column hidden, no false guarantees). mock-host matches. Native UI helpers: window.trek now carries `trek.ui` — a tiny bundler-free DOM builder (el/button/card/chip/input/mount) that emits the kit's trek-* classes, so a widget builds themed UI with no CSS and no build step. Ships inlined via the same <!-- trek:ui --> marker. Wiki updated. * fix(plugins): scope packing.list to the acting user's #858 visibility (eco audit) The final eco audit found one real (medium) gap: the db:read:packing delegate called packingService.listItems(tripId) with NO userId, which takes the UNFILTERED branch and returns every member's private (is_private=1) packing items — leaking another member's personal/surprise-gift items to a plugin the normal UI/REST hides them from. The handler had the host-bound acting user but dropped it when delegating. Thread it through: tripRead now hands the membership-checked userId to the read callback, packing.list forwards it to listPackingItems(tripId, userId), and the service applies its three-tier #858 filter — a plugin now sees exactly what its user sees. files.list is unaffected (no per-user file visibility). Tests assert the user is passed. The other three audited surfaces (event subscriptions, trek.ui, and the regression sweep of the capability boundary) were clean. * security(plugins): prevent open redirect * fix(plugins): resolve PR #1433 full-audit findings (code + tests) The comprehensive PR audit confirmed 21 findings; this fixes the code/test ones I own: - ctx.users.getById was DEAD: the runtime SDK omitted the _inv tag, so actingUser never bound and every call hit RESOURCE_FORBIDDEN. Add _inv (the test had codified the bug — corrected). - Plugin place writes bypassed the REST STRING_LIMITS (a 100k-char name the web app rejects). Mirror the caps (name 200 / description 2000 / address 500 / notes 2000). - packing.list / files.list were missing from the capability audit log while every other core read is audited — add them to isAuditable + auditResource. - SDK lockstep: CalendarSource.getEvents drifted (published Date vs runtime string); the host->plugin boundary is JSON, so align both to string. - Admin panel didn't know the new trip-page plugin type (unlocalised badge, missing filter) — add it to KNOWN_TYPES + the type filter + a 22-locale label. - Tests for previously-uncovered paths: the child-side invoke.hook/invoke.event dispatch (real fork, hook + event + non-matching-subscription), and invokeHook's defense-in-depth grant re-check. Julien's settlement-FX-refreeze finding is his budget code (flagged, not touched). * docs(plugins): correct the wiki against the shipped capability surface (#1433 audit) Fixes the 11 doc findings from the PR audit — every corrected claim was cross-checked against the code: - Plugin-Development: CSP connect-src is built from granted http:outbound:<host>, not egress[]; dropped the stale "costs.create is the first and only core mutation"; documented costs.update/delete + ctx.packing/ctx.files; the manifest permission table gained the six missing scopes (db:write:places/days/itinerary/trips, db:meta, hook:trip-warning-provider); the widget slot table gained place-detail. - Plugin-Cookbook: days.create no longer passes a title the schema drops; broadcastToUser uses the real (userId, event, data) signature; fixed the broken #the-trek-ui-design-kit anchor and noted window.trek.ui. - Plugin-Permissions: added db:read:packing, db:read:files and events:subscribe; the provider hooks are implemented in `hooks: {...}` on the definition, not on ctx. * fix(unsplash) allow api key usage * fix(guests): scope guest display names per-trip, not globally (#1446) A guest is a per-trip person, but their name lived in the globally UNIQUE users.username, so uniqueGuestUsername() auto-renamed a second "Jake" (on any other trip) to "Jake 2". Add a non-unique users.display_name: a guest now stores the human name there and gets a uuid-based username that is never shown, and every member view (members list, day-assignment participants, budget members/payers, packing recipients/contributors/bags/assignees) COALESCEs display_name over username. Rename updates display_name with no dedup. Real users are unchanged (display_name NULL → COALESCE falls through to username). Migration adds the nullable column; existing guests keep their current username via the COALESCE fallback. This also unblocks ctx.users.getById (the audit's #4 fix), whose projection selects display_name. Tests: two "Jake" guests on two trips both keep the name; the two codified-the-old-behaviour guest tests corrected. * fix(costs): don't re-freeze a settlement's FX rate on an unrelated edit (#1445) The full audit found that updateSettlement called freezeForeignRate without the "currency unchanged" guard the item path has, so any edit of a foreign-currency settlement (e.g. correcting from/to) re-fetched the LIVE rate and overwrote the frozen one — re-opening an already-balanced position with a small residual, the exact drift #1445 was meant to prevent. freezeForeignRate's unchanged-check was item-centric (it queried budget_items), which a settlement (a different table) can't use. Give it an explicit existingCurrency param; updateSettlement now reads the settlement's stored currency and passes it, so an edit that doesn't change the currency keeps the frozen rate (the service UPDATE already preserves exchange_rate when it's left unset). Tests cover both: unchanged currency keeps the rate, a real currency change re-freezes. * feat(plugins): add inter plugin dependency support and addon dependency support * feat(plugins): add inter plugin dependency support and addon dependency support * docs(plugins) inter dependencies --------- Co-authored-by: Maurice <mauriceboe@icloud.com> Co-authored-by: trongbinhnguyen <43725147+trongbinh15@users.noreply.github.com> |
||
|
|
ad893eb1cc |
Release 3.1.0 (#1185)
* Phase 0 — NestJS + Zod foundation harness (F1–F8) (#1050) Co-hosted NestJS app behind the existing Express server via a strangler-fig dispatcher, sharing the same better-sqlite3 connection and JWT httpOnly cookie. Additive and dormant: default routing stays on Express, Nest only serves its own /api/_nest diagnostics until a module opts in. F1 @trek/shared Zod contract package; F2 Nest bootstrap co-hosted (fall-through, single Dockerfile/port); F3 shared better-sqlite3 provider; F4 JWT cookie auth guard (+ @CurrentUser, admin guard); F5 Zod validation pipe + error-envelope parity; F6 Nest test + coverage gates; F7 per-prefix strangler toggle (env, default Express); F8 CI build/typecheck/test/coverage. Remaining F4/F6/F8 checklist items (trip-access + permission levels + MFA policy, e2e harness/seed + 80% gate, Nest↔Express parity test, Playwright PR-comment workflow) are tracked on the first consuming module cards (L1/A1/C1). * feat(weather): migrate /api/weather to the NestJS pilot module (L1) (#1053) First strangler migration (L1): /api/weather is served by a NestJS module. - @trek/shared/weather Zod contract; Nest controller byte-identical to the legacy Express route (paths, query params, status codes, { error } bodies, lang default, ApiError/500 passthrough). Service reuses getWeather/getDetailedWeather (+ shared cache; MCP tools unchanged). - Strangler routes /api/weather to Nest by default; the legacy Express route + its migration-time parity test were decommissioned in this PR. - Frontend (FE2): weatherApi typed against the @trek/shared WeatherResult contract. - Harness: reusable Nest-vs-Express parity harness, e2e harness (temp SQLite + seed/cookie helpers, real JwtAuthGuard), src/nest coverage gate raised to >=80%, src/nest test guide. - Verified end-to-end on a prod mirror (dev1): 401/400/200 via Nest with real Open-Meteo data, Express route gone. * fix(packing): multiply item weight by quantity in bag/total weight calcs (#898) Quantity now counts toward bag and total weights. Generalised to an itemWeight() helper used by every weight sum (bag totals + max, unassigned, grand total; sidebar + bag modal) with unit tests. * feat(i18n): add Korean (ko) translation (#977) Korean translation by @ppuassi, topped up to full en.ts key parity. Language registration follows separately. * feat(i18n): add Japanese (ja) translation (#829) Japanese translation by @soma3978, at full en.ts key parity, registered in supportedLanguages + TranslationContext. * Add Turkish (tr) translation + language registry (#1029) Turkish translation by @SkyLostTR, at full en.ts key parity, registered in supportedLanguages + TranslationContext. * i18n: register Korean + add Ukrainian translation (#1055) Korean translation by @ppuassi (#977) — now registered. Ukrainian by @JeffyOLOLO (#902) — lifted onto a clean branch. Both at full en.ts key parity (2258 keys). * chore: fix monorepo build pipeline and migrate shared to built package (#1056) * chore: fix monorepo build pipeline and migrate shared to built package - Root package.json: add workspace scripts (dev, build, test, test:cov, test:e2e) that delegate to actual scripts in shared/server/client workspaces - shared: add tsup build step (CJS + ESM dual output, .d.ts); consumers now import from the built dist instead of raw TS source via path aliases - server: replace tsc-alias with tsconfig-paths (tsc-alias mangled node_modules paths); fix MCP SDK path aliases to point to root node_modules (../node_modules) - server/scripts/dev.mjs: delay node --watch until tsc -w signals first-pass done, eliminating the spurious restart on every dev startup - client/vite.config.js + vitest.config.ts: remove @trek/shared path alias (no longer needed now that shared is a proper package) - Consolidate package-lock.json at the workspace root; drop per-workspace lock files * chore: fix test script to reflect root package.json * chore: add missing lint and prettier script in root package.json * fix(ci): build shared before tests; fix vitest MCP SDK alias paths vitest.config.ts aliases pointed at ./node_modules/ (server-local) but packages are hoisted to the root node_modules/ in the npm workspace — changed to ../node_modules/. CI jobs now install and build shared before running server/client tests so that @trek/shared's dist/ exists when vitest resolves the package. * fix(docker): update Dockerfile and CI for monorepo workspace structure Dockerfile: - Add shared-builder stage that produces @trek/shared dist before client and server stages need it - Each build stage carries root package.json + package-lock.json so npm can resolve @trek/shared as a workspace dependency - Production stage installs via workspace context (npm ci --workspace=server --omit=dev) so node_modules/@trek/shared symlinks to shared/dist correctly - Copy server/tsconfig.json into the image so tsconfig-paths/register can find the MCP SDK path aliases at runtime - CMD cds into /app/server before starting node so tsconfig-paths baseUrl resolves and ../node_modules points to /app/node_modules - Remove mkdir for /app/server (now a real dir); keep symlinks for uploads/data docker.yml version-bump: - Replace manual per-workspace cd+npm-version calls with single: npm version --workspaces --include-workspace-root --no-git-tag-version (mirrors the version:* scripts in root package.json) - git add now references root package-lock.json; adds shared/package.json .dockerignore: add shared/dist package.json: fix version:prerelease preid (alpha → pre) * fix(tests): use in-memory SQLite per worker in test mode vitest pool:forks spawns parallel worker processes that all called initDb() on the same data/travel.db, causing SQLite "database is locked" and "duplicate column name" races. When NODE_ENV=test each fork now gets an isolated :memory: DB so migrations run independently with no file contention. * chore(ci): add ACT guards to skip DockerHub steps in local act runs act sets ACT=true automatically. Guards added: - docker login: if: ${{ !env.ACT }} - build outputs: type=docker (local load) when ACT, push-by-digest when CI - digest export/upload: if: ${{ !env.ACT }} - merge job: if: ${{ !env.ACT }} - release-helm job (docker.yml): if: ${{ !env.ACT }} - version-bump git push (docker.yml): wrapped in [ -z "$ACT" ] shell guard Run locally with: ./bin/act -j build -W .github/workflows/docker.yml \ -P ubuntu-latest=catthehacker/ubuntu:act-latest * fix(ci): move ACT guards to step level; add guards to security.yml env context is invalid in job-level if conditions — moved all ACT guards down to individual steps. Also guards docker login + scout in security.yml so act can run the build-only part of that workflow. * fix(ci): skip git fetch and tag logic in act (no remote access in local containers) * Revert "fix(ci): skip git fetch and tag logic in act (no remote access in local containers)" This reverts commit |