From 631fea3c51d4e8d173ffd06ced9e6d7113ec3ad2 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 18 Jul 2026 16:38:12 +0200 Subject: [PATCH] chore: update repo url --- .../workflows/close-stale-invalid-titles.yml | 2 +- .../workflows/close-stale-wrong-branch.yml | 2 +- .github/workflows/close-untitled-issues.yml | 2 +- .github/workflows/docker-dev.yml | 14 +- .github/workflows/docker.yml | 14 +- .github/workflows/enforce-target-branch.yml | 2 +- .github/workflows/publish-plugin-sdk.yml | 2 +- .github/workflows/security.yml | 2 +- .github/workflows/wiki.yml | 2 +- README.md | 16 +- SECURITY.md | 2 +- ca_profile.xml | 2 +- charts/trek/values.yaml | 2 +- client/src/components/Admin/GitHubPanel.tsx | 2 +- client/src/pages/admin/AdminUserModals.tsx | 4 +- docker-compose.yml | 2 +- plugin-sdk/src/cli/checks/network.ts | 2 +- plugin-sdk/src/cli/help.ts | 2 +- plugin-sdk/src/cli/submit.ts | 2 +- plugin-sdk/test/sdk.test.ts | 18 +- .../nest/plugins/registry/registry.service.ts | 155 ++++-- server/src/services/adminService.ts | 480 ++++++++++++------ server/src/services/wikiService.ts | 8 +- unraid-template.xml | 8 +- wiki/Admin-GitHub-Releases.md | 2 +- wiki/Development-environment.md | 6 +- wiki/Environment-Variables.md | 2 +- wiki/Install-Docker-Compose.md | 10 +- wiki/Install-Docker.md | 12 +- wiki/Install-Helm.md | 2 +- wiki/Install-Portainer.md | 6 +- wiki/Install-Unraid.md | 2 +- wiki/Quick-Start.md | 2 +- wiki/Updating.md | 10 +- 34 files changed, 529 insertions(+), 272 deletions(-) diff --git a/.github/workflows/close-stale-invalid-titles.yml b/.github/workflows/close-stale-invalid-titles.yml index 9eb1ce338..7cb2944f9 100644 --- a/.github/workflows/close-stale-invalid-titles.yml +++ b/.github/workflows/close-stale-invalid-titles.yml @@ -9,7 +9,7 @@ permissions: jobs: close-stale: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest steps: - name: Close stale invalid-title issues diff --git a/.github/workflows/close-stale-wrong-branch.yml b/.github/workflows/close-stale-wrong-branch.yml index c1a41a3cd..e7b68dc4c 100644 --- a/.github/workflows/close-stale-wrong-branch.yml +++ b/.github/workflows/close-stale-wrong-branch.yml @@ -10,7 +10,7 @@ permissions: jobs: close-stale: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest steps: - name: Close stale wrong-base-branch PRs diff --git a/.github/workflows/close-untitled-issues.yml b/.github/workflows/close-untitled-issues.yml index 0e303dffe..a09a74719 100644 --- a/.github/workflows/close-untitled-issues.yml +++ b/.github/workflows/close-untitled-issues.yml @@ -9,7 +9,7 @@ permissions: jobs: check-title: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest steps: - name: Flag or redirect issue diff --git a/.github/workflows/docker-dev.yml b/.github/workflows/docker-dev.yml index d2e3ed070..2ec2126c7 100644 --- a/.github/workflows/docker-dev.yml +++ b/.github/workflows/docker-dev.yml @@ -18,7 +18,7 @@ concurrency: jobs: version-bump: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest outputs: version: ${{ steps.bump.outputs.VERSION }} @@ -98,7 +98,7 @@ jobs: with: context: . platforms: ${{ matrix.platform }} - outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true + outputs: type=image,name=iiketrek/TREK,push-by-digest=true,name-canonical=true,push=true no-cache: true build-args: | APP_VERSION=${{ needs.version-bump.outputs.version }} @@ -145,16 +145,16 @@ jobs: working-directory: /tmp/digests run: | VERSION="${{ needs.version-bump.outputs.version }}" - mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *) + mapfile -t digests < <(printf 'iiketrek/TREK@sha256:%s\n' *) MAJOR_TAG="$(echo "$VERSION" | cut -d. -f1)-pre" docker buildx imagetools create \ - -t "mauriceboe/trek:latest-pre" \ - -t "mauriceboe/trek:$MAJOR_TAG" \ - -t "mauriceboe/trek:$VERSION" \ + -t "iiketrek/TREK:latest-pre" \ + -t "iiketrek/TREK:$MAJOR_TAG" \ + -t "iiketrek/TREK:$VERSION" \ "${digests[@]}" - name: Inspect manifest - run: docker buildx imagetools inspect mauriceboe/trek:latest-pre + run: docker buildx imagetools inspect iiketrek/TREK:latest-pre - name: Push git tag run: | diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bd31e32f3..bd7904e6e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -33,7 +33,7 @@ concurrency: jobs: version-bump: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest outputs: version: ${{ steps.bump.outputs.VERSION }} @@ -149,7 +149,7 @@ jobs: with: context: . platforms: ${{ matrix.platform }} - outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true + outputs: type=image,name=iiketrek/TREK,push-by-digest=true,name-canonical=true,push=true no-cache: true build-args: | APP_VERSION=${{ needs.version-bump.outputs.version }} @@ -194,16 +194,16 @@ jobs: working-directory: /tmp/digests run: | VERSION="${{ needs.version-bump.outputs.version }}" - mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *) + mapfile -t digests < <(printf 'iiketrek/TREK@sha256:%s\n' *) MAJOR_TAG="$(echo "$VERSION" | cut -d. -f1)" docker buildx imagetools create \ - -t "mauriceboe/trek:latest" \ - -t "mauriceboe/trek:$MAJOR_TAG" \ - -t "mauriceboe/trek:$VERSION" \ + -t "iiketrek/TREK:latest" \ + -t "iiketrek/TREK:$MAJOR_TAG" \ + -t "iiketrek/TREK:$VERSION" \ "${digests[@]}" - name: Inspect manifest - run: docker buildx imagetools inspect mauriceboe/trek:latest + run: docker buildx imagetools inspect iiketrek/TREK:latest release-helm: runs-on: ubuntu-latest diff --git a/.github/workflows/enforce-target-branch.yml b/.github/workflows/enforce-target-branch.yml index 54e3a0ecb..14762d012 100644 --- a/.github/workflows/enforce-target-branch.yml +++ b/.github/workflows/enforce-target-branch.yml @@ -6,7 +6,7 @@ on: jobs: check-target: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest permissions: pull-requests: write diff --git a/.github/workflows/publish-plugin-sdk.yml b/.github/workflows/publish-plugin-sdk.yml index 705771351..52c597eda 100644 --- a/.github/workflows/publish-plugin-sdk.yml +++ b/.github/workflows/publish-plugin-sdk.yml @@ -14,7 +14,7 @@ permissions: jobs: publish: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index af22f82a0..782bec7a8 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -13,7 +13,7 @@ jobs: scout: # Docker Hub secrets are not exposed to pull requests from forks, so the # Scout login can never succeed there. - if: github.repository == 'mauriceboe/TREK' && github.event.pull_request.head.repo.fork != true + if: github.repository == 'iiketrek/TREK' && github.event.pull_request.head.repo.fork != true runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index 8c6974520..79d72398a 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -17,7 +17,7 @@ concurrency: jobs: deploy: - if: github.repository == 'mauriceboe/TREK' + if: github.repository == 'iiketrek/TREK' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 724bcd710..f415fbe2d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa Demo   -Docker +Docker   Discord   @@ -31,9 +31,9 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa BMAC
License -Latest Release -Docker Pulls -Stars +Latest Release +Docker Pulls +Stars @@ -176,7 +176,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa ```bash ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d -p 3000:3000 \ -e ENCRYPTION_KEY=$ENCRYPTION_KEY \ - -v ./data:/app/data -v ./uploads:/app/uploads mauriceboe/trek + -v ./data:/app/data -v ./uploads:/app/uploads iiketrek/TREK ``` Open `http://localhost:3000`. On first boot TREK seeds an admin account — if you set `ADMIN_EMAIL`/`ADMIN_PASSWORD` those are used, otherwise the credentials are printed to the container log (`docker logs trek`). @@ -217,7 +217,7 @@ Real-time sync via WebSocket (`ws`). Backend on NestJS 11. State with Zustand. A ```yaml services: app: - image: mauriceboe/trek:latest + image: iiketrek/TREK:latest container_name: trek read_only: true security_opt: @@ -305,9 +305,9 @@ docker compose pull && docker compose up -d **Docker run** — reuse the original volume paths: ```bash -docker pull mauriceboe/trek +docker pull iiketrek/TREK docker rm -f trek -docker run -d --name trek -p 3000:3000 -v ./data:/app/data -v ./uploads:/app/uploads --restart unless-stopped mauriceboe/trek +docker run -d --name trek -p 3000:3000 -v ./data:/app/data -v ./uploads:/app/uploads --restart unless-stopped iiketrek/TREK ``` > Not sure which paths you used? `docker inspect trek --format '{{json .Mounts}}'` before removing the container. diff --git a/SECURITY.md b/SECURITY.md index e2ce04147..49b7d195d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,6 +21,6 @@ You will receive a response within 48 hours. Once confirmed, a fix will be relea ## Scope -This policy covers the TREK application and its Docker image (`mauriceboe/trek`). +This policy covers the TREK application and its Docker image (`iiketrek/TREK`). Third-party dependencies are monitored via GitHub Dependabot. diff --git a/ca_profile.xml b/ca_profile.xml index 60e60a0bf..58494a471 100644 --- a/ca_profile.xml +++ b/ca_profile.xml @@ -1,7 +1,7 @@ TREK is a self-hosted, real-time collaborative travel planner. Plan trips together with interactive maps, budgets, bookings, packing lists, day-by-day itineraries and file management — every change syncs instantly across everyone in your group. Includes OIDC/SSO, TOTP MFA, dark mode, PWA support, multi-language UI and a modular addon system (Vacay, Atlas, Collab, Budget, Packing, Journey). Maintained by mauriceboe — support and bug reports via GitHub Issues. - https://raw.githubusercontent.com/mauriceboe/TREK/main/docs/trek-icon.png + https://raw.githubusercontent.com/iiketrek/TREK/main/docs/trek-icon.png https://github.com/liketrek/TREK https://github.com/liketrek/TREK/issues https://ko-fi.com/mauriceboe diff --git a/charts/trek/values.yaml b/charts/trek/values.yaml index 4af6bd9ba..c4d32d6b4 100644 --- a/charts/trek/values.yaml +++ b/charts/trek/values.yaml @@ -1,6 +1,6 @@ image: - repository: mauriceboe/trek + repository: iiketrek/TREK # tag: latest pullPolicy: IfNotPresent diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index bc64a1648..c63ed2d85 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -15,7 +15,7 @@ import { useEffect, useState } from 'react'; import apiClient from '../../api/client'; import { getLocaleForLanguage, useTranslation } from '../../i18n'; -const REPO = 'mauriceboe/TREK'; +const REPO = 'iiketrek/TREK'; const PER_PAGE = 10; interface GithubRelease { diff --git a/client/src/pages/admin/AdminUserModals.tsx b/client/src/pages/admin/AdminUserModals.tsx index d74b4bea1..6375ac2cf 100644 --- a/client/src/pages/admin/AdminUserModals.tsx +++ b/client/src/pages/admin/AdminUserModals.tsx @@ -336,14 +336,14 @@ export default function AdminUserModals({ admin, t }: AdminUserModalsProps): Rea }} className="border border-gray-700 bg-gray-900 text-gray-100 dark:bg-gray-950" > - {`docker pull mauriceboe/trek:latest + {`docker pull iiketrek/TREK:latest docker stop trek && docker rm trek docker run -d --name trek \\ -p 3000:3000 \\ -v /opt/trek/data:/app/data \\ -v /opt/trek/uploads:/app/uploads \\ --restart unless-stopped \\ - mauriceboe/trek:latest`} + iiketrek/TREK:latest`} )} diff --git a/docker-compose.yml b/docker-compose.yml index e59ccbc2a..febebf0c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: mauriceboe/trek:latest + image: iiketrek/TREK:latest container_name: trek read_only: true security_opt: diff --git a/plugin-sdk/src/cli/checks/network.ts b/plugin-sdk/src/cli/checks/network.ts index ab85b16e5..a1ab83758 100644 --- a/plugin-sdk/src/cli/checks/network.ts +++ b/plugin-sdk/src/cli/checks/network.ts @@ -27,7 +27,7 @@ import type { NetworkCheck, CheckContext, RegistryEntry, RegistryEntryVersion } import { pass, fail, skip } from './types.js'; import { REQUIRED_SECTIONS, MIN_PROSE_CHARS, missingSections, placeholders, proseLength, images, undocumentedPermissions } from './readme.js'; -export const DEFAULT_REGISTRY = 'mauriceboe/TREK-Plugins'; +export const DEFAULT_REGISTRY = 'iiketrek/TREK-Plugins'; const NATIVE_RE = /(^|\/)[^/]+\.node$|(^|\/)binding\.gyp$|(^|\/)prebuilds?\//i; diff --git a/plugin-sdk/src/cli/help.ts b/plugin-sdk/src/cli/help.ts index 6f6f98bb6..a9b8882f4 100644 --- a/plugin-sdk/src/cli/help.ts +++ b/plugin-sdk/src/cli/help.ts @@ -107,7 +107,7 @@ Scripts are never prompted; pass --sign. Needs git and an authenticated \`gh\`. --sign [key] sign the artifact (default key: ~/.trek-plugin/signing.key) - --registry o/n a registry other than mauriceboe/TREK-Plugins + --registry o/n a registry other than iiketrek/TREK-Plugins --notes release notes --draft open the registry PR as a draft --force overwrite an existing release's artifact. Breaks the sha256 pin for diff --git a/plugin-sdk/src/cli/submit.ts b/plugin-sdk/src/cli/submit.ts index 32e28f9d2..236d73627 100644 --- a/plugin-sdk/src/cli/submit.ts +++ b/plugin-sdk/src/cli/submit.ts @@ -12,7 +12,7 @@ import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { readJsonFile } from './json.js'; -const DEFAULT_REGISTRY = 'mauriceboe/TREK-Plugins'; +const DEFAULT_REGISTRY = 'iiketrek/TREK-Plugins'; interface EntryLike { id: string; name?: string; authorPublicKey?: string; diff --git a/plugin-sdk/test/sdk.test.ts b/plugin-sdk/test/sdk.test.ts index 2558a6570..7989271ef 100644 --- a/plugin-sdk/test/sdk.test.ts +++ b/plugin-sdk/test/sdk.test.ts @@ -726,7 +726,7 @@ describe('pack + entry (publishing automation)', () => { const out = path.join(tmp, 'plugin.zip'); const packed = packPluginDir(koffi, out); const entry = buildEntry({ - dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, + dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), now: '2026-07-04T00:00:00.000Z', }); expect(entry.id).toBe('koffi'); @@ -751,7 +751,7 @@ describe('pack + entry (publishing automation)', () => { const out = path.join(tmp, 'plugin.zip'); packPluginDir(koffi, out); const entry = buildEntry({ - dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, + dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), now: '2026-07-04T00:00:00.000Z', }); expect(entry.icon).toBe('Luggage'); // koffi's trek-plugin.json declares it @@ -764,7 +764,7 @@ describe('pack + entry (publishing automation)', () => { const p = path.join(tmp, `prior-${icon}.json`); fs.writeFileSync(p, JSON.stringify({ id: 'koffi', name: 'Koffi', author: 'TREK', description: 'x', - repo: 'mauriceboe/trek-plugin-koffi', type: 'widget', icon, + repo: 'iiketrek/TREK-plugin-koffi', type: 'widget', icon, versions: [{ version: '0.9.0', gitTag: 'v0.9.0', commitSha: 'b'.repeat(40), downloadUrl: 'https://x/y.zip', sha256: 'c'.repeat(64), size: 10, apiVersion: 1, nativeModules: false, publishedAt: '2026-01-01T00:00:00.000Z' }], })); return p; @@ -772,7 +772,7 @@ describe('pack + entry (publishing automation)', () => { // koffi's manifest declares Luggage → an author who rebrands sees it in the store const rebranded = buildEntry({ - dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, + dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), now: '2026-07-04T00:00:00.000Z', mergePath: prior('Coffee'), }); expect(rebranded.icon).toBe('Luggage'); @@ -820,11 +820,11 @@ describe('pack + entry (publishing automation)', () => { packPluginDir(koffi, out); const existingPath = path.join(tmp, 'koffi.json'); fs.writeFileSync(existingPath, JSON.stringify({ - id: 'koffi', name: 'Koffi', author: 'TREK', description: 'x', repo: 'mauriceboe/trek-plugin-koffi', type: 'widget', + id: 'koffi', name: 'Koffi', author: 'TREK', description: 'x', repo: 'iiketrek/TREK-plugin-koffi', type: 'widget', versions: [{ version: '0.9.0', gitTag: 'v0.9.0', commitSha: 'b'.repeat(40), downloadUrl: 'https://github.com/x/y/releases/download/v0.9.0/plugin.zip', sha256: 'c'.repeat(64), minTrekVersion: '3.2.0', size: 10, apiVersion: 1, nativeModules: false, publishedAt: '2026-01-01T00:00:00.000Z' }], })); const merged = buildEntry({ - dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, + dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), mergePath: existingPath, now: '2026-07-04T00:00:00.000Z', }); expect(merged.versions.map((v) => v.version)).toEqual(['1.0.0', '0.9.0']); @@ -868,7 +868,7 @@ describe('sign + keygen (author signatures, TOFU)', () => { packPluginDir(koffi, out); const keyPath = path.join(tmp, 'k.pem'); const { publicKey } = generateKeypair(keyPath); - const entry = buildEntry({ dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), signKeyPath: keyPath, now: '2026-07-04T00:00:00.000Z' }); + const entry = buildEntry({ dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), signKeyPath: keyPath, now: '2026-07-04T00:00:00.000Z' }); expect(entry.authorPublicKey).toBe(publicKey); const sig = entry.versions[0].signature; expect(sig).toBeTruthy(); @@ -880,12 +880,12 @@ describe('sign + keygen (author signatures, TOFU)', () => { packPluginDir(koffi, out); const key1 = path.join(tmp, 'k1.pem'); generateKeypair(key1); - const existing = buildEntry({ dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), signKeyPath: key1, now: '2026-07-04T00:00:00.000Z' }); + const existing = buildEntry({ dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), signKeyPath: key1, now: '2026-07-04T00:00:00.000Z' }); const existingPath = path.join(tmp, 'koffi.json'); fs.writeFileSync(existingPath, JSON.stringify({ ...existing, versions: existing.versions.map((v) => ({ ...v, version: '0.9.0', gitTag: 'v0.9.0' })) })); const key2 = path.join(tmp, 'k2.pem'); generateKeypair(key2); - expect(() => buildEntry({ dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.1.0', zipPath: out, commit: 'a'.repeat(40), mergePath: existingPath, signKeyPath: key2, now: '2026-07-04T00:00:00.000Z' })).toThrow(/differs from the one already published/); + expect(() => buildEntry({ dir: koffi, repo: 'iiketrek/TREK-plugin-koffi', tag: 'v1.1.0', zipPath: out, commit: 'a'.repeat(40), mergePath: existingPath, signKeyPath: key2, now: '2026-07-04T00:00:00.000Z' })).toThrow(/differs from the one already published/); }); }); diff --git a/server/src/nest/plugins/registry/registry.service.ts b/server/src/nest/plugins/registry/registry.service.ts index 47ab9b322..acb9ebf72 100644 --- a/server/src/nest/plugins/registry/registry.service.ts +++ b/server/src/nest/plugins/registry/registry.service.ts @@ -1,18 +1,19 @@ -import { Injectable } from '@nestjs/common'; -import semver from 'semver'; -import fs from 'node:fs'; -import path from 'node:path'; import { db } from '../../../db/database'; +import { discoverPlugins } from '../install/discovery'; +import { hostSatisfies, hostVersion, normalizedHost } from '../install/host-compat'; import type { PluginDependency } from '../install/manifest'; -import { pluginCodeDir, pluginsCodeRoot, pluginsDataRoot } from '../paths'; +import { parseJsonText, parseManifest } from '../install/manifest'; +import { scanForNativeBinaries } from '../install/native-scan'; +import { extractArchive } from '../install/safe-extract'; import { safeDownload, sha256Matches } from '../install/safe-fetch'; import { verifyAuthorSignature, SignatureError } from '../install/verify-signature'; +import { pluginCodeDir, pluginsCodeRoot, pluginsDataRoot } from '../paths'; import { clearUpdateBlock, isSignatureCode, setUpdateBlock, RETRUSTABLE_CODE } from '../signature-status'; -import { extractArchive } from '../install/safe-extract'; -import { scanForNativeBinaries } from '../install/native-scan'; -import { parseJsonText, parseManifest } from '../install/manifest'; -import { hostSatisfies, hostVersion, normalizedHost } from '../install/host-compat'; -import { discoverPlugins } from '../install/discovery'; +import { Injectable } from '@nestjs/common'; + +import fs from 'node:fs'; +import path from 'node:path'; +import semver from 'semver'; /** * TREK-side of the plugin registry (#plugins, M5). Fetches the single aggregated @@ -25,7 +26,7 @@ import { discoverPlugins } from '../install/discovery'; const REGISTRY_URL = process.env.TREK_PLUGIN_REGISTRY_URL || - 'https://raw.githubusercontent.com/mauriceboe/TREK-Plugins/main/dist/index.json'; + 'https://raw.githubusercontent.com/iiketrek/TREK-Plugins/main/dist/index.json'; const CACHE_TTL = 30 * 60 * 1000; const MANIFEST_MAX_BYTES = 256 * 1024; // Sideload upload ceiling — matches the SDK `pack` limit (50 MB) plus zip overhead. @@ -151,7 +152,10 @@ export class PluginRegistryService { try { const url = force ? `${REGISTRY_URL}${REGISTRY_URL.includes('?') ? '&' : '?'}_=${Date.now()}` : REGISTRY_URL; const headers: Record = { 'User-Agent': 'TREK-Server' }; - if (force) { headers['Cache-Control'] = 'no-cache'; headers.Pragma = 'no-cache'; } + if (force) { + headers['Cache-Control'] = 'no-cache'; + headers.Pragma = 'no-cache'; + } const resp = await fetch(url, { headers }); if (!resp.ok) throw new Error(`registry ${resp.status}`); const data = (await resp.json()) as Registry; @@ -172,16 +176,40 @@ export class PluginRegistryService { * `authorPublicKey` is exposed in full — it is a public key, and the re-trust * round-trip compares it exactly (a truncated fingerprint would be a weak check). */ - async browse(force = false): Promise & { latest: string | null; minTrekVersion: string | null; requiredAddons: string[]; pluginDependencies: PluginDependency[]; screenshotUrl: string | null; signed: boolean; authorPublicKey: string | null } & HostCompat>> { + async browse( + force = false, + ): Promise< + Array< + Omit & { + latest: string | null; + minTrekVersion: string | null; + requiredAddons: string[]; + pluginDependencies: PluginDependency[]; + screenshotUrl: string | null; + signed: boolean; + authorPublicKey: string | null; + } & HostCompat + > + > { const reg = await this.fetchRegistry(force); return reg.plugins.map((p) => { const latest = p.versions[0] ?? null; return { - id: p.id, name: p.name, author: p.author, description: p.description, repo: p.repo, - homepage: p.homepage, tags: p.tags, type: p.type, icon: p.icon, reviewedAt: p.reviewedAt ?? null, + id: p.id, + name: p.name, + author: p.author, + description: p.description, + repo: p.repo, + homepage: p.homepage, + tags: p.tags, + type: p.type, + icon: p.icon, + reviewedAt: p.reviewedAt ?? null, downloadCount: p.downloadCount ?? null, - latest: latest?.version ?? null, minTrekVersion: latest?.minTrekVersion ?? null, - requiredAddons: latest?.requiredAddons ?? [], pluginDependencies: latest?.pluginDependencies ?? [], + latest: latest?.version ?? null, + minTrekVersion: latest?.minTrekVersion ?? null, + requiredAddons: latest?.requiredAddons ?? [], + pluginDependencies: latest?.pluginDependencies ?? [], screenshotUrl: latest ? rawFileUrl(p.repo, latest.commitSha, 'docs/screenshot.png') : null, signed: !!p.authorPublicKey && !!latest?.signature, authorPublicKey: p.authorPublicKey ?? null, @@ -230,7 +258,10 @@ export class PluginRegistryService { let manifest: ManifestPreview | null = null; if (latest) { try { - const { bytes } = await safeDownload(rawFileUrl(entry.repo, latest.commitSha, 'trek-plugin.json'), MANIFEST_MAX_BYTES); + const { bytes } = await safeDownload( + rawFileUrl(entry.repo, latest.commitSha, 'trek-plugin.json'), + MANIFEST_MAX_BYTES, + ); manifest = previewManifest(parseJsonText(bytes.toString('utf8'))); } catch { // Soft-fail: the detail view still renders from registry metadata alone. @@ -238,12 +269,22 @@ export class PluginRegistryService { } const data = { - id: entry.id, name: entry.name, author: entry.author, description: entry.description, - repo: entry.repo, homepage: entry.homepage ?? null, tags: entry.tags ?? [], type: entry.type, - reviewedAt: entry.reviewedAt ?? null, downloadCount: entry.downloadCount ?? null, - latest: latest?.version ?? null, minTrekVersion: latest?.minTrekVersion ?? null, - size: latest?.size ?? null, publishedAt: latest?.publishedAt ?? null, - requiredAddons: latest?.requiredAddons ?? [], pluginDependencies: latest?.pluginDependencies ?? [], + id: entry.id, + name: entry.name, + author: entry.author, + description: entry.description, + repo: entry.repo, + homepage: entry.homepage ?? null, + tags: entry.tags ?? [], + type: entry.type, + reviewedAt: entry.reviewedAt ?? null, + downloadCount: entry.downloadCount ?? null, + latest: latest?.version ?? null, + minTrekVersion: latest?.minTrekVersion ?? null, + size: latest?.size ?? null, + publishedAt: latest?.publishedAt ?? null, + requiredAddons: latest?.requiredAddons ?? [], + pluginDependencies: latest?.pluginDependencies ?? [], screenshotUrl: latest ? rawFileUrl(entry.repo, latest.commitSha, 'docs/screenshot.png') : null, signed: !!entry.authorPublicKey && !!latest?.signature, authorPublicKey: entry.authorPublicKey ?? null, @@ -336,7 +377,10 @@ export class PluginRegistryService { * blessed key that doesn't actually sign the code is refused like any other bad * signature. It is never a way to install something unverified. */ - async install(id: string, opts?: { version?: string; constraint?: string; retrustKey?: string }): Promise<{ id: string; version: string }> { + async install( + id: string, + opts?: { version?: string; constraint?: string; retrustKey?: string }, + ): Promise<{ id: string; version: string }> { const reg = await this.fetchRegistry(); const entry = reg.plugins.find((p) => p.id === id); if (!entry) throw new RegistryError(`plugin ${id} not in registry`); @@ -366,7 +410,10 @@ export class PluginRegistryService { if (!pluginRoot) throw new RegistryError('archive contains no trek-plugin.json'); // 4. re-validate the bundled manifest + 5. native re-scan - const manifest = parseManifest(parseJsonText(fs.readFileSync(path.join(pluginRoot, 'trek-plugin.json'), 'utf8')), { requireTrek: true }); + const manifest = parseManifest( + parseJsonText(fs.readFileSync(path.join(pluginRoot, 'trek-plugin.json'), 'utf8')), + { requireTrek: true }, + ); if (manifest.id !== id) throw new RegistryError(`manifest id "${manifest.id}" != "${id}"`); // The artifact's OWN range is the authoritative compat check. The index metadata // gated above is only what the registry says about this version, and it is weaker: @@ -385,7 +432,11 @@ export class PluginRegistryService { // 7. register INACTIVE (record provenance) discoverPlugins(db); db.prepare('UPDATE plugins SET source_repo = ?, source_commit = ?, sha256 = ?, reviewed_at = ? WHERE id = ?').run( - entry.repo, ver.commitSha, ver.sha256, entry.reviewedAt ?? null, id, + entry.repo, + ver.commitSha, + ver.sha256, + entry.reviewedAt ?? null, + id, ); // Pin the author key on first successful install of a signed plugin (TOFU) — // and, after a re-trust, re-pin to the new key the admin blessed. Only ever set @@ -410,8 +461,13 @@ export class PluginRegistryService { * can't be installed; they're collected and returned so the caller can prompt the * admin to enable them. Cycle-safe. Already-installed plugins are left untouched. */ - async installWithDependencies(id: string, constraint?: string): Promise<{ installed: string[]; requiredAddons: string[] }> { - const installedNow = new Set((db.prepare('SELECT id FROM plugins').all() as Array<{ id: string }>).map((r) => r.id)); + async installWithDependencies( + id: string, + constraint?: string, + ): Promise<{ installed: string[]; requiredAddons: string[] }> { + const installedNow = new Set( + (db.prepare('SELECT id FROM plugins').all() as Array<{ id: string }>).map((r) => r.id), + ); const done = new Set(); const installed: string[] = []; const requiredAddons = new Set(); @@ -451,7 +507,9 @@ export class PluginRegistryService { extractArchive(bytes, stagingDir); const root = locateManifestDir(stagingDir); if (!root) throw new RegistryError('archive contains no trek-plugin.json'); - const manifest = parseManifest(parseJsonText(fs.readFileSync(path.join(root, 'trek-plugin.json'), 'utf8')), { requireTrek: true }); + const manifest = parseManifest(parseJsonText(fs.readFileSync(path.join(root, 'trek-plugin.json'), 'utf8')), { + requireTrek: true, + }); assertHostCompatible(manifest.trekRange, manifest.id); if (scanForNativeBinaries(root).length) throw new RegistryError('artifact contains native binaries'); return { id: manifest.id, version: manifest.version, root, stagingDir }; @@ -514,17 +572,31 @@ export class PluginRegistryService { * key, over the artifact bytes. A key an admin blesses must still sign the code it * ships — a re-trust moves the pin from one VERIFIED key to another verified key. */ - private verifySignatureAndTofu(id: string, bytes: Buffer, entry: RegistryEntry, ver: RegistryVersion, retrustKey?: string): void { - const pinned = (db.prepare('SELECT author_pubkey FROM plugins WHERE id = ?').get(id) as { author_pubkey?: string } | undefined)?.author_pubkey ?? null; + private verifySignatureAndTofu( + id: string, + bytes: Buffer, + entry: RegistryEntry, + ver: RegistryVersion, + retrustKey?: string, + ): void { + const pinned = + (db.prepare('SELECT author_pubkey FROM plugins WHERE id = ?').get(id) as { author_pubkey?: string } | undefined) + ?.author_pubkey ?? null; if (!entry.authorPublicKey && !ver.signature) { if (pinned) { - throw new RegistryError('this plugin was signed before but the update is unsigned — refusing', 'SIGNATURE_MISSING'); + throw new RegistryError( + 'this plugin was signed before but the update is unsigned — refusing', + 'SIGNATURE_MISSING', + ); } return; // unsigned throughout: sha256 is the only pin } if (!entry.authorPublicKey || !ver.signature) { - throw new RegistryError('incomplete signature: an author key and a version signature must both be present', 'SIGNATURE_INCOMPLETE'); + throw new RegistryError( + 'incomplete signature: an author key and a version signature must both be present', + 'SIGNATURE_INCOMPLETE', + ); } if (pinned && pinned !== entry.authorPublicKey && retrustKey !== entry.authorPublicKey) { throw new RegistryError( @@ -574,7 +646,10 @@ export class PluginRegistryService { const pinned = row.author_pubkey ?? null; if (!pinned || !entry.authorPublicKey || pinned === entry.authorPublicKey) { - throw new RegistryError("this plugin's signing key has not changed — there is nothing to re-trust", 'RETRUST_NOT_APPLICABLE'); + throw new RegistryError( + "this plugin's signing key has not changed — there is nothing to re-trust", + 'RETRUST_NOT_APPLICABLE', + ); } if (entry.authorPublicKey !== publicKey) { throw new RegistryError( @@ -615,10 +690,7 @@ function hostCompatible(v: RegistryVersion, host: string | null): boolean { */ export function assertHostCompatible(range: string | null, id: string): void { if (hostSatisfies(range)) return; - throw new RegistryError( - `${id} requires TREK ${range} — this is TREK ${hostVersion()}`, - 'TREK_VERSION_INCOMPATIBLE', - ); + throw new RegistryError(`${id} requires TREK ${range} — this is TREK ${hostVersion()}`, 'TREK_VERSION_INCOMPATIBLE'); } /** @@ -660,7 +732,10 @@ function previewManifest(raw: unknown): ManifestPreview { const pluginDependencies = Array.isArray(m.pluginDependencies) ? m.pluginDependencies .filter((d): d is Record => !!d && typeof d === 'object') - .map((d) => ({ id: typeof d.id === 'string' ? d.id : '', version: typeof d.version === 'string' ? d.version : '' })) + .map((d) => ({ + id: typeof d.id === 'string' ? d.id : '', + version: typeof d.version === 'string' ? d.version : '', + })) .filter((d) => d.id && d.version) : []; return { diff --git a/server/src/services/adminService.ts b/server/src/services/adminService.ts index 401f42c55..cb10b158c 100644 --- a/server/src/services/adminService.ts +++ b/server/src/services/adminService.ts @@ -1,22 +1,23 @@ -import bcrypt from 'bcryptjs'; -import { avatarUrl } from './avatarUrl'; -import crypto from 'crypto'; -import path from 'path'; -import fs from 'fs'; -import { db } from '../db/database'; -import { User, Addon } from '../types'; -import { updateJwtSecret } from '../config'; -import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto'; -import { getAllPermissions, savePermissions as savePerms, PERMISSION_ACTIONS } from './permissions'; -import { revokeUserSessions, revokeUserSessionsForClient } from '../mcp'; -import { deleteUserCompletely } from './userCleanupService'; -import { emitUserDeleted } from '../plugin-user-lifecycle'; -import { validatePassword } from './passwordPolicy'; -import { getPhotoProviderConfig } from './memories/helpersService'; import { ADDON_IDS } from '../addons'; -import { prepareLlmAddonConfigForWrite, maskLlmAddonConfig } from './llmConfig'; -import { send as sendNotification } from './notificationService'; +import { updateJwtSecret } from '../config'; +import { db } from '../db/database'; +import { revokeUserSessions, revokeUserSessionsForClient } from '../mcp'; +import { emitUserDeleted } from '../plugin-user-lifecycle'; +import { User, Addon } from '../types'; +import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto'; import { resolveAuthToggles } from './authService'; +import { avatarUrl } from './avatarUrl'; +import { prepareLlmAddonConfigForWrite, maskLlmAddonConfig } from './llmConfig'; +import { getPhotoProviderConfig } from './memories/helpersService'; +import { send as sendNotification } from './notificationService'; +import { validatePassword } from './passwordPolicy'; +import { getAllPermissions, savePermissions as savePerms, PERMISSION_ACTIONS } from './permissions'; +import { deleteUserCompletely } from './userCleanupService'; + +import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; // ── Helpers ──────────────────────────────────────────────────────────────── @@ -36,9 +37,11 @@ export function compareVersions(a: string, b: string): number { const preN = n !== null && Number.isFinite(n) ? n : null; return { parts, preN }; }; - const pa = parse(a), pb = parse(b); + const pa = parse(a), + pb = parse(b); for (let i = 0; i < Math.max(pa.parts.length, pb.parts.length); i++) { - const na = pa.parts[i] || 0, nb = pb.parts[i] || 0; + const na = pa.parts[i] || 0, + nb = pb.parts[i] || 0; if (na > nb) return 1; if (na < nb) return -1; } @@ -54,8 +57,13 @@ export function compareVersions(a: string, b: string): number { export const isDocker = (() => { try { - return fs.existsSync('/.dockerenv') || (fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker')); - } catch { return false; } + return ( + fs.existsSync('/.dockerenv') || + (fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker')) + ); + } catch { + return false; + } })(); // ── User CRUD ────────────────────────────────────────────────────────────── @@ -63,15 +71,21 @@ export const isDocker = (() => { export function listUsers() { // Guests (#1362) are accountless trip participants, not real users — keep them out // of admin user management entirely. - const users = db.prepare( - 'SELECT id, username, email, role, avatar, created_at, updated_at, last_login FROM users WHERE COALESCE(is_guest, 0) = 0 ORDER BY created_at DESC' - ).all() as (Pick & { avatar?: string | null })[]; + const users = db + .prepare( + 'SELECT id, username, email, role, avatar, created_at, updated_at, last_login FROM users WHERE COALESCE(is_guest, 0) = 0 ORDER BY created_at DESC', + ) + .all() as (Pick & { + avatar?: string | null; + })[]; let onlineUserIds = new Set(); try { const { getOnlineUserIds } = require('../websocket'); onlineUserIds = getOnlineUserIds(); - } catch { /* */ } - return users.map(u => ({ + } catch { + /* */ + } + return users.map((u) => ({ ...u, avatar_url: avatarUrl(u), created_at: utcSuffix(u.created_at), @@ -98,7 +112,9 @@ export function createUser(data: { username: string; email: string; password: st } // Guests (#1362) live in a reserved synthetic namespace; never let one block a real account. - const existingUsername = db.prepare('SELECT id FROM users WHERE username = ? AND COALESCE(is_guest, 0) = 0').get(username); + const existingUsername = db + .prepare('SELECT id FROM users WHERE username = ? AND COALESCE(is_guest, 0) = 0') + .get(username); if (existingUsername) return { error: 'Username already taken', status: 409 }; const existingEmail = db.prepare('SELECT id FROM users WHERE email = ? AND COALESCE(is_guest, 0) = 0').get(email); @@ -106,13 +122,13 @@ export function createUser(data: { username: string; email: string; password: st const passwordHash = bcrypt.hashSync(password, BCRYPT_COST); - const result = db.prepare( - 'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)' - ).run(username, email, passwordHash, data.role || 'user'); + const result = db + .prepare('INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)') + .run(username, email, passwordHash, data.role || 'user'); - const user = db.prepare( - 'SELECT id, username, email, role, created_at, updated_at FROM users WHERE id = ?' - ).get(result.lastInsertRowid); + const user = db + .prepare('SELECT id, username, email, role, created_at, updated_at FROM users WHERE id = ?') + .get(result.lastInsertRowid); return { user, @@ -134,11 +150,15 @@ export function updateUser(id: string, data: { username?: string; email?: string } if (username && username !== user.username) { - const conflict = db.prepare('SELECT id FROM users WHERE username = ? AND id != ? AND COALESCE(is_guest, 0) = 0').get(username, id); + const conflict = db + .prepare('SELECT id FROM users WHERE username = ? AND id != ? AND COALESCE(is_guest, 0) = 0') + .get(username, id); if (conflict) return { error: 'Username already taken', status: 409 }; } if (email && email !== user.email) { - const conflict = db.prepare('SELECT id FROM users WHERE email = ? AND id != ? AND COALESCE(is_guest, 0) = 0').get(email, id); + const conflict = db + .prepare('SELECT id FROM users WHERE email = ? AND id != ? AND COALESCE(is_guest, 0) = 0') + .get(email, id); if (conflict) return { error: 'Email already taken', status: 409 }; } @@ -153,12 +173,15 @@ export function updateUser(id: string, data: { username?: string; email?: string if (role && role !== 'admin') { const current = db.prepare('SELECT role FROM users WHERE id = ?').get(id) as { role?: string } | undefined; if (current?.role === 'admin') { - const adminCount = (db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number }).count; + const adminCount = ( + db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get() as { count: number } + ).count; if (adminCount <= 1) return { error: 'Cannot remove the last admin', status: 400 }; } } - db.prepare(` + db.prepare( + ` UPDATE users SET username = COALESCE(?, username), email = COALESCE(?, email), @@ -166,11 +189,12 @@ export function updateUser(id: string, data: { username?: string; email?: string password_hash = COALESCE(?, password_hash), updated_at = CURRENT_TIMESTAMP WHERE id = ? - `).run(username || null, email || null, role || null, passwordHash, id); + `, + ).run(username || null, email || null, role || null, passwordHash, id); - const updated = db.prepare( - 'SELECT id, username, email, role, created_at, updated_at FROM users WHERE id = ?' - ).get(id); + const updated = db + .prepare('SELECT id, username, email, role, created_at, updated_at FROM users WHERE id = ?') + .get(id); const changed: string[] = []; if (username) changed.push('username'); @@ -190,7 +214,9 @@ export function deleteUser(id: string, currentUserId: number) { return { error: 'Cannot delete own account', status: 400 }; } - const userToDel = db.prepare('SELECT id, email FROM users WHERE id = ?').get(id) as { id: number; email: string } | undefined; + const userToDel = db.prepare('SELECT id, email FROM users WHERE id = ?').get(id) as + | { id: number; email: string } + | undefined; if (!userToDel) return { error: 'User not found', status: 404 }; deleteUserCompletely(userToDel.id); @@ -201,7 +227,9 @@ export function deleteUser(id: string, currentUserId: number) { // ── Stats ────────────────────────────────────────────────────────────────── export function getStats() { - const totalUsers = (db.prepare('SELECT COUNT(*) as count FROM users WHERE COALESCE(is_guest, 0) = 0').get() as { count: number }).count; + const totalUsers = ( + db.prepare('SELECT COUNT(*) as count FROM users WHERE COALESCE(is_guest, 0) = 0').get() as { count: number } + ).count; const totalTrips = (db.prepare('SELECT COUNT(*) as count FROM trips').get() as { count: number }).count; const totalPlaces = (db.prepare('SELECT COUNT(*) as count FROM places').get() as { count: number }).count; const totalFiles = (db.prepare('SELECT COUNT(*) as count FROM trip_files').get() as { count: number }).count; @@ -212,7 +240,7 @@ export function getStats() { export function getPermissions() { const current = getAllPermissions(); - const actions = PERMISSION_ACTIONS.map(a => ({ + const actions = PERMISSION_ACTIONS.map((a) => ({ key: a.key, level: current[a.key], defaultLevel: a.defaultLevel, @@ -246,13 +274,17 @@ export function getAuditLog(query: { limit?: string; offset?: string }) { ip: string | null; }; - const rows = db.prepare(` + const rows = db + .prepare( + ` SELECT a.id, a.created_at, a.user_id, u.username, u.email as user_email, a.action, a.resource, a.details, a.ip FROM audit_log a LEFT JOIN users u ON u.id = a.user_id ORDER BY a.id DESC LIMIT ? OFFSET ? - `).all(limit, offset) as Row[]; + `, + ) + .all(limit, offset) as Row[]; const total = (db.prepare('SELECT COUNT(*) as c FROM audit_log').get() as { c: number }).c; @@ -265,7 +297,8 @@ export function getAuditLog(query: { limit?: string; offset?: string }) { details = { _parse_error: true }; } } - const created_at = r.created_at && !r.created_at.endsWith('Z') ? r.created_at.replace(' ', 'T') + 'Z' : r.created_at; + const created_at = + r.created_at && !r.created_at.endsWith('Z') ? r.created_at.replace(' ', 'T') + 'Z' : r.created_at; return { ...r, created_at, details }; }); @@ -275,7 +308,8 @@ export function getAuditLog(query: { limit?: string; offset?: string }) { // ── OIDC Settings ────────────────────────────────────────────────────────── export function getOidcSettings() { - const get = (key: string) => (db.prepare("SELECT value FROM app_settings WHERE key = ?").get(key) as { value: string } | undefined)?.value || ''; + const get = (key: string) => + (db.prepare('SELECT value FROM app_settings WHERE key = ?').get(key) as { value: string } | undefined)?.value || ''; const secret = decrypt_api_key(get('oidc_client_secret')); return { issuer: get('oidc_issuer'), @@ -296,10 +330,14 @@ export function updateOidcSettings(data: { }): { error?: string; status?: number; success?: boolean } { // Lockout prevention: can't remove OIDC config when password login is disabled if ((data.issuer === '' || data.client_id === '') && !resolveAuthToggles().password_login) { - return { error: 'Cannot remove SSO configuration while password login is disabled. Enable password login first.', status: 400 }; + return { + error: 'Cannot remove SSO configuration while password login is disabled. Enable password login first.', + status: 400, + }; } - const set = (key: string, val: string) => db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)").run(key, val || ''); + const set = (key: string, val: string) => + db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run(key, val || ''); set('oidc_issuer', data.issuer ?? ''); set('oidc_client_id', data.client_id ?? ''); if (data.client_secret !== undefined) set('oidc_client_secret', maybe_encrypt_api_key(data.client_secret) ?? ''); @@ -328,10 +366,9 @@ export function saveDemoBaseline(): { error?: string; status?: number; message?: export async function getGithubReleases(perPage: string = '10', page: string = '1') { try { - const resp = await fetch( - `https://api.github.com/repos/mauriceboe/TREK/releases?per_page=${perPage}&page=${page}`, - { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } } - ); + const resp = await fetch(`https://api.github.com/repos/iiketrek/TREK/releases?per_page=${perPage}&page=${page}`, { + headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' }, + }); if (!resp.ok) return []; const data = await resp.json(); return Array.isArray(data) ? data : []; @@ -364,41 +401,59 @@ export async function checkVersion(): Promise { const currentVersion: string = process.env.APP_VERSION || require('../../package.json').version; const isPrerelease = currentVersion.includes('-pre.'); - const fallback: VersionInfo = { current: currentVersion, latest: currentVersion, update_available: false, is_docker: isDocker, is_prerelease: isPrerelease }; + const fallback: VersionInfo = { + current: currentVersion, + latest: currentVersion, + update_available: false, + is_docker: isDocker, + is_prerelease: isPrerelease, + }; let result: VersionInfo; try { if (isPrerelease) { // Fetch release list and find the newest prerelease - const resp = await fetch( - 'https://api.github.com/repos/mauriceboe/TREK/releases?per_page=100', - { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } } - ); + const resp = await fetch('https://api.github.com/repos/iiketrek/TREK/releases?per_page=100', { + headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' }, + }); if (!resp.ok) { return fallback; } - const data = await resp.json() as Array<{ tag_name?: string; html_url?: string; prerelease?: boolean }>; - const prereleases = Array.isArray(data) ? data.filter(r => r.prerelease) : []; + const data = (await resp.json()) as Array<{ tag_name?: string; html_url?: string; prerelease?: boolean }>; + const prereleases = Array.isArray(data) ? data.filter((r) => r.prerelease) : []; if (!prereleases.length) { return fallback; } // Pre-compute stripped versions, then sort descending - const tagged = prereleases.map(r => ({ r, v: (r.tag_name || '').replace(/^v/, '') })); + const tagged = prereleases.map((r) => ({ r, v: (r.tag_name || '').replace(/^v/, '') })); tagged.sort((a, b) => compareVersions(b.v, a.v)); const latest = tagged[0].v; const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0; - result = { current: currentVersion, latest, update_available, release_url: tagged[0].r.html_url || '', is_docker: isDocker, is_prerelease: true }; + result = { + current: currentVersion, + latest, + update_available, + release_url: tagged[0].r.html_url || '', + is_docker: isDocker, + is_prerelease: true, + }; } else { - const resp = await fetch( - 'https://api.github.com/repos/mauriceboe/TREK/releases/latest', - { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } } - ); + const resp = await fetch('https://api.github.com/repos/iiketrek/TREK/releases/latest', { + headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' }, + }); if (!resp.ok) { return fallback; } - const data = await resp.json() as { tag_name?: string; html_url?: string }; + const data = (await resp.json()) as { tag_name?: string; html_url?: string }; const latest = (data.tag_name || '').replace(/^v/, ''); const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0; - result = { current: currentVersion, latest, update_available, release_url: data.html_url || '', is_docker: isDocker, is_prerelease: false }; + result = { + current: currentVersion, + latest, + update_available, + release_url: data.html_url || '', + is_docker: isDocker, + is_prerelease: false, + }; } } catch { return fallback; @@ -413,10 +468,17 @@ export async function checkAndNotifyVersion(): Promise { const result = await checkVersion(); if (!result.update_available) return; - const lastNotified = (db.prepare('SELECT value FROM app_settings WHERE key = ?').get('last_notified_version') as { value: string } | undefined)?.value; + const lastNotified = ( + db.prepare('SELECT value FROM app_settings WHERE key = ?').get('last_notified_version') as + | { value: string } + | undefined + )?.value; if (lastNotified === result.latest) return; - db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('last_notified_version', result.latest); + db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run( + 'last_notified_version', + result.latest, + ); await sendNotification({ event: 'version_available', @@ -433,13 +495,17 @@ export async function checkAndNotifyVersion(): Promise { // ── Invite Tokens ────────────────────────────────────────────────────────── export function listInvites() { - return db.prepare(` + return db + .prepare( + ` SELECT i.*, u.username as created_by_name, t.title as trip_title FROM invite_tokens i JOIN users u ON i.created_by = u.id LEFT JOIN trips t ON i.trip_id = t.id ORDER BY i.created_at DESC - `).all(); + `, + ) + .all(); } /** Trips an admin can bind an invite to — id + title only, for the picker (#1402). */ @@ -447,7 +513,10 @@ export function listTripsForInvite() { return db.prepare('SELECT id, title FROM trips ORDER BY title COLLATE NOCASE ASC').all(); } -export function createInvite(createdBy: number, data: { max_uses?: string | number; expires_in_days?: string | number; trip_id?: string | number | null }) { +export function createInvite( + createdBy: number, + data: { max_uses?: string | number; expires_in_days?: string | number; trip_id?: string | number | null }, +) { const rawUses = parseInt(String(data.max_uses)); const uses = rawUses === 0 ? 0 : Math.min(Math.max(rawUses || 1, 1), 5); const token = crypto.randomBytes(16).toString('hex'); @@ -465,18 +534,22 @@ export function createInvite(createdBy: number, data: { max_uses?: string | numb } } - const ins = db.prepare( - 'INSERT INTO invite_tokens (token, max_uses, expires_at, created_by, trip_id) VALUES (?, ?, ?, ?, ?)' - ).run(token, uses, expiresAt, createdBy, tripId); + const ins = db + .prepare('INSERT INTO invite_tokens (token, max_uses, expires_at, created_by, trip_id) VALUES (?, ?, ?, ?, ?)') + .run(token, uses, expiresAt, createdBy, tripId); const inviteId = Number(ins.lastInsertRowid); - const invite = db.prepare(` + const invite = db + .prepare( + ` SELECT i.*, u.username as created_by_name, t.title as trip_title FROM invite_tokens i JOIN users u ON i.created_by = u.id LEFT JOIN trips t ON i.trip_id = t.id WHERE i.id = ? - `).get(inviteId); + `, + ) + .get(inviteId); return { invite, inviteId, uses, expiresInDays: data.expires_in_days ?? null, tripId }; } @@ -491,57 +564,82 @@ export function deleteInvite(id: string) { // ── Bag Tracking ─────────────────────────────────────────────────────────── export function getBagTracking() { - const row = db.prepare("SELECT value FROM app_settings WHERE key = 'bag_tracking_enabled'").get() as { value: string } | undefined; + const row = db.prepare("SELECT value FROM app_settings WHERE key = 'bag_tracking_enabled'").get() as + | { value: string } + | undefined; return { enabled: row?.value === 'true' }; } export function updateBagTracking(enabled: boolean) { - db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('bag_tracking_enabled', ?)").run(enabled ? 'true' : 'false'); + db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('bag_tracking_enabled', ?)").run( + enabled ? 'true' : 'false', + ); return { enabled: !!enabled }; } // ── Places Photos ───────────────────────────────────────────────────────── export function getPlacesPhotos() { - const row = db.prepare("SELECT value FROM app_settings WHERE key = 'places_photos_enabled'").get() as { value: string } | undefined; + const row = db.prepare("SELECT value FROM app_settings WHERE key = 'places_photos_enabled'").get() as + | { value: string } + | undefined; return { enabled: row?.value !== 'false' }; } export function updatePlacesPhotos(enabled: boolean) { - db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_photos_enabled', ?)").run(enabled ? 'true' : 'false'); + db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_photos_enabled', ?)").run( + enabled ? 'true' : 'false', + ); return { enabled: !!enabled }; } // ── Places Autocomplete ──────────────────────────────────────────────────── export function getPlacesAutocomplete() { - const row = db.prepare("SELECT value FROM app_settings WHERE key = 'places_autocomplete_enabled'").get() as { value: string } | undefined; + const row = db.prepare("SELECT value FROM app_settings WHERE key = 'places_autocomplete_enabled'").get() as + | { value: string } + | undefined; return { enabled: row?.value !== 'false' }; } export function updatePlacesAutocomplete(enabled: boolean) { - db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_autocomplete_enabled', ?)").run(enabled ? 'true' : 'false'); + db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_autocomplete_enabled', ?)").run( + enabled ? 'true' : 'false', + ); return { enabled: !!enabled }; } // ── Places Details ───────────────────────────────────────────────────────── export function getPlacesDetails() { - const row = db.prepare("SELECT value FROM app_settings WHERE key = 'places_details_enabled'").get() as { value: string } | undefined; + const row = db.prepare("SELECT value FROM app_settings WHERE key = 'places_details_enabled'").get() as + | { value: string } + | undefined; return { enabled: row?.value !== 'false' }; } export function updatePlacesDetails(enabled: boolean) { - db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_details_enabled', ?)").run(enabled ? 'true' : 'false'); + db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('places_details_enabled', ?)").run( + enabled ? 'true' : 'false', + ); return { enabled: !!enabled }; } // ── Collab Features ─────────────────────────────────────────────────────── -const COLLAB_FEATURE_KEYS = ['collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled'] as const; +const COLLAB_FEATURE_KEYS = [ + 'collab_chat_enabled', + 'collab_notes_enabled', + 'collab_polls_enabled', + 'collab_whatsnext_enabled', +] as const; export function getCollabFeatures() { - const rows = db.prepare("SELECT key, value FROM app_settings WHERE key IN ('collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled')").all() as { key: string; value: string }[]; + const rows = db + .prepare( + "SELECT key, value FROM app_settings WHERE key IN ('collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled')", + ) + .all() as { key: string; value: string }[]; const map: Record = {}; for (const r of rows) map[r.key] = r.value; return { @@ -552,10 +650,20 @@ export function getCollabFeatures() { }; } -export function updateCollabFeatures(features: { chat?: boolean; notes?: boolean; polls?: boolean; whatsnext?: boolean }) { - const mapping: Record = { chat: 'collab_chat_enabled', notes: 'collab_notes_enabled', polls: 'collab_polls_enabled', whatsnext: 'collab_whatsnext_enabled' }; +export function updateCollabFeatures(features: { + chat?: boolean; + notes?: boolean; + polls?: boolean; + whatsnext?: boolean; +}) { + const mapping: Record = { + chat: 'collab_chat_enabled', + notes: 'collab_notes_enabled', + polls: 'collab_polls_enabled', + whatsnext: 'collab_whatsnext_enabled', + }; const before = getCollabFeatures(); - const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)"); + const stmt = db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)'); for (const [feat, key] of Object.entries(mapping)) { if (features[feat] !== undefined) stmt.run(key, features[feat] ? 'true' : 'false'); } @@ -563,38 +671,50 @@ export function updateCollabFeatures(features: { chat?: boolean; notes?: boolean // Collab flags gate MCP tool/resource registration, so callers must know // whether anything actually flipped — a no-op save must not tear down every // live MCP session (#1414). - const changed = (Object.keys(after) as Array).some(k => after[k] !== before[k]); + const changed = (Object.keys(after) as Array).some((k) => after[k] !== before[k]); return { features: after, changed }; } // ── Packing Templates ────────────────────────────────────────────────────── export function listPackingTemplates() { - return db.prepare(` + return db + .prepare( + ` SELECT pt.*, u.username as created_by_name, (SELECT COUNT(*) FROM packing_template_items ti JOIN packing_template_categories tc ON ti.category_id = tc.id WHERE tc.template_id = pt.id) as item_count, (SELECT COUNT(*) FROM packing_template_categories WHERE template_id = pt.id) as category_count FROM packing_templates pt JOIN users u ON pt.created_by = u.id ORDER BY pt.created_at DESC - `).all(); + `, + ) + .all(); } export function getPackingTemplate(id: string) { const template = db.prepare('SELECT * FROM packing_templates WHERE id = ?').get(id); if (!template) return { error: 'Template not found', status: 404 }; - const categories = db.prepare('SELECT * FROM packing_template_categories WHERE template_id = ? ORDER BY sort_order, id').all(id) as any[]; - const items = db.prepare(` + const categories = db + .prepare('SELECT * FROM packing_template_categories WHERE template_id = ? ORDER BY sort_order, id') + .all(id) as any[]; + const items = db + .prepare( + ` SELECT ti.* FROM packing_template_items ti JOIN packing_template_categories tc ON ti.category_id = tc.id WHERE tc.template_id = ? ORDER BY ti.sort_order, ti.id - `).all(id); + `, + ) + .all(id); return { template, categories, items }; } export function createPackingTemplate(name: string, createdBy: number) { if (!name?.trim()) return { error: 'Name is required', status: 400 }; - const result = db.prepare('INSERT INTO packing_templates (name, created_by) VALUES (?, ?)').run(name.trim(), createdBy); + const result = db + .prepare('INSERT INTO packing_templates (name, created_by) VALUES (?, ?)') + .run(name.trim(), createdBy); const template = db.prepare('SELECT * FROM packing_templates WHERE id = ?').get(result.lastInsertRowid); return { template }; } @@ -619,20 +739,29 @@ export function createTemplateCategory(templateId: string, name: string) { if (!name?.trim()) return { error: 'Category name is required', status: 400 }; const template = db.prepare('SELECT * FROM packing_templates WHERE id = ?').get(templateId); if (!template) return { error: 'Template not found', status: 404 }; - const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_template_categories WHERE template_id = ?').get(templateId) as { max: number | null }; - const result = db.prepare('INSERT INTO packing_template_categories (template_id, name, sort_order) VALUES (?, ?, ?)').run(templateId, name.trim(), (maxOrder.max ?? -1) + 1); + const maxOrder = db + .prepare('SELECT MAX(sort_order) as max FROM packing_template_categories WHERE template_id = ?') + .get(templateId) as { max: number | null }; + const result = db + .prepare('INSERT INTO packing_template_categories (template_id, name, sort_order) VALUES (?, ?, ?)') + .run(templateId, name.trim(), (maxOrder.max ?? -1) + 1); return { category: db.prepare('SELECT * FROM packing_template_categories WHERE id = ?').get(result.lastInsertRowid) }; } export function updateTemplateCategory(templateId: string, catId: string, data: { name?: string }) { - const cat = db.prepare('SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?').get(catId, templateId); + const cat = db + .prepare('SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?') + .get(catId, templateId); if (!cat) return { error: 'Category not found', status: 404 }; - if (data.name?.trim()) db.prepare('UPDATE packing_template_categories SET name = ? WHERE id = ?').run(data.name.trim(), catId); + if (data.name?.trim()) + db.prepare('UPDATE packing_template_categories SET name = ? WHERE id = ?').run(data.name.trim(), catId); return { category: db.prepare('SELECT * FROM packing_template_categories WHERE id = ?').get(catId) }; } export function deleteTemplateCategory(templateId: string, catId: string) { - const cat = db.prepare('SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?').get(catId, templateId); + const cat = db + .prepare('SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?') + .get(catId, templateId); if (!cat) return { error: 'Category not found', status: 404 }; db.prepare('DELETE FROM packing_template_categories WHERE id = ?').run(catId); return {}; @@ -642,17 +771,24 @@ export function deleteTemplateCategory(templateId: string, catId: string) { export function createTemplateItem(templateId: string, catId: string, name: string) { if (!name?.trim()) return { error: 'Item name is required', status: 400 }; - const cat = db.prepare('SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?').get(catId, templateId); + const cat = db + .prepare('SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?') + .get(catId, templateId); if (!cat) return { error: 'Category not found', status: 404 }; - const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_template_items WHERE category_id = ?').get(catId) as { max: number | null }; - const result = db.prepare('INSERT INTO packing_template_items (category_id, name, sort_order) VALUES (?, ?, ?)').run(catId, name.trim(), (maxOrder.max ?? -1) + 1); + const maxOrder = db + .prepare('SELECT MAX(sort_order) as max FROM packing_template_items WHERE category_id = ?') + .get(catId) as { max: number | null }; + const result = db + .prepare('INSERT INTO packing_template_items (category_id, name, sort_order) VALUES (?, ?, ?)') + .run(catId, name.trim(), (maxOrder.max ?? -1) + 1); return { item: db.prepare('SELECT * FROM packing_template_items WHERE id = ?').get(result.lastInsertRowid) }; } export function updateTemplateItem(itemId: string, data: { name?: string }) { const item = db.prepare('SELECT * FROM packing_template_items WHERE id = ?').get(itemId); if (!item) return { error: 'Item not found', status: 404 }; - if (data.name?.trim()) db.prepare('UPDATE packing_template_items SET name = ? WHERE id = ?').run(data.name.trim(), itemId); + if (data.name?.trim()) + db.prepare('UPDATE packing_template_items SET name = ? WHERE id = ?').run(data.name.trim(), itemId); return { item: db.prepare('SELECT * FROM packing_template_items WHERE id = ?').get(itemId) }; } @@ -672,16 +808,31 @@ export function isAddonEnabled(addonId: string): boolean { export function listAddons() { const addons = db.prepare('SELECT * FROM addons ORDER BY sort_order, id').all() as Addon[]; - const providers = db.prepare(` + const providers = db + .prepare( + ` SELECT id, name, description, icon, enabled, sort_order FROM photo_providers ORDER BY sort_order, id - `).all() as Array<{ id: string; name: string; description?: string | null; icon: string; enabled: number; sort_order: number }>; - const fields = db.prepare(` + `, + ) + .all() as Array<{ + id: string; + name: string; + description?: string | null; + icon: string; + enabled: number; + sort_order: number; + }>; + const fields = db + .prepare( + ` SELECT provider_id, field_key, label, input_type, placeholder, required, secret, settings_key, payload_key, sort_order FROM photo_provider_fields ORDER BY sort_order, id - `).all() as Array<{ + `, + ) + .all() as Array<{ provider_id: string; field_key: string; label: string; @@ -701,14 +852,15 @@ export function listAddons() { } return [ - ...addons.map(a => ({ + ...addons.map((a) => ({ ...a, enabled: !!a.enabled, - config: a.id === ADDON_IDS.LLM_PARSING - ? maskLlmAddonConfig(JSON.parse(a.config || '{}')) - : JSON.parse(a.config || '{}'), + config: + a.id === ADDON_IDS.LLM_PARSING + ? maskLlmAddonConfig(JSON.parse(a.config || '{}')) + : JSON.parse(a.config || '{}'), })), - ...providers.map(p => ({ + ...providers.map((p) => ({ id: p.id, name: p.name, description: p.description, @@ -716,7 +868,7 @@ export function listAddons() { icon: p.icon, enabled: !!p.enabled, config: getPhotoProviderConfig(p.id), - fields: (fieldsByProvider.get(p.id) || []).map(f => ({ + fields: (fieldsByProvider.get(p.id) || []).map((f) => ({ key: f.field_key, label: f.label, input_type: f.input_type, @@ -734,44 +886,52 @@ export function listAddons() { export function updateAddon(id: string, data: { enabled?: boolean; config?: Record }) { const addon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon | undefined; - const provider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; sort_order: number } | undefined; + const provider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as + | { id: string; name: string; description?: string | null; icon: string; enabled: number; sort_order: number } + | undefined; if (!addon && !provider) return { error: 'Addon not found', status: 404 }; if (addon) { - if (data.enabled !== undefined) db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); + if (data.enabled !== undefined) + db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); if (data.config !== undefined) { // The AI-parsing addon holds an API key — encrypt it at rest and preserve // the stored key when the client echoes the mask sentinel (see llmConfig.ts). - const configToStore = id === ADDON_IDS.LLM_PARSING - ? prepareLlmAddonConfigForWrite(data.config, JSON.parse(addon.config || '{}')) - : data.config; + const configToStore = + id === ADDON_IDS.LLM_PARSING + ? prepareLlmAddonConfigForWrite(data.config, JSON.parse(addon.config || '{}')) + : data.config; db.prepare('UPDATE addons SET config = ? WHERE id = ?').run(JSON.stringify(configToStore), id); } } else { - if (data.enabled !== undefined) db.prepare('UPDATE photo_providers SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); + if (data.enabled !== undefined) + db.prepare('UPDATE photo_providers SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id); } const updatedAddon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon | undefined; - const updatedProvider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; sort_order: number } | undefined; + const updatedProvider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as + | { id: string; name: string; description?: string | null; icon: string; enabled: number; sort_order: number } + | undefined; const updated = updatedAddon ? { - ...updatedAddon, - enabled: !!updatedAddon.enabled, - config: updatedAddon.id === ADDON_IDS.LLM_PARSING - ? maskLlmAddonConfig(JSON.parse(updatedAddon.config || '{}')) - : JSON.parse(updatedAddon.config || '{}'), - } + ...updatedAddon, + enabled: !!updatedAddon.enabled, + config: + updatedAddon.id === ADDON_IDS.LLM_PARSING + ? maskLlmAddonConfig(JSON.parse(updatedAddon.config || '{}')) + : JSON.parse(updatedAddon.config || '{}'), + } : updatedProvider ? { - id: updatedProvider.id, - name: updatedProvider.name, - description: updatedProvider.description, - type: 'photo_provider', - icon: updatedProvider.icon, - enabled: !!updatedProvider.enabled, - config: getPhotoProviderConfig(updatedProvider.id), - sort_order: updatedProvider.sort_order, - } + id: updatedProvider.id, + name: updatedProvider.name, + description: updatedProvider.description, + type: 'photo_provider', + icon: updatedProvider.icon, + enabled: !!updatedProvider.enabled, + config: getPhotoProviderConfig(updatedProvider.id), + sort_order: updatedProvider.sort_order, + } : null; // Only these addons gate MCP tool/resource/prompt registration (see @@ -779,31 +939,45 @@ export function updateAddon(id: string, data: { enabled?: boolean; config?: Reco // what a session would register. Config-only saves, photo providers and // MCP-irrelevant addons must not tear down every live session (#1414). const MCP_RELEVANT_ADDONS = new Set([ - ADDON_IDS.MCP, ADDON_IDS.PACKING, ADDON_IDS.BUDGET, ADDON_IDS.COLLAB, - ADDON_IDS.ATLAS, ADDON_IDS.VACAY, ADDON_IDS.JOURNEY, + ADDON_IDS.MCP, + ADDON_IDS.PACKING, + ADDON_IDS.BUDGET, + ADDON_IDS.COLLAB, + ADDON_IDS.ATLAS, + ADDON_IDS.VACAY, + ADDON_IDS.JOURNEY, ]); const enabledChanged = !!addon && data.enabled !== undefined && (data.enabled ? 1 : 0) !== addon.enabled; return { addon: updated, mcpAffected: enabledChanged && MCP_RELEVANT_ADDONS.has(id), - auditDetails: { enabled: data.enabled !== undefined ? !!data.enabled : undefined, config_changed: data.config !== undefined }, + auditDetails: { + enabled: data.enabled !== undefined ? !!data.enabled : undefined, + config_changed: data.config !== undefined, + }, }; } // ── MCP Tokens ───────────────────────────────────────────────────────────── export function listMcpTokens() { - return db.prepare(` + return db + .prepare( + ` SELECT t.id, t.name, t.token_prefix, t.created_at, t.last_used_at, t.user_id, u.username FROM mcp_tokens t JOIN users u ON u.id = t.user_id ORDER BY t.created_at DESC - `).all(); + `, + ) + .all(); } export function deleteMcpToken(id: string) { - const token = db.prepare('SELECT id, user_id FROM mcp_tokens WHERE id = ?').get(id) as { id: number; user_id: number } | undefined; + const token = db.prepare('SELECT id, user_id FROM mcp_tokens WHERE id = ?').get(id) as + | { id: number; user_id: number } + | undefined; if (!token) return { error: 'Token not found', status: 404 }; db.prepare('DELETE FROM mcp_tokens WHERE id = ?').run(id); revokeUserSessions(token.user_id); @@ -813,7 +987,9 @@ export function deleteMcpToken(id: string) { // ── OAuth Sessions ───────────────────────────────────────────────────────── export function listOAuthSessions() { - const rows = db.prepare(` + const rows = db + .prepare( + ` SELECT ot.id, ot.client_id, oc.name AS client_name, ot.user_id, u.username, ot.scopes, ot.access_token_expires_at, ot.refresh_token_expires_at, ot.created_at FROM oauth_tokens ot @@ -822,12 +998,16 @@ export function listOAuthSessions() { WHERE ot.revoked_at IS NULL AND ot.refresh_token_expires_at > CURRENT_TIMESTAMP ORDER BY ot.created_at DESC - `).all() as (Record & { scopes: string })[]; - return rows.map(r => ({ ...r, scopes: JSON.parse(r.scopes) })); + `, + ) + .all() as (Record & { scopes: string })[]; + return rows.map((r) => ({ ...r, scopes: JSON.parse(r.scopes) })); } export function revokeOAuthSession(id: string) { - const row = db.prepare('SELECT id, user_id, client_id FROM oauth_tokens WHERE id = ?').get(id) as { id: number; user_id: number; client_id: string } | undefined; + const row = db.prepare('SELECT id, user_id, client_id FROM oauth_tokens WHERE id = ?').get(id) as + | { id: number; user_id: number; client_id: string } + | undefined; if (!row) return { error: 'Session not found', status: 404 }; db.prepare('UPDATE oauth_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE id = ?').run(id); revokeUserSessionsForClient(row.user_id, row.client_id); diff --git a/server/src/services/wikiService.ts b/server/src/services/wikiService.ts index 09b4f53ce..7a1f3d769 100644 --- a/server/src/services/wikiService.ts +++ b/server/src/services/wikiService.ts @@ -1,5 +1,5 @@ -import path from 'path'; import { existsSync, promises as fs } from 'fs'; +import path from 'path'; /** * In-app Help/Wiki content, sourced from the `wiki/**` directory that ships with @@ -13,7 +13,7 @@ import { existsSync, promises as fs } from 'fs'; * GitHub directly either way; images are proxied through /api/help/asset. */ -const REPO = 'mauriceboe/TREK'; +const REPO = 'iiketrek/TREK'; const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/main/wiki`; const TTL_MS = 60 * 60 * 1000; // remote fallback only: refresh from GitHub at most hourly const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; @@ -33,7 +33,9 @@ const WIKI_DIR = process.env.TREK_WIKI_DIR ?? path.join(__dirname, '..', '..', ' const useLocalWiki = existsSync(path.join(WIKI_DIR, '_Sidebar.md')); if (!useLocalWiki) { - console.warn(`[help] wiki not found at ${WIKI_DIR} — falling back to the GitHub wiki (help may not match this version)`); + console.warn( + `[help] wiki not found at ${WIKI_DIR} — falling back to the GitHub wiki (help may not match this version)`, + ); } export class WikiNotFound extends Error { diff --git a/unraid-template.xml b/unraid-template.xml index 799b0a569..d8cbe369f 100644 --- a/unraid-template.xml +++ b/unraid-template.xml @@ -1,8 +1,8 @@ TREK - mauriceboe/trek - https://hub.docker.com/r/mauriceboe/trek + iiketrek/TREK + https://hub.docker.com/r/iiketrek/TREK latest Latest stable release @@ -14,8 +14,8 @@ TREK is a self-hosted, real-time collaborative travel planner with interactive maps, budgets, bookings, packing lists, file management, and more. Plan trips together with your group — changes sync instantly across all connected users. Includes OIDC/SSO support, dark mode, PWA, and a modular addon system (Vacay, Atlas, Collab, Budget, Packing). Productivity: Tools: http://[IP]:[PORT:3000] - https://raw.githubusercontent.com/mauriceboe/TREK/main/unraid-template.xml - https://raw.githubusercontent.com/mauriceboe/TREK/main/docs/trek-icon.png + https://raw.githubusercontent.com/iiketrek/TREK/main/unraid-template.xml + https://raw.githubusercontent.com/iiketrek/TREK/main/docs/trek-icon.png Support TREK development diff --git a/wiki/Admin-GitHub-Releases.md b/wiki/Admin-GitHub-Releases.md index fc2196f26..76ae705c3 100644 --- a/wiki/Admin-GitHub-Releases.md +++ b/wiki/Admin-GitHub-Releases.md @@ -21,7 +21,7 @@ Six cards at the top of the tab link to external resources: ## Release timeline -Below the support cards, a chronological timeline lists GitHub releases for the `mauriceboe/TREK` repository. Each entry shows: +Below the support cards, a chronological timeline lists GitHub releases for the `iiketrek/TREK` repository. Each entry shows: - **Version tag** (e.g., `v2.9.14`) - A **Latest** badge on the first (most recent) entry in the displayed list diff --git a/wiki/Development-environment.md b/wiki/Development-environment.md index d9d2c945d..79f57499b 100644 --- a/wiki/Development-environment.md +++ b/wiki/Development-environment.md @@ -30,7 +30,7 @@ cd TREK Add the original repository as `upstream` so you can pull in future updates: ```bash -git remote add upstream git@github.com:mauriceboe/TREK.git +git remote add upstream git@github.com:iiketrek/TREK.git ``` You should now have two remotes: @@ -38,7 +38,7 @@ You should now have two remotes: | Remote | URL | Purpose | |------------|----------------------------------------------|--------------------------------| | `origin` | `git@github.com:your-username/TREK.git` | Your fork — push changes here | -| `upstream` | `git@github.com:mauriceboe/TREK.git` | Main repo — pull updates from here | +| `upstream` | `git@github.com:iiketrek/TREK.git` | Main repo — pull updates from here | --- @@ -192,7 +192,7 @@ git push origin fix/my-changes git push origin dev ``` -Then open a Pull Request from your fork to `mauriceboe/TREK` targeting the `dev` branch. If your PR only modifies files under `wiki/`, it is exempt from branch enforcement and may target any branch. +Then open a Pull Request from your fork to `iiketrek/TREK` targeting the `dev` branch. If your PR only modifies files under `wiki/`, it is exempt from branch enforcement and may target any branch. --- diff --git a/wiki/Environment-Variables.md b/wiki/Environment-Variables.md index 61811e585..3f9e79441 100644 --- a/wiki/Environment-Variables.md +++ b/wiki/Environment-Variables.md @@ -294,7 +294,7 @@ The plugin system is **on by default**. The runtime and the Admin → Plugins pa | `TREK_PLUGINS_ENABLED` | Master switch for the plugin system. Enabled unless set to `false` (also accepts `0`, `off`, `no`, case-insensitive). Turning it off is a kill switch — installed plugins stay on disk but nothing runs. | enabled | | `TREK_PLUGINS_DIR` | Directory where installed plugin **code** is stored. Persist it as a volume if you use plugins. | `/plugins` | | `TREK_PLUGINS_DATA_DIR` | Directory for each plugin's own **data** (its private SQLite file). Kept separate from the code tree; persist it as a volume too. | `/plugins-data` | -| `TREK_PLUGIN_REGISTRY_URL` | Override the plugin registry index the *Discover* tab browses. Point it at your own fork or mirror of the registry. | `https://raw.githubusercontent.com/mauriceboe/TREK-Plugins/main/dist/index.json` | +| `TREK_PLUGIN_REGISTRY_URL` | Override the plugin registry index the *Discover* tab browses. Point it at your own fork or mirror of the registry. | `https://raw.githubusercontent.com/iiketrek/TREK-Plugins/main/dist/index.json` | | `TREK_PLUGIN_MAX_RSS_MB` | Per-plugin memory ceiling in MB. A plugin process that exceeds it is stopped. | `300` | | `TREK_PLUGIN_PERMISSIONS` | Set to `off` to opt **out** of the Node.js OS-level permission sandbox for plugin child processes (not recommended). Any other value keeps the sandbox on. | `on` | | `TREK_PLUGIN_ALLOW_PRIVATE_EGRESS`| Set to `on` to let a plugin's declared outbound hosts resolve to private/internal addresses (e.g. a service on your LAN). By default connections to private, loopback, link-local and metadata addresses are refused. | off (private egress blocked) | diff --git a/wiki/Install-Docker-Compose.md b/wiki/Install-Docker-Compose.md index 3fe6f7034..5719fa9ba 100644 --- a/wiki/Install-Docker-Compose.md +++ b/wiki/Install-Docker-Compose.md @@ -66,15 +66,15 @@ Three tag strategies are available: | Tag | Example | Behavior | |---|---|---| -| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions | -| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version | -| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes | +| `latest` | `iiketrek/TREK:latest` | Always the newest release across all major versions | +| Major version | `iiketrek/TREK:3` | Latest release pinned to that major version | +| Full version | `iiketrek/TREK:3.0.15` | Exact release; never changes | The compose file above uses `latest`. To pin, change the `image:` line: ```yaml -image: mauriceboe/trek:3 # track major version 3 -image: mauriceboe/trek:3.0.15 # pin to exact release +image: iiketrek/TREK:3 # track major version 3 +image: iiketrek/TREK:3.0.15 # pin to exact release ``` ## Start TREK diff --git a/wiki/Install-Docker.md b/wiki/Install-Docker.md index 6443ebc4a..3c56c1679 100644 --- a/wiki/Install-Docker.md +++ b/wiki/Install-Docker.md @@ -12,7 +12,7 @@ docker run -d \ -v ./uploads:/app/uploads \ -e ENCRYPTION_KEY= \ --restart unless-stopped \ - mauriceboe/trek:latest + iiketrek/TREK:latest ``` `ENCRYPTION_KEY` is strongly recommended but not strictly required. If omitted, a key is auto-generated on first start and persisted to `data/.encryption_key`. Setting it explicitly means you can recreate the container from scratch (e.g. on a new host) without losing access to stored encrypted data (API keys, SMTP credentials, OIDC secrets, MFA secrets). @@ -38,11 +38,11 @@ See [Environment-Variables](Environment-Variables) for the full list. | Tag | Example | Behavior | |---|---|---| -| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions | -| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version | -| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes | +| `latest` | `iiketrek/TREK:latest` | Always the newest release across all major versions | +| Major version | `iiketrek/TREK:3` | Latest release pinned to that major version | +| Full version | `iiketrek/TREK:3.0.15` | Exact release; never changes | -Replace `mauriceboe/trek:latest` in the run command with your chosen tag to pin to a major version or exact release. +Replace `iiketrek/TREK:latest` in the run command with your chosen tag to pin to a major version or exact release. ## Volume Reference @@ -65,7 +65,7 @@ docker run -d \ -v trek_uploads:/app/uploads \ -e ENCRYPTION_KEY= \ --restart unless-stopped \ - mauriceboe/trek:latest + iiketrek/TREK:latest ``` Docker creates `trek_data` and `trek_uploads` automatically on first run. Named volumes are easier to manage with `docker volume` commands and work better in some NAS or container-management environments. diff --git a/wiki/Install-Helm.md b/wiki/Install-Helm.md index c164564a0..6e04ab125 100644 --- a/wiki/Install-Helm.md +++ b/wiki/Install-Helm.md @@ -72,7 +72,7 @@ helm install trek trek/trek \ ```yaml image: - repository: mauriceboe/trek + repository: iiketrek/TREK # tag: latest # defaults to the chart's appVersion pullPolicy: IfNotPresent diff --git a/wiki/Install-Portainer.md b/wiki/Install-Portainer.md index d9ef23de1..06e48d487 100644 --- a/wiki/Install-Portainer.md +++ b/wiki/Install-Portainer.md @@ -40,9 +40,9 @@ Three tag strategies are available: | Tag | Example | Behavior | |---|---|---| -| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions | -| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version | -| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes | +| `latest` | `iiketrek/TREK:latest` | Always the newest release across all major versions | +| Major version | `iiketrek/TREK:3` | Latest release pinned to that major version | +| Full version | `iiketrek/TREK:3.0.15` | Exact release; never changes | Use `latest` or a major-version tag (e.g. `3`) if you want automatic updates on redeploy. Use a full version tag (e.g. `3.0.15`) if you want explicit control over which release runs. diff --git a/wiki/Install-Unraid.md b/wiki/Install-Unraid.md index e98a699eb..2479f8caa 100644 --- a/wiki/Install-Unraid.md +++ b/wiki/Install-Unraid.md @@ -17,7 +17,7 @@ Docker must be enabled in Unraid (**Settings → Docker → Enable Docker: Yes** If the app does not appear, you can install directly from the template URL. In **Docker → Add Container**, paste the template URL: ``` -https://raw.githubusercontent.com/mauriceboe/TREK/main/unraid-template.xml +https://raw.githubusercontent.com/iiketrek/TREK/main/unraid-template.xml ``` ## Template Fields diff --git a/wiki/Quick-Start.md b/wiki/Quick-Start.md index 17e7b970d..ef6bed110 100644 --- a/wiki/Quick-Start.md +++ b/wiki/Quick-Start.md @@ -21,7 +21,7 @@ ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d \ -v ./data:/app/data \ -v ./uploads:/app/uploads \ --restart unless-stopped \ - mauriceboe/trek:latest + iiketrek/TREK:latest ``` **Flag breakdown:** diff --git a/wiki/Updating.md b/wiki/Updating.md index 32f7733d9..feaba4916 100644 --- a/wiki/Updating.md +++ b/wiki/Updating.md @@ -10,9 +10,9 @@ Back up your data first. Go to Admin Panel → Backups and create a manual backu | Tag | Example | Behavior | |---|---|---| -| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions | -| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version | -| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes | +| `latest` | `iiketrek/TREK:latest` | Always the newest release across all major versions | +| Major version | `iiketrek/TREK:3` | Latest release pinned to that major version | +| Full version | `iiketrek/TREK:3.0.15` | Exact release; never changes | Use `latest` or a major-version tag if you want updates on each redeploy. Use a full version tag for explicit control — update by changing the tag, not by re-pulling. @@ -39,14 +39,14 @@ docker compose up -d If you started TREK with `docker run`, pull the new image and replace the container: ```bash -docker pull mauriceboe/trek +docker pull iiketrek/TREK docker rm -f trek docker run -d --name trek -p 3000:3000 \ -v ./data:/app/data \ -v ./uploads:/app/uploads \ -e ENCRYPTION_KEY= \ --restart unless-stopped \ - mauriceboe/trek + iiketrek/TREK ``` > **Tip:** Not sure which volume paths you used? Check before removing: