From 8ac3d517fe04f9429abc20576c12ea8221b32817 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 20 Feb 2026 20:37:21 -0800 Subject: [PATCH] feat(jellyfin): move auth to env and stored session --- ...-from-config-use-env-and-stored-session.md | 32 +++++++ docs/configuration.md | 9 +- docs/jellyfin-integration.md | 6 +- docs/public/config.example.jsonc | 8 +- docs/subagents/INDEX.md | 4 +- ...yfin-secret-store-20260220T101428Z-om4z.md | 55 ++++++++++- docs/subagents/collaboration.md | 4 + src/config/definitions.ts | 6 +- src/config/service.ts | 2 - src/core/services/jellyfin-token-store.ts | 96 +++++++++++++------ src/main.ts | 11 +-- src/main/runtime/jellyfin-cli-auth.test.ts | 39 +++++--- src/main/runtime/jellyfin-cli-auth.ts | 24 ++--- .../runtime/jellyfin-cli-main-deps.test.ts | 8 +- src/main/runtime/jellyfin-cli-main-deps.ts | 4 +- .../jellyfin-client-info-main-deps.test.ts | 6 +- .../runtime/jellyfin-client-info-main-deps.ts | 3 +- src/main/runtime/jellyfin-client-info.test.ts | 54 ++++++++++- src/main/runtime/jellyfin-client-info.ts | 37 ++++--- src/main/runtime/jellyfin-remote-commands.ts | 10 +- .../jellyfin-remote-session-lifecycle.ts | 4 +- .../jellyfin-setup-window-main-deps.test.ts | 4 +- .../jellyfin-setup-window-main-deps.ts | 2 +- .../runtime/jellyfin-setup-window.test.ts | 25 ++++- src/main/runtime/jellyfin-setup-window.ts | 11 ++- src/types.ts | 4 - 26 files changed, 336 insertions(+), 132 deletions(-) create mode 100644 backlog/tasks/task-93 - Remove-Jellyfin-token-userId-from-config-use-env-and-stored-session.md diff --git a/backlog/tasks/task-93 - Remove-Jellyfin-token-userId-from-config-use-env-and-stored-session.md b/backlog/tasks/task-93 - Remove-Jellyfin-token-userId-from-config-use-env-and-stored-session.md new file mode 100644 index 0000000..2d70799 --- /dev/null +++ b/backlog/tasks/task-93 - Remove-Jellyfin-token-userId-from-config-use-env-and-stored-session.md @@ -0,0 +1,32 @@ +--- +id: TASK-93 +title: Remove Jellyfin token/userId from config; use env override and stored session +status: Done +assignee: [] +created_date: '2026-02-21 04:20' +updated_date: '2026-02-21 04:27' +labels: [] +dependencies: [] +priority: high +--- + +## Description + + +Remove `jellyfin.accessToken` and `jellyfin.userId` from editable config surface. Resolve Jellyfin auth via env override (`SUBMINER_JELLYFIN_ACCESS_TOKEN`, optional `SUBMINER_JELLYFIN_USER_ID`) and locally stored Jellyfin auth session payload persisted by login/setup. + + +## Acceptance Criteria + +- [ ] #1 `jellyfin.accessToken` and `jellyfin.userId` no longer parsed/documented as config fields. +- [ ] #2 Jellyfin login/setup persists token + userId in local encrypted session store. +- [ ] #3 Jellyfin runtime resolves auth from env override first, then stored session payload. +- [ ] #4 Jellyfin logout clears stored session payload. +- [ ] #5 Runtime tests cover env override + stored session fallback behavior. + + +## Implementation Notes + + +Implemented: evolved `jellyfin-token-store` to encrypted session payload store (`accessToken` + `userId`), updated Jellyfin resolver precedence to env-first (`SUBMINER_JELLYFIN_ACCESS_TOKEN`, optional `SUBMINER_JELLYFIN_USER_ID`) then stored session fallback, removed `jellyfin.accessToken`/`jellyfin.userId` from config defaults+parsing+public docs/example, and updated CLI/setup auth wiring to persist/clear session store while keeping `serverUrl`/`username` config behavior. + diff --git a/docs/configuration.md b/docs/configuration.md index d4cdc53..5e52924 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -477,8 +477,6 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner "enabled": true, "serverUrl": "http://127.0.0.1:8096", "username": "", - "accessToken": "", - "userId": "", "remoteControlEnabled": true, "remoteControlAutoConnect": true, "autoAnnounce": false, @@ -496,8 +494,6 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner | `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) | | `serverUrl` | string (URL) | Jellyfin server base URL | | `username` | string | Default username used by `--jellyfin-login` | -| `accessToken` | string | Optional explicit Jellyfin access token override; leave empty to use stored local token | -| `userId` | string | Jellyfin user id bound to token/session | | `deviceId` | string | Client device id sent in auth headers (default: `subminer`) | | `clientName` | string | Client name sent in auth headers (default: `SubMiner`) | | `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) | @@ -512,7 +508,10 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner | `directPlayContainers` | string[] | Container allowlist for direct play decisions | | `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) | -When `jellyfin.accessToken` is empty, SubMiner uses the locally stored encrypted token saved from Jellyfin login/setup. +Jellyfin auth resolution order: + +1. `SUBMINER_JELLYFIN_ACCESS_TOKEN` (and optional `SUBMINER_JELLYFIN_USER_ID`) environment overrides. +2. Locally stored encrypted Jellyfin session payload saved by login/setup (`accessToken` + `userId`). Jellyfin direct app CLI commands (`SubMiner.AppImage ...`): diff --git a/docs/jellyfin-integration.md b/docs/jellyfin-integration.md index f910d90..e295020 100644 --- a/docs/jellyfin-integration.md +++ b/docs/jellyfin-integration.md @@ -109,7 +109,7 @@ remote playback target in Jellyfin's cast-to-device menu. ### Requirements - `jellyfin.enabled=true` -- valid `jellyfin.serverUrl`, `jellyfin.accessToken`, and `jellyfin.userId` +- valid `jellyfin.serverUrl` and Jellyfin auth session (env override or stored login session) - `jellyfin.remoteControlEnabled=true` (default) - `jellyfin.remoteControlAutoConnect=true` (default) - `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect) @@ -149,8 +149,8 @@ User-visible errors are shown through CLI logs and mpv OSD for: ## Security Notes and Limitations -- Jellyfin access token is stored in local encrypted token storage after login/setup. -- `jellyfin.accessToken` remains as an optional explicit override in `config.jsonc`. +- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup. +- Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`. - Treat both token storage and config files as secrets and avoid committing them. - Password is used only for login and is not stored. - Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags. diff --git a/docs/public/config.example.jsonc b/docs/public/config.example.jsonc index fa2ea12..f4a38da 100644 --- a/docs/public/config.example.jsonc +++ b/docs/public/config.example.jsonc @@ -273,15 +273,15 @@ // ========================================== // Jellyfin // Optional Jellyfin integration for auth, browsing, and playback launch. - // Access token is stored in local encrypted token storage after login/setup. - // jellyfin.accessToken below remains an optional explicit override. + // Auth session (access token + user id) is stored in local encrypted storage after login/setup. + // Optional environment overrides: + // SUBMINER_JELLYFIN_ACCESS_TOKEN + // SUBMINER_JELLYFIN_USER_ID // ========================================== "jellyfin": { "enabled": false, // Enable optional Jellyfin integration and CLI control commands. Values: true | false "serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096). "username": "", // Default Jellyfin username used during CLI login. - "accessToken": "", // Optional explicit access token override; leave empty to use stored local token. - "userId": "", // User id setting. "deviceId": "subminer", // Device id setting. "clientName": "SubMiner", // Client name setting. "clientVersion": "0.1.0", // Client version setting. diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index fbb9290..40480bf 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -5,6 +5,7 @@ Read first. Keep concise. | agent_id | alias | mission | status | file | last_update_utc | | ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- | | `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` | +| `codex-frequency-dup-log-20260221T042815Z-r4k1` | `codex-frequency-dup-log` | `Reduce frequency dictionary duplicate-term startup log spam` | `completed` | `docs/subagents/agents/codex-frequency-dup-log-20260221T042815Z-r4k1.md` | `2026-02-21T04:32:40Z` | | `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | | `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` | | `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | @@ -20,9 +21,10 @@ Read first. Keep concise. | `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` | | `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` | | `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` | -| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` | +| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Move Jellyfin token/userId out of config into env override + stored session payload` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-21T04:27:24Z` | | `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` | | `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` | | `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-20T11:48:28Z` | | `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` | | `codex-overlay-whitespace-newline-20260221T040705Z-aw2j` | `codex-overlay-whitespace-newline` | `Fix visible overlay whitespace/newline token rendering bug with TDD regression coverage` | `completed` | `docs/subagents/agents/codex-overlay-whitespace-newline-20260221T040705Z-aw2j.md` | `2026-02-21T04:18:16Z` | +| `codex-duplicate-kiku-20260221T043006Z-5vkz` | `codex-duplicate-kiku` | `Fix Kiku duplicate-card detection/grouping regression for Yomitan duplicate-marked + N+1-highlighted cards` | `completed` | `docs/subagents/agents/codex-duplicate-kiku-20260221T043006Z-5vkz.md` | `2026-02-21T04:33:17Z` | diff --git a/docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md b/docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md index ce688ed..42d4c27 100644 --- a/docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md +++ b/docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md @@ -1,9 +1,9 @@ # codex-jellyfin-secret-store-20260220T101428Z-om4z - alias: `codex-jellyfin-secret-store` -- mission: `Verify whether Jellyfin token can use same secret-store path as AniList token` +- mission: `Move Jellyfin token/userId out of config into env override + stored session payload` - status: `completed` -- last_update_utc: `2026-02-20T10:22:45Z` +- last_update_utc: `2026-02-21T04:27:24Z` ## Intent @@ -63,3 +63,54 @@ - `node --test dist/main/runtime/jellyfin-client-info.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-client-info-main-deps.test.js dist/main/runtime/jellyfin-cli-main-deps.test.js dist/main/runtime/jellyfin-setup-window-main-deps.test.js` - `bun run test:fast` - `bun run docs:build` + +## Next Intent (2026-02-21) + +- remove `jellyfin.accessToken` config path; add env override (`SUBMINER_JELLYFIN_ACCESS_TOKEN`) +- move `jellyfin.userId` out of config into stored Jellyfin auth session payload +- keep login/logout semantics via setup/CLI commands +- add/update runtime tests first, then implementation + docs + +## 2026-02-21 Outcome + +- removed `jellyfin.accessToken` + `jellyfin.userId` from config defaults/parsing/docs surface +- added env override precedence in resolver: + - `SUBMINER_JELLYFIN_ACCESS_TOKEN` + - `SUBMINER_JELLYFIN_USER_ID` (optional; falls back to stored userId if session exists) +- moved Jellyfin store from token-only to auth-session payload (`accessToken` + `userId`) +- updated login/setup/logout flows to save/clear stored session payload + +## 2026-02-21 Files Touched + +- `src/core/services/jellyfin-token-store.ts` +- `src/main.ts` +- `src/main/runtime/jellyfin-client-info.ts` +- `src/main/runtime/jellyfin-client-info-main-deps.ts` +- `src/main/runtime/jellyfin-cli-auth.ts` +- `src/main/runtime/jellyfin-cli-main-deps.ts` +- `src/main/runtime/jellyfin-setup-window.ts` +- `src/main/runtime/jellyfin-setup-window-main-deps.ts` +- `src/main/runtime/jellyfin-remote-commands.ts` +- `src/main/runtime/jellyfin-remote-session-lifecycle.ts` +- `src/config/definitions.ts` +- `src/config/service.ts` +- `src/types.ts` +- `src/main/runtime/jellyfin-client-info.test.ts` +- `src/main/runtime/jellyfin-client-info-main-deps.test.ts` +- `src/main/runtime/jellyfin-cli-auth.test.ts` +- `src/main/runtime/jellyfin-cli-main-deps.test.ts` +- `src/main/runtime/jellyfin-setup-window.test.ts` +- `src/main/runtime/jellyfin-setup-window-main-deps.test.ts` +- `docs/configuration.md` +- `docs/jellyfin-integration.md` +- `docs/public/config.example.jsonc` +- `backlog/tasks/task-93 - Remove-Jellyfin-token-userId-from-config-use-env-and-stored-session.md` + +## 2026-02-21 Verification + +- `bun run tsc --noEmit` +- `bun test src/main/runtime/jellyfin-client-info.test.ts src/main/runtime/jellyfin-cli-auth.test.ts src/main/runtime/jellyfin-setup-window.test.ts src/main/runtime/jellyfin-client-info-main-deps.test.ts src/main/runtime/jellyfin-cli-main-deps.test.ts src/main/runtime/jellyfin-setup-window-main-deps.test.ts src/main/runtime/jellyfin-remote-session-lifecycle.test.ts src/main/runtime/jellyfin-command-dispatch.test.ts src/main/runtime/jellyfin-remote-commands.test.ts src/config/config.test.ts` (73 pass) + +## Blockers + +- full `bun run build` currently blocked by local disk-full (`ENOSPC`) in workspace output path diff --git a/docs/subagents/collaboration.md b/docs/subagents/collaboration.md index c7b2aa0..0ea3cc2 100644 --- a/docs/subagents/collaboration.md +++ b/docs/subagents/collaboration.md @@ -20,3 +20,7 @@ Shared notes. Append-only. - [2026-02-21T04:09:02Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] completed: whitespace-only token surfaces no longer become token segments; non-preserve mode now flattens token newlines to spaces and renders whitespace as text nodes; added regression test in renderer suite. - [2026-02-21T04:14:30Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] preserve-line-breaks follow-up: when token surface mismatches source (e.g., `1` vs `1`), alignment now skips unmatched token instead of appending both source tail + token; fixes duplicated no-break line artifact. - [2026-02-21T04:18:16Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] follow-up fix: non-token fallback now honors preserveLineBreaks flag by collapsing line breaks when disabled; prevents visible multi-line -> single-line transition while tokenized payload arrives. +- [2026-02-21T04:18:58Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] overlap note: follow-up Jellyfin auth refactor touching `src/main.ts`, `src/main/runtime/jellyfin-*`, and config/docs to remove config token/userId fields in favor of env+stored session payload. +- [2026-02-21T04:27:24Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] completed TASK-93: removed Jellyfin accessToken/userId config fields; resolver now uses env-first (`SUBMINER_JELLYFIN_ACCESS_TOKEN` + optional `SUBMINER_JELLYFIN_USER_ID`) then stored encrypted session payload; login/setup save session and logout clears session. +- [2026-02-21T04:30:06Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] investigating Kiku duplicate grouping regression; expecting touches in `src/anki-integration/duplicate.ts` and duplicate-detection tests only. +- [2026-02-21T04:33:17Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] completed TASK-94: duplicate check now resolves `word`/`expression` alias fields when validating candidate notes; added regression test `src/anki-integration/duplicate.test.ts`; targeted build + duplicate/anki-integration tests passed. diff --git a/src/config/definitions.ts b/src/config/definitions.ts index 4724160..c397d20 100644 --- a/src/config/definitions.ts +++ b/src/config/definitions.ts @@ -220,8 +220,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = { enabled: false, serverUrl: '', username: '', - accessToken: '', - userId: '', deviceId: 'subminer', clientName: 'SubMiner', clientVersion: '0.1.0', @@ -807,8 +805,8 @@ export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [ title: 'Jellyfin', description: [ 'Optional Jellyfin integration for auth, browsing, and playback launch.', - 'Access token is stored in local encrypted token storage after login/setup.', - 'jellyfin.accessToken remains an optional explicit override in config.', + 'Auth session (access token + user id) is stored in local encrypted storage after login/setup.', + 'Optional env overrides: SUBMINER_JELLYFIN_ACCESS_TOKEN and SUBMINER_JELLYFIN_USER_ID.', ], key: 'jellyfin', }, diff --git a/src/config/service.ts b/src/config/service.ts index 3a6b3e9..34311f0 100644 --- a/src/config/service.ts +++ b/src/config/service.ts @@ -519,8 +519,6 @@ export class ConfigService { const stringKeys = [ 'serverUrl', 'username', - 'accessToken', - 'userId', 'deviceId', 'clientName', 'clientVersion', diff --git a/src/core/services/jellyfin-token-store.ts b/src/core/services/jellyfin-token-store.ts index 828120d..ce9e38f 100644 --- a/src/core/services/jellyfin-token-store.ts +++ b/src/core/services/jellyfin-token-store.ts @@ -2,16 +2,27 @@ import * as fs from 'fs'; import * as path from 'path'; import { safeStorage } from 'electron'; -interface PersistedTokenPayload { +interface PersistedSessionPayload { + encryptedSession?: string; + plaintextSession?: { + accessToken?: string; + userId?: string; + }; + // Legacy payload fields (token only). encryptedToken?: string; plaintextToken?: string; updatedAt?: number; } +export interface JellyfinStoredSession { + accessToken: string; + userId: string; +} + export interface JellyfinTokenStore { - loadToken: () => string | null; - saveToken: (token: string) => void; - clearToken: () => void; + loadSession: () => JellyfinStoredSession | null; + saveSession: (session: JellyfinStoredSession) => void; + clearSession: () => void; } function ensureDirectory(filePath: string): void { @@ -21,7 +32,7 @@ function ensureDirectory(filePath: string): void { } } -function writePayload(filePath: string, payload: PersistedTokenPayload): void { +function writePayload(filePath: string, payload: PersistedSessionPayload): void { ensureDirectory(filePath); fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8'); } @@ -35,65 +46,94 @@ export function createJellyfinTokenStore( }, ): JellyfinTokenStore { return { - loadToken(): string | null { + loadSession(): JellyfinStoredSession | null { if (!fs.existsSync(filePath)) { return null; } try { const raw = fs.readFileSync(filePath, 'utf-8'); - const parsed = JSON.parse(raw) as PersistedTokenPayload; - if (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) { - const encrypted = Buffer.from(parsed.encryptedToken, 'base64'); + const parsed = JSON.parse(raw) as PersistedSessionPayload; + + if (typeof parsed.encryptedSession === 'string' && parsed.encryptedSession.length > 0) { + const encrypted = Buffer.from(parsed.encryptedSession, 'base64'); if (!safeStorage.isEncryptionAvailable()) { - logger.warn('Jellyfin token encryption is not available on this system.'); + logger.warn('Jellyfin session encryption is not available on this system.'); return null; } const decrypted = safeStorage.decryptString(encrypted).trim(); - return decrypted.length > 0 ? decrypted : null; + const session = JSON.parse(decrypted) as Partial; + const accessToken = typeof session.accessToken === 'string' ? session.accessToken.trim() : ''; + const userId = typeof session.userId === 'string' ? session.userId.trim() : ''; + if (!accessToken || !userId) return null; + return { accessToken, userId }; } - if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) { - const plaintext = parsed.plaintextToken.trim(); - this.saveToken(plaintext); - return plaintext; + + if (parsed.plaintextSession && typeof parsed.plaintextSession === 'object') { + const accessToken = + typeof parsed.plaintextSession.accessToken === 'string' + ? parsed.plaintextSession.accessToken.trim() + : ''; + const userId = + typeof parsed.plaintextSession.userId === 'string' + ? parsed.plaintextSession.userId.trim() + : ''; + if (accessToken && userId) { + const session = { accessToken, userId }; + this.saveSession(session); + return session; + } + } + + if ( + (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) || + (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) + ) { + logger.warn('Ignoring legacy Jellyfin token-only store payload because userId is missing.'); } } catch (error) { - logger.error('Failed to read Jellyfin token store.', error); + logger.error('Failed to read Jellyfin session store.', error); } return null; }, - saveToken(token: string): void { - const trimmed = token.trim(); - if (trimmed.length === 0) { - this.clearToken(); + saveSession(session: JellyfinStoredSession): void { + const accessToken = session.accessToken.trim(); + const userId = session.userId.trim(); + if (!accessToken || !userId) { + this.clearSession(); return; } try { if (!safeStorage.isEncryptionAvailable()) { - logger.warn('Jellyfin token encryption unavailable; storing token in plaintext fallback.'); + logger.warn( + 'Jellyfin session encryption unavailable; storing session in plaintext fallback.', + ); writePayload(filePath, { - plaintextToken: trimmed, + plaintextSession: { + accessToken, + userId, + }, updatedAt: Date.now(), }); return; } - const encrypted = safeStorage.encryptString(trimmed); + const encrypted = safeStorage.encryptString(JSON.stringify({ accessToken, userId })); writePayload(filePath, { - encryptedToken: encrypted.toString('base64'), + encryptedSession: encrypted.toString('base64'), updatedAt: Date.now(), }); } catch (error) { - logger.error('Failed to persist Jellyfin token.', error); + logger.error('Failed to persist Jellyfin session.', error); } }, - clearToken(): void { + clearSession(): void { if (!fs.existsSync(filePath)) return; try { fs.unlinkSync(filePath); - logger.info('Cleared stored Jellyfin token.'); + logger.info('Cleared stored Jellyfin session.'); } catch (error) { - logger.error('Failed to clear stored Jellyfin token.', error); + logger.error('Failed to clear stored Jellyfin session.', error); } }, }; diff --git a/src/main.ts b/src/main.ts index 5022f4c..dbd6d86 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1166,7 +1166,8 @@ function getResolvedConfig() { const buildGetResolvedJellyfinConfigMainDepsHandler = createBuildGetResolvedJellyfinConfigMainDepsHandler({ getResolvedConfig: () => getResolvedConfig(), - loadStoredToken: () => jellyfinTokenStore.loadToken(), + loadStoredSession: () => jellyfinTokenStore.loadSession(), + getEnv: (name: string) => process.env[name], }); const getResolvedJellyfinConfigMainDeps = buildGetResolvedJellyfinConfigMainDepsHandler(); @@ -1322,8 +1323,8 @@ const buildHandleJellyfinAuthCommandsMainDepsHandler = }, authenticateWithPassword: (serverUrl, username, password, clientInfo) => authenticateWithPasswordRuntime(serverUrl, username, password, clientInfo), - saveStoredToken: (token) => jellyfinTokenStore.saveToken(token), - clearStoredToken: () => jellyfinTokenStore.clearToken(), + saveStoredSession: (session) => jellyfinTokenStore.saveSession(session), + clearStoredSession: () => jellyfinTokenStore.clearSession(), logInfo: (message) => logger.info(message), }); const handleJellyfinAuthCommandsMainDeps = @@ -1586,15 +1587,13 @@ const buildOpenJellyfinSetupWindowMainDepsHandler = authenticateWithPassword: (server, username, password, clientInfo) => authenticateWithPasswordRuntime(server, username, password, clientInfo), getJellyfinClientInfo: () => getJellyfinClientInfo(), - saveStoredToken: (token) => jellyfinTokenStore.saveToken(token), + saveStoredSession: (session) => jellyfinTokenStore.saveSession(session), patchJellyfinConfig: (session) => { configService.patchRawConfig({ jellyfin: { enabled: true, serverUrl: session.serverUrl, username: session.username, - accessToken: '', - userId: session.userId, }, }); }, diff --git a/src/main/runtime/jellyfin-cli-auth.test.ts b/src/main/runtime/jellyfin-cli-auth.test.ts index 358a675..6a59c7c 100644 --- a/src/main/runtime/jellyfin-cli-auth.test.ts +++ b/src/main/runtime/jellyfin-cli-auth.test.ts @@ -6,8 +6,8 @@ test('jellyfin auth handler processes logout', async () => { const calls: string[] = []; const handleAuth = createHandleJellyfinAuthCommands({ patchRawConfig: () => calls.push('patch'), - saveStoredToken: () => calls.push('save'), - clearStoredToken: () => calls.push('clear'), + saveStoredSession: () => calls.push('save'), + clearStoredSession: () => calls.push('clear'), authenticateWithPassword: async () => { throw new Error('should not authenticate'); }, @@ -24,8 +24,6 @@ test('jellyfin auth handler processes logout', async () => { jellyfinConfig: { serverUrl: '', username: '', - accessToken: '', - userId: '', }, serverUrl: 'http://localhost', clientInfo: { @@ -41,10 +39,18 @@ test('jellyfin auth handler processes logout', async () => { test('jellyfin auth handler processes login', async () => { const calls: string[] = []; + let patchPayload: unknown = null; + let storedSession: unknown = null; const handleAuth = createHandleJellyfinAuthCommands({ - patchRawConfig: () => calls.push('patch'), - saveStoredToken: () => calls.push('save'), - clearStoredToken: () => calls.push('clear'), + patchRawConfig: (patch) => { + patchPayload = patch; + calls.push('patch'); + }, + saveStoredSession: (session) => { + storedSession = session; + calls.push('save'); + }, + clearStoredSession: () => calls.push('clear'), authenticateWithPassword: async () => ({ serverUrl: 'http://localhost', username: 'user', @@ -64,8 +70,6 @@ test('jellyfin auth handler processes login', async () => { jellyfinConfig: { serverUrl: '', username: '', - accessToken: '', - userId: '', }, serverUrl: 'http://localhost', clientInfo: { @@ -78,14 +82,25 @@ test('jellyfin auth handler processes login', async () => { assert.equal(handled, true); assert.ok(calls.includes('save')); assert.ok(calls.includes('patch')); + assert.deepEqual(storedSession, { accessToken: 'token', userId: 'uid' }); + assert.deepEqual(patchPayload, { + jellyfin: { + enabled: true, + serverUrl: 'http://localhost', + username: 'user', + deviceId: 'd1', + clientName: 'SubMiner', + clientVersion: '1.0', + }, + }); assert.ok(calls.some((entry) => entry.includes('Jellyfin login succeeded'))); }); test('jellyfin auth handler no-ops when no auth command', async () => { const handleAuth = createHandleJellyfinAuthCommands({ patchRawConfig: () => {}, - saveStoredToken: () => {}, - clearStoredToken: () => {}, + saveStoredSession: () => {}, + clearStoredSession: () => {}, authenticateWithPassword: async () => ({ serverUrl: '', username: '', @@ -105,8 +120,6 @@ test('jellyfin auth handler no-ops when no auth command', async () => { jellyfinConfig: { serverUrl: '', username: '', - accessToken: '', - userId: '', }, serverUrl: 'http://localhost', clientInfo: { diff --git a/src/main/runtime/jellyfin-cli-auth.ts b/src/main/runtime/jellyfin-cli-auth.ts index 9e823c6..5f5f8aa 100644 --- a/src/main/runtime/jellyfin-cli-auth.ts +++ b/src/main/runtime/jellyfin-cli-auth.ts @@ -3,8 +3,6 @@ import type { CliArgs } from '../../cli/args'; type JellyfinConfig = { serverUrl: string; username: string; - accessToken: string; - userId: string; }; type JellyfinClientInfo = { @@ -26,8 +24,6 @@ export function createHandleJellyfinAuthCommands(deps: { enabled: boolean; serverUrl: string; username: string; - accessToken: string; - userId: string; deviceId: string; clientName: string; clientVersion: string; @@ -39,8 +35,8 @@ export function createHandleJellyfinAuthCommands(deps: { password: string, clientInfo: JellyfinClientInfo, ) => Promise; - saveStoredToken: (token: string) => void; - clearStoredToken: () => void; + saveStoredSession: (session: { accessToken: string; userId: string }) => void; + clearStoredSession: () => void; logInfo: (message: string) => void; }) { return async (params: { @@ -50,14 +46,11 @@ export function createHandleJellyfinAuthCommands(deps: { clientInfo: JellyfinClientInfo; }): Promise => { if (params.args.jellyfinLogout) { - deps.clearStoredToken(); + deps.clearStoredSession(); deps.patchRawConfig({ - jellyfin: { - accessToken: '', - userId: '', - }, + jellyfin: {}, }); - deps.logInfo('Cleared stored Jellyfin access token.'); + deps.logInfo('Cleared stored Jellyfin auth session.'); return true; } @@ -73,14 +66,15 @@ export function createHandleJellyfinAuthCommands(deps: { password, params.clientInfo, ); - deps.saveStoredToken(session.accessToken); + deps.saveStoredSession({ + accessToken: session.accessToken, + userId: session.userId, + }); deps.patchRawConfig({ jellyfin: { enabled: true, serverUrl: session.serverUrl, username: session.username, - accessToken: '', - userId: session.userId, deviceId: params.clientInfo.deviceId, clientName: params.clientInfo.clientName, clientVersion: params.clientInfo.clientVersion, diff --git a/src/main/runtime/jellyfin-cli-main-deps.test.ts b/src/main/runtime/jellyfin-cli-main-deps.test.ts index 058a330..66f8b08 100644 --- a/src/main/runtime/jellyfin-cli-main-deps.test.ts +++ b/src/main/runtime/jellyfin-cli-main-deps.test.ts @@ -12,8 +12,8 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => { const deps = createBuildHandleJellyfinAuthCommandsMainDepsHandler({ patchRawConfig: () => calls.push('patch'), authenticateWithPassword: async () => ({}) as never, - saveStoredToken: () => calls.push('save'), - clearStoredToken: () => calls.push('clear'), + saveStoredSession: () => calls.push('save'), + clearStoredSession: () => calls.push('clear'), logInfo: (message) => calls.push(`info:${message}`), })(); @@ -23,8 +23,8 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => { clientName: '', clientVersion: '', }); - deps.saveStoredToken('token'); - deps.clearStoredToken(); + deps.saveStoredSession({ accessToken: 'token', userId: 'uid' }); + deps.clearStoredSession(); deps.logInfo('ok'); assert.deepEqual(calls, ['patch', 'save', 'clear', 'info:ok']); }); diff --git a/src/main/runtime/jellyfin-cli-main-deps.ts b/src/main/runtime/jellyfin-cli-main-deps.ts index 6afcfd7..41042a2 100644 --- a/src/main/runtime/jellyfin-cli-main-deps.ts +++ b/src/main/runtime/jellyfin-cli-main-deps.ts @@ -25,8 +25,8 @@ export function createBuildHandleJellyfinAuthCommandsMainDepsHandler( patchRawConfig: (patch) => deps.patchRawConfig(patch), authenticateWithPassword: (serverUrl, username, password, clientInfo) => deps.authenticateWithPassword(serverUrl, username, password, clientInfo), - saveStoredToken: (token) => deps.saveStoredToken(token), - clearStoredToken: () => deps.clearStoredToken(), + saveStoredSession: (session) => deps.saveStoredSession(session), + clearStoredSession: () => deps.clearStoredSession(), logInfo: (message: string) => deps.logInfo(message), }); } diff --git a/src/main/runtime/jellyfin-client-info-main-deps.test.ts b/src/main/runtime/jellyfin-client-info-main-deps.test.ts index 86622e9..8fa9886 100644 --- a/src/main/runtime/jellyfin-client-info-main-deps.test.ts +++ b/src/main/runtime/jellyfin-client-info-main-deps.test.ts @@ -9,10 +9,12 @@ test('get resolved jellyfin config main deps builder maps callbacks', () => { const resolved = { jellyfin: { url: 'https://example.com' } }; const deps = createBuildGetResolvedJellyfinConfigMainDepsHandler({ getResolvedConfig: () => resolved as never, - loadStoredToken: () => 'stored-token', + loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'uid' }), + getEnv: (key: string) => (key === 'TEST' ? 'x' : undefined), })(); assert.equal(deps.getResolvedConfig(), resolved); - assert.equal(deps.loadStoredToken(), 'stored-token'); + assert.deepEqual(deps.loadStoredSession(), { accessToken: 'stored-token', userId: 'uid' }); + assert.equal(deps.getEnv('TEST'), 'x'); }); test('get jellyfin client info main deps builder maps callbacks', () => { diff --git a/src/main/runtime/jellyfin-client-info-main-deps.ts b/src/main/runtime/jellyfin-client-info-main-deps.ts index 6cd5902..2bc1d1e 100644 --- a/src/main/runtime/jellyfin-client-info-main-deps.ts +++ b/src/main/runtime/jellyfin-client-info-main-deps.ts @@ -11,7 +11,8 @@ export function createBuildGetResolvedJellyfinConfigMainDepsHandler( ) { return (): GetResolvedJellyfinConfigMainDeps => ({ getResolvedConfig: () => deps.getResolvedConfig(), - loadStoredToken: () => deps.loadStoredToken(), + loadStoredSession: () => deps.loadStoredSession(), + getEnv: (name: string) => deps.getEnv(name), }); } diff --git a/src/main/runtime/jellyfin-client-info.test.ts b/src/main/runtime/jellyfin-client-info.test.ts index 36736f6..135aa93 100644 --- a/src/main/runtime/jellyfin-client-info.test.ts +++ b/src/main/runtime/jellyfin-client-info.test.ts @@ -9,23 +9,23 @@ test('get resolved jellyfin config returns jellyfin section from resolved config const jellyfin = { url: 'https://jellyfin.local' } as never; const getConfig = createGetResolvedJellyfinConfigHandler({ getResolvedConfig: () => ({ jellyfin } as never), - loadStoredToken: () => null, + loadStoredSession: () => null, + getEnv: () => undefined, }); assert.equal(getConfig(), jellyfin); }); -test('get resolved jellyfin config falls back to stored token when config token is blank', () => { +test('get resolved jellyfin config falls back to stored session when env is unset', () => { const getConfig = createGetResolvedJellyfinConfigHandler({ getResolvedConfig: () => ({ jellyfin: { serverUrl: 'http://localhost:8096', - accessToken: ' ', - userId: 'uid-1', }, }) as never, - loadStoredToken: () => 'stored-token', + loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'uid-1' }), + getEnv: () => undefined, }); assert.deepEqual(getConfig(), { @@ -35,6 +35,50 @@ test('get resolved jellyfin config falls back to stored token when config token }); }); +test('get resolved jellyfin config prefers env token and env user id over stored session', () => { + const getConfig = createGetResolvedJellyfinConfigHandler({ + getResolvedConfig: () => + ({ + jellyfin: { + serverUrl: 'http://localhost:8096', + }, + }) as never, + loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }), + getEnv: (key: string) => + key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' + ? 'env-token' + : key === 'SUBMINER_JELLYFIN_USER_ID' + ? 'env-user' + : undefined, + }); + + assert.deepEqual(getConfig(), { + serverUrl: 'http://localhost:8096', + accessToken: 'env-token', + userId: 'env-user', + }); +}); + +test('get resolved jellyfin config uses stored user id when env token set without env user id', () => { + const getConfig = createGetResolvedJellyfinConfigHandler({ + getResolvedConfig: () => + ({ + jellyfin: { + serverUrl: 'http://localhost:8096', + }, + }) as never, + loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }), + getEnv: (key: string) => + key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined, + }); + + assert.deepEqual(getConfig(), { + serverUrl: 'http://localhost:8096', + accessToken: 'env-token', + userId: 'stored-user', + }); +}); + test('jellyfin client info resolves defaults when fields are missing', () => { const getClientInfo = createGetJellyfinClientInfoHandler({ getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never), diff --git a/src/main/runtime/jellyfin-client-info.ts b/src/main/runtime/jellyfin-client-info.ts index 335fa1d..80ef052 100644 --- a/src/main/runtime/jellyfin-client-info.ts +++ b/src/main/runtime/jellyfin-client-info.ts @@ -1,24 +1,37 @@ export function createGetResolvedJellyfinConfigHandler(deps: { getResolvedConfig: () => { jellyfin: unknown }; - loadStoredToken: () => string | null | undefined; + loadStoredSession: () => { accessToken: string; userId: string } | null | undefined; + getEnv: (name: string) => string | undefined; }) { return () => { const jellyfin = deps.getResolvedConfig().jellyfin as { - accessToken?: string; + userId?: string; [key: string]: unknown; }; - const configToken = jellyfin.accessToken?.trim() ?? ''; - if (configToken.length > 0) { - return jellyfin as never; + + const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? ''; + const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? ''; + const stored = deps.loadStoredSession(); + const storedToken = stored?.accessToken?.trim() ?? ''; + const storedUserId = stored?.userId?.trim() ?? ''; + + if (envToken.length > 0) { + return { + ...jellyfin, + accessToken: envToken, + userId: envUserId || storedUserId || '', + } as never; } - const storedToken = deps.loadStoredToken()?.trim() ?? ''; - if (storedToken.length === 0) { - return jellyfin as never; + + if (storedToken.length > 0 && storedUserId.length > 0) { + return { + ...jellyfin, + accessToken: storedToken, + userId: storedUserId, + } as never; } - return { - ...jellyfin, - accessToken: storedToken, - } as never; + + return jellyfin as never; }; } diff --git a/src/main/runtime/jellyfin-remote-commands.ts b/src/main/runtime/jellyfin-remote-commands.ts index bc30fc1..ec6f777 100644 --- a/src/main/runtime/jellyfin-remote-commands.ts +++ b/src/main/runtime/jellyfin-remote-commands.ts @@ -20,10 +20,10 @@ type JellyfinClientInfo = { }; type JellyfinConfigLike = { - serverUrl: string; - accessToken: string; - userId: string; - username: string; + serverUrl?: string; + accessToken?: string; + userId?: string; + username?: string; }; function asInteger(value: unknown): number | undefined { @@ -39,7 +39,7 @@ export function getConfiguredJellyfinSession(config: JellyfinConfigLike): Jellyf serverUrl: config.serverUrl, accessToken: config.accessToken, userId: config.userId, - username: config.username, + username: config.username || '', }; } diff --git a/src/main/runtime/jellyfin-remote-session-lifecycle.ts b/src/main/runtime/jellyfin-remote-session-lifecycle.ts index 1911f72..c8d6060 100644 --- a/src/main/runtime/jellyfin-remote-session-lifecycle.ts +++ b/src/main/runtime/jellyfin-remote-session-lifecycle.ts @@ -2,8 +2,8 @@ type JellyfinRemoteConfig = { remoteControlEnabled: boolean; remoteControlAutoConnect: boolean; serverUrl: string; - accessToken: string; - userId: string; + accessToken?: string; + userId?: string; deviceId: string; clientName: string; clientVersion: string; diff --git a/src/main/runtime/jellyfin-setup-window-main-deps.test.ts b/src/main/runtime/jellyfin-setup-window-main-deps.test.ts index bce3ef9..aa3107e 100644 --- a/src/main/runtime/jellyfin-setup-window-main-deps.test.ts +++ b/src/main/runtime/jellyfin-setup-window-main-deps.test.ts @@ -17,7 +17,7 @@ test('open jellyfin setup window main deps builder maps callbacks', async () => userId: 'uid', }), getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }), - saveStoredToken: () => calls.push('save'), + saveStoredSession: () => calls.push('save'), patchJellyfinConfig: () => calls.push('patch'), logInfo: (message) => calls.push(`info:${message}`), logError: (message) => calls.push(`error:${message}`), @@ -44,7 +44,7 @@ test('open jellyfin setup window main deps builder maps callbacks', async () => accessToken: 'token', userId: 'uid', }); - deps.saveStoredToken('token'); + deps.saveStoredSession({ accessToken: 'token', userId: 'uid' }); deps.patchJellyfinConfig({ serverUrl: 'http://127.0.0.1:8096', username: 'alice', diff --git a/src/main/runtime/jellyfin-setup-window-main-deps.ts b/src/main/runtime/jellyfin-setup-window-main-deps.ts index a4c8954..08a2ff1 100644 --- a/src/main/runtime/jellyfin-setup-window-main-deps.ts +++ b/src/main/runtime/jellyfin-setup-window-main-deps.ts @@ -15,7 +15,7 @@ export function createBuildOpenJellyfinSetupWindowMainDepsHandler( authenticateWithPassword: (server: string, username: string, password: string, clientInfo) => deps.authenticateWithPassword(server, username, password, clientInfo), getJellyfinClientInfo: () => deps.getJellyfinClientInfo(), - saveStoredToken: (token: string) => deps.saveStoredToken(token), + saveStoredSession: (session) => deps.saveStoredSession(session), patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session), logInfo: (message: string) => deps.logInfo(message), logError: (message: string, error: unknown) => deps.logError(message, error), diff --git a/src/main/runtime/jellyfin-setup-window.test.ts b/src/main/runtime/jellyfin-setup-window.test.ts index 8972eb1..b87cfd6 100644 --- a/src/main/runtime/jellyfin-setup-window.test.ts +++ b/src/main/runtime/jellyfin-setup-window.test.ts @@ -40,6 +40,8 @@ test('parseJellyfinSetupSubmissionUrl parses setup url parameters', () => { test('createHandleJellyfinSetupSubmissionHandler applies successful login', async () => { const calls: string[] = []; + let patchPayload: unknown = null; + let savedSession: unknown = null; const handler = createHandleJellyfinSetupSubmissionHandler({ parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl), authenticateWithPassword: async () => ({ @@ -49,8 +51,14 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn userId: 'uid', }), getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), - saveStoredToken: () => calls.push('save'), - patchJellyfinConfig: () => calls.push('patch'), + saveStoredSession: (session) => { + savedSession = session; + calls.push('save'); + }, + patchJellyfinConfig: (session) => { + patchPayload = session; + calls.push('patch'); + }, logInfo: () => calls.push('info'), logError: () => calls.push('error'), showMpvOsd: (message) => calls.push(`osd:${message}`), @@ -62,6 +70,13 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn ); assert.equal(handled, true); assert.deepEqual(calls, ['save', 'patch', 'info', 'osd:Jellyfin login success', 'close']); + assert.deepEqual(savedSession, { accessToken: 'token', userId: 'uid' }); + assert.deepEqual(patchPayload, { + serverUrl: 'http://localhost', + username: 'user', + accessToken: 'token', + userId: 'uid', + }); }); test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async () => { @@ -72,7 +87,7 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async throw new Error('bad credentials'); }, getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), - saveStoredToken: () => calls.push('save'), + saveStoredSession: () => calls.push('save'), patchJellyfinConfig: () => calls.push('patch'), logInfo: () => calls.push('info'), logError: () => calls.push('error'), @@ -166,7 +181,7 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is throw new Error('should not auth'); }, getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), - saveStoredToken: () => {}, + saveStoredSession: () => {}, patchJellyfinConfig: () => {}, logInfo: () => {}, logError: () => {}, @@ -219,7 +234,7 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li userId: 'uid', }), getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), - saveStoredToken: () => calls.push('save'), + saveStoredSession: () => calls.push('save'), patchJellyfinConfig: () => calls.push('patch'), logInfo: () => calls.push('info'), logError: () => calls.push('error'), diff --git a/src/main/runtime/jellyfin-setup-window.ts b/src/main/runtime/jellyfin-setup-window.ts index f94fd2a..568ead4 100644 --- a/src/main/runtime/jellyfin-setup-window.ts +++ b/src/main/runtime/jellyfin-setup-window.ts @@ -117,7 +117,7 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: { clientInfo: JellyfinClientInfo, ) => Promise; getJellyfinClientInfo: () => JellyfinClientInfo; - saveStoredToken: (token: string) => void; + saveStoredSession: (session: { accessToken: string; userId: string }) => void; patchJellyfinConfig: (session: JellyfinSession) => void; logInfo: (message: string) => void; logError: (message: string, error: unknown) => void; @@ -137,7 +137,10 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: { submission.password, deps.getJellyfinClientInfo(), ); - deps.saveStoredToken(session.accessToken); + deps.saveStoredSession({ + accessToken: session.accessToken, + userId: session.userId, + }); deps.patchJellyfinConfig(session); deps.logInfo(`Jellyfin setup saved for ${session.username}.`); deps.showMpvOsd('Jellyfin login success'); @@ -197,7 +200,7 @@ export function createOpenJellyfinSetupWindowHandler Promise; getJellyfinClientInfo: () => JellyfinClientInfo; - saveStoredToken: (token: string) => void; + saveStoredSession: (session: { accessToken: string; userId: string }) => void; patchJellyfinConfig: (session: JellyfinSession) => void; logInfo: (message: string) => void; logError: (message: string, error: unknown) => void; @@ -221,7 +224,7 @@ export function createOpenJellyfinSetupWindowHandler deps.authenticateWithPassword(server, username, password, clientInfo), getJellyfinClientInfo: () => deps.getJellyfinClientInfo(), - saveStoredToken: (token) => deps.saveStoredToken(token), + saveStoredSession: (session) => deps.saveStoredSession(session), patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session), logInfo: (message) => deps.logInfo(message), logError: (message, error) => deps.logError(message, error), diff --git a/src/types.ts b/src/types.ts index bd462e5..b2e4e0b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -342,8 +342,6 @@ export interface JellyfinConfig { enabled?: boolean; serverUrl?: string; username?: string; - accessToken?: string; - userId?: string; deviceId?: string; clientName?: string; clientVersion?: string; @@ -515,8 +513,6 @@ export interface ResolvedConfig { enabled: boolean; serverUrl: string; username: string; - accessToken: string; - userId: string; deviceId: string; clientName: string; clientVersion: string;