From f23d8076bf61b4bb9da4b4b17b773a45716c2c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 20 Aug 2026 20:34:59 +0000 Subject: [PATCH] grant every library on creation, by sending back the whole policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new Jellyfin user can see nothing: EnableAllFolders is false by default, so a provisioned account signs in to an empty home screen and looks like a bug in Officer rather than an unset flag. POST /Users/{id}/Policy replaces rather than merges, so the policy Jellyfin returned from /Users/New goes straight back with one field flipped. A hand-built body would quietly reset the other 43 properties. EnabledFolders is left as-is — ignored while the blanket flag is on, and still there if the owner ever turns it off. Not fatal if it fails: the account exists and works, and failing the create here would leave an orphan upstream with no mapping row, which is worse than a user who cannot see films yet. Co-Authored-By: Claude Opus 5 --- sidecar/provision.ts | 50 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/sidecar/provision.ts b/sidecar/provision.ts index ae5ba6a..b1775a2 100644 --- a/sidecar/provision.ts +++ b/sidecar/provision.ts @@ -24,6 +24,37 @@ import { planReconcile, describePlan, normalize, type JellyfinUser, type Reconci // and here the sign-in is not merely a check, it is how the token is obtained. The verification and the // useful work are the same call, which is the only reason it cannot be skipped when someone is in a hurry. +/** + * Turn on access to every library, preserving everything else about the policy. + * + * Not fatal if it fails: the account exists and can sign in, and library access is fixable from Jellyfin's + * own UI. Failing the whole creation here would leave an orphan account upstream with no mapping row, + * which is a worse state than a user who cannot see films yet. + */ +async function grantAllLibraries( + cfg: UpstreamTarget, + jellyfinUserId: string, + policy: Record | undefined, +): Promise { + if (!policy) { + console.warn(`[jellyfin:provision] ${jellyfinUserId}: no Policy on the created user — library access not set`); + return; + } + const res = await callUpstream(cfg, { + method: 'POST', + path: `/Users/${jellyfinUserId}/Policy`, + contentType: 'application/json', + // `EnabledFolders` is left as Jellyfin returned it. With `EnableAllFolders` true it is ignored, and + // clearing it would lose the per-folder list if the owner ever turns the blanket flag back off. + body: JSON.stringify({ ...policy, EnableAllFolders: true }), + }); + if (!res.ok) { + console.error( + `[jellyfin:provision] ${jellyfinUserId}: POST Policy → ${res.status}: ${(await res.text()).slice(0, 200)}`, + ); + } +} + /** Long enough that it does not matter that it is never rotated, since nobody ever knows it. */ const generatePassword = (): string => randomBytes(32).toString('base64url'); @@ -72,9 +103,21 @@ async function createOne( throw new Error(`POST /Users/New → ${res.status}: ${(await res.text()).slice(0, 200)}`); } - const created = (await res.json()) as { Id?: string }; + const created = (await res.json()) as { Id?: string; Policy?: Record }; if (!created.Id) throw new Error('POST /Users/New returned no Id'); + // ── Give them every library ── + // + // A new Jellyfin user can see NOTHING by default: `EnableAllFolders` is false and `EnabledFolders` is + // empty, so the account exists, signs in, and shows an empty home screen. Provisioning that and calling + // it done would look like a bug in Officer rather than an unset flag. + // + // `POST /Users/{id}/Policy` REPLACES the policy rather than merging it — so the whole object goes back, + // taken from what Jellyfin just returned and modified by one field. Sending a hand-built policy would + // silently reset the 43 properties not mentioned, which is how a user ends up with no transcoding or no + // remote access for reasons nobody can trace. + await grantAllLibraries(cfg, created.Id, created.Policy); + // Rule 4, and the token in one call. A per-user device id keeps this session distinct from the owner's // in Jellyfin's Devices screen, so revoking one does not sign out the other. const auth = await authenticate(cfg.base, `officer-${user.id}`, user.username!, password); @@ -124,7 +167,10 @@ export async function provision(params: { // One person failing must not stop the rest. A partially provisioned server is recoverable by // running again; a run that aborts on the first error leaves everyone after them unprovisioned and // gives no clue which ones those were. - outcome.failed.push({ username: user.username ?? String(user.id), error: error instanceof Error ? error.message : String(error) }); + outcome.failed.push({ + username: user.username ?? String(user.id), + error: error instanceof Error ? error.message : String(error), + }); } }