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
-
+
@@ -31,9 +31,9 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
-
-
-
+
+
+
@@ -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: