Skip to content

fix(plex): match server by /identity machineIdentifier for hostname URLs - #74

Merged
engels74 merged 13 commits into
mainfrom
fix/plex-hostname-membership-matching
Apr 19, 2026
Merged

fix(plex): match server by /identity machineIdentifier for hostname URLs#74
engels74 merged 13 commits into
mainfrom
fix/plex-hostname-membership-matching

Conversation

@engels74

@engels74 engels74 commented Apr 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes engels74/obzorarr-docker#5.

When PLEX_SERVER_URL is a hostname (e.g. http://plex.local.timo.be:32400, a LAN DNS name, or a reverse-proxied URL) and the Plex server advertises only .plex.direct connection URIs under the signed-in user's Plex.tv resources list, Obzorarr's four URL-string matching strategies all fail and verifyServerMembership returns { isMember: false }. The user sees a greyed-out Continue button with no actionable error message and cannot complete onboarding.

The authoritative test is not URL-string matching — Obzorarr already reaches the configured server successfully, so it can ask the server for its own identity via GET /identity and match the returned machineIdentifier against each Plex.tv resource's clientIdentifier. This is the same value Plex itself uses for server identity and matches regardless of how the URL is expressed.

Changes

New Strategy 0: match by machineIdentifier from /identity

  • New service src/lib/server/plex/server-identity.service.ts exposes fetchServerIdentity(), getConfiguredServerMachineId() (cache-first), and refreshConfiguredServerMachineId() (force refresh). Uses a 10 s AbortController timeout and the existing classifyConnectionError helper.
  • src/lib/server/auth/membership.ts prepends a new matching strategy that compares the fetched machineIdentifier against each server's clientIdentifier. The existing four URL-based strategies remain as fallbacks for when /identity is unreachable.
  • MembershipResult gains a discriminated reason?: 'not_reachable' | 'not_in_resources' | 'not_owner' and configuredMachineId?: string so the UI can render specific messages and offer an explicit ownership override.

Cache + invalidation

  • app_settings gains a SERVER_MACHINE_ID row, mirroring the existing SERVER_NAME cache (getCachedServerMachineId / setCachedServerMachineId / clearCachedServerMachineId).
  • The admin settings form invalidates the cache whenever PLEX_SERVER_URL or PLEX_TOKEN changes so the next membership check re-fetches /identity.

Onboarding UX

  • src/routes/onboarding/plex/+page.server.ts proactively calls getConfiguredServerMachineId() in load and returns configuredUrlReachable, configuredUrlErrorReason, and configuredMachineId so the page can warn before the user signs in.
  • New forceManualSelection action sets an app-settings flag that forces the manual server picker on the next load; src/routes/onboarding/+layout.server.ts respects the flag.
  • New confirmOwnershipOverride action: when /identity succeeded but Plex.tv does not list the server for the signed-in user, the user can explicitly attest ownership to bypass the resources check. Logged at logger.warn with URL, machineId, and username for audit trail.
  • src/routes/onboarding/plex/+page.svelte renders specific error cards with remediation buttons instead of the previous silent disabled state.

Tests

  • tests/unit/auth/membership.test.ts — four new cases covering Strategy 0 match, fallback when /identity unreachable, not_in_resources with machineId, and the cache interaction.
  • tests/unit/plex/server-identity.service.test.ts — new file covering the valid response, missing friendlyName, 401 / non-401 HTTP errors, AbortError classification, URL trailing-slash normalization, cache hit/miss, and the force-refresh path.

Reproduction and verification

Bug reproduced locally against a Bun proxy (/tmp/plex-proxy.ts) that forwards a plain http://localhost:7777 origin to the live Plex server. Pre-fix, the logs show the exact failure from the upstream issue:

[Membership] Configured PLEX_SERVER_URL: http://localhost:7777 (source: env)
[Membership] Found 1 server(s) accessible to user
[Membership] Server: PARENTi | clientIdentifier: <id> | owned: true
[Membership]   - Connection URI: https://<ip>.<hash>.plex.direct:32400 ...
[Membership] No matching server found for configured URL

Sign-in throws NotServerMemberError and the onboarding page renders the authentication-failure card:

Authentication error card

Pre-fix, onboarding step 2 leaves the user stuck at CONNECT (URL blurred):

Before: CONNECT step disabled

Post-fix, with the same hostname-only PLEX_SERVER_URL, Strategy 0 fetches /identity, matches the returned machineIdentifier against Plex.tv resources, and the user advances to SYNC:

After: advanced to SYNC

Test plan

  • bun run check — 0 errors
  • bun run test — full suite passes, new tests included, coverage ≥ 80 %
  • bun run check:biome — lint + format clean
  • Manual repro against live Plex server via local proxy: pre-fix blocks on CONNECT with No matching server found; post-fix matches via machineIdentifier and advances to SYNC
  • Manual unreachable-URL path: error card appears before sign-in with specific reason, forceManualSelection button falls back to manual picker
  • Cache invalidation on PLEX_SERVER_URL / PLEX_TOKEN change in admin settings

Hostname-based PLEX_SERVER_URL values (e.g. http://plex.local.timo.be:32400)
failed membership matching when the server only advertised .plex.direct URIs,
leaving users stuck on onboarding step 2.

Add Strategy 0 in findConfiguredServer that fetches /identity and compares the
returned machineIdentifier against each resource's clientIdentifier. Cache the
configured machineId in app_settings and invalidate on URL/token change.

Surface granular membership failure reasons (not_reachable / not_in_resources
/ not_owner) and expose them in onboarding: show a reachability error card
with a "Use a different server" fallback, and — only when /identity succeeds
but Plex.tv doesn't list the server — an explicit "I confirm I own this
server" override that logs an audit warn.

Fixes edbfi/obzorarr-docker#5.
@augmentcode

augmentcode Bot commented Apr 18, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Fixes Plex onboarding failures when PLEX_SERVER_URL is a hostname/reverse-proxy URL by identifying the configured server via GET /identity and matching its machineIdentifier against Plex.tv resources.

Changes:

  • Adds SERVER_MACHINE_ID app-setting cache with helpers to get/set/clear it
  • Introduces server-identity.service.ts to fetch/parse /identity with a 10s abort timeout and sanitized error classification
  • Prepends “Strategy 0” in verifyServerMembership() to match servers by machine id (case-insensitive), then falls back to prior URL strategies
  • Adds a reachability gate: if /identity fails, membership now returns reason: 'not_reachable' instead of allowing URL-only matches
  • Threads structured failure reasons (not_reachable, not_in_resources, not_owner) and configuredMachineId through MembershipResult and improves user-facing failure messages
  • Updates onboarding to proactively probe reachability, show targeted error cards, and support a guarded ownership override flow during initial setup
  • Clears/invalidates the machine-id cache when Plex URL/token changes (admin settings, onboarding server selection, env-conflict cleanup)
  • Adds unit coverage for the new identity service and the updated membership strategy/gating behavior

Technical Notes: Membership checks now always perform a live /identity probe (refresh) to avoid admitting users based on stale cached ids after server/token changes.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
- guard confirmOwnershipOverride with isOnboardingComplete() to prevent post-onboarding admin escalation (onboardingHandle skip-list exposes /onboarding actions after setup)
- atomically update sessions.isAdmin, users.isAdmin, and users.accountId in one db.transaction to avoid inconsistent state on partial failure
- require locals.user.isAdmin in forceManualSelection, matching continueAfterServerSelection
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/lib/server/plex/server-identity.service.ts
- invalidate cached SERVER_MACHINE_ID in clearConflictingDbSettings when PLEX_SERVER_URL or PLEX_TOKEN env is set, so a restart with new env values doesn't return a stale machineId from a previous server
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/lib/server/auth/membership.ts Outdated
Comment thread src/routes/onboarding/+layout.server.ts Outdated
- drop "confirm ownership to bypass" hint from messageForMembershipFailure, since that override is only reachable during onboarding but this message is surfaced post-onboarding via requireServerMembership
- reject forceManualSelection with fail(400) when PLEX_SERVER_URL or PLEX_TOKEN is set as an env var; getPlexConfig always prefers env over DB, so the manual picker would otherwise save values the rest of the app ignores
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
Comment thread src/lib/server/auth/membership.ts Outdated
…ride write

- Add optional identityErrorReason to MembershipResult and thread it
  through verifyServerMembership when /identity fails without a machineId
- Export messageForMembershipFailure and interpolate identityErrorReason
  into the not_reachable branch so auth/timeout/other causes are surfaced
  instead of a generic "could not reach" message
- Use messageForMembershipFailure(membership) in the onboarding
  verifyAdmin action to keep a single source of truth for failure copy
- Remove setPlexServerUrlOverrideManual(true) from forceManualSelection:
  the iter 3 env guard makes the write unreachable when the flag would
  matter, and writing it when env is absent is dead state that would
  silently bypass the guard if env vars were added in a future boot
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/lib/server/auth/membership.ts Outdated
Comment thread src/lib/server/plex/server-identity.service.ts Outdated
Comment thread src/routes/onboarding/plex/+page.server.ts
… probe on onboarding load

- Move clearTimeout into a finally block so CONNECTION_TIMEOUT_MS caps
  response.json() + schema parsing, not just the header fetch resolving.
  A slow or partial body stream now aborts via the AbortController signal
  and surfaces through classifyConnectionError like any other timeout.
- Swap the onboarding load's getConfiguredServerMachineId() for
  refreshConfiguredServerMachineId() so the reachability banner reflects
  the live server state. A cached machineId could previously mask a
  now-offline server or revoked token and hide the remediation path.
  confirmOwnershipOverride keeps the cached call — it fires seconds after
  the load, so cache reuse is intentional.
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/routes/api/onboarding/select-server/+server.ts
Comment thread src/lib/server/plex/server-identity.service.ts
Comment thread src/lib/server/admin/settings.service.ts

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread tests/unit/auth/membership.test.ts
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/lib/server/admin/settings.service.ts Outdated
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
Comment thread src/routes/onboarding/plex/+page.server.ts Outdated
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/lib/server/auth/membership.ts Outdated
Comment thread tests/unit/plex/server-identity.service.test.ts
verifyServerMembership() previously admitted URL-based matches even when
the /identity probe against the configured server failed, letting users
proceed into onboarding states where subsequent Plex sync would fail.

Short-circuit to reason='not_reachable' when getConfiguredServerMachineId()
returns null so the onboarding UI surfaces the unreachable state instead
of advancing to sync. Simplify the not-in-resources branch since
configuredMachineIdFromIdentity is now guaranteed past the gate.

Also add a regression test asserting refreshConfiguredServerMachineId()
evicts stale cache entries when /identity fails, so a failed refresh
cannot leave a pre-existing machineId in place.
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread tests/unit/auth/membership.test.ts
The "no matching server" test previously only mocked global.fetch, so the
same mock response was also consumed by the /identity probe path (as an
accidentally-invalid shape). The result then depended on that parse-failure
fallback rather than the intended no-matching-server scenario.

Mock getConfiguredServerMachineId() explicitly to return a known machineId
and assert reason='not_in_resources' plus the returned configuredMachineId
so the test deterministically exercises the intended branch.
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/lib/server/auth/membership.ts Outdated
The gate read the configured machineId via the cache-first helper, so a
stale SQLite-backed entry could satisfy it after the configured
PLEX_TOKEN was revoked or the server went offline. Only the onboarding
page's load() pre-refreshed, leaving the 15-minute session revalidation
and other callers exposed. Switch the call in verifyServerMembership to
refreshConfiguredServerMachineId so the gate is robust-by-default.
@engels74

Copy link
Copy Markdown
Collaborator Author

augment review

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

@engels74
engels74 merged commit 39c3d12 into main Apr 19, 2026
3 checks passed
@engels74
engels74 deleted the fix/plex-hostname-membership-matching branch April 19, 2026 07:16
engels74 added a commit that referenced this pull request Apr 19, 2026
Align the two onboarding connection-test endpoints with the reference
pattern already used in fetchServerIdentity (PR #74). Previously the
timer was cleared immediately after fetch resolved its headers, leaving
response.json() and PlexServerIdentitySchema.safeParse() unprotected by
CONNECTION_TIMEOUT_MS. A Plex server that sent headers and then stalled
mid-body could hang the request indefinitely.

Consolidate clearTimeout(timeoutId) into a single finally block so the
timeout budget covers the full /identity call: fetch, body parsing, and
schema validation. Pure control-flow refactor - no behaviour change on
the happy path or already-timed-out path.

- src/routes/api/onboarding/test-connection/+server.ts
- src/routes/api/onboarding/select-server/+server.ts (testConnection helper)

select-server keeps its 15000ms budget; test-connection keeps 10000ms.
engels74 added a commit that referenced this pull request Apr 19, 2026
#75)

Align the two onboarding connection-test endpoints with the reference
pattern already used in fetchServerIdentity (PR #74). Previously the
timer was cleared immediately after fetch resolved its headers, leaving
response.json() and PlexServerIdentitySchema.safeParse() unprotected by
CONNECTION_TIMEOUT_MS. A Plex server that sent headers and then stalled
mid-body could hang the request indefinitely.

Consolidate clearTimeout(timeoutId) into a single finally block so the
timeout budget covers the full /identity call: fetch, body parsing, and
schema validation. Pure control-flow refactor - no behaviour change on
the happy path or already-timed-out path.

- src/routes/api/onboarding/test-connection/+server.ts
- src/routes/api/onboarding/select-server/+server.ts (testConnection helper)

select-server keeps its 15000ms budget; test-connection keeps 10000ms.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Initial Setup - Step 2 - Issue

1 participant