diff --git a/sidecar/index.ts b/sidecar/index.ts index 23f6ea9..cf9e0f0 100644 --- a/sidecar/index.ts +++ b/sidecar/index.ts @@ -5,6 +5,7 @@ import { handleBytesRoute, handleOfficerRoute } from './routes'; import { getConfig, probe } from './upstream'; import { API_URL } from '@@/officer-url.mjs'; import { setProvisionableUsers } from './user-list'; +import { provisionForAccountChange } from './provision-run'; // The officer-jellyfin sidecar. Owns the whole Jellyfin contract for Officer: the instance URL, the access // token, the Jellyfin user it belongs to and the DeviceId its sessions are keyed by. The platform API is a @@ -146,6 +147,10 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { // not a question. case 'users:list': setProvisionableUsers(cmd.users); + // And act on it. Detached deliberately: this reaches out to Jellyfin and can take seconds, while the + // command handler is on the socket the platform is waiting on. Nothing replies to `users:list`, so + // there is no answer to delay — but blocking here would stall every later message on this socket. + void provisionForAccountChange(); break; default: reply({ diff --git a/sidecar/provision-run.ts b/sidecar/provision-run.ts index 2a94417..e24bae6 100644 --- a/sidecar/provision-run.ts +++ b/sidecar/provision-run.ts @@ -1,7 +1,13 @@ -import { listJellyfinAccounts, linkJellyfinAccount, setJellyfinAccountToken } from '../db'; +import { OWNER_USER_ID } from 'officerdb'; +import { + getActiveJellyfinCredentials, + listJellyfinAccounts, + linkJellyfinAccount, + setJellyfinAccountToken, +} from '../db'; import { mintTokenForLinked, provision, type ProvisionOutcome } from './provision'; import { getProvisionableUsers, hasProvisionableUsers } from './user-list'; -import { invalidateConfig, type UpstreamTarget } from './upstream'; +import { invalidateConfig, normalizeBase, type UpstreamTarget } from './upstream'; // The one place that turns "an admin just connected" into accounts on the server. // @@ -165,3 +171,57 @@ export async function backfillAccountTokens(params: { return report; } + +/** + * Provision because the ACCOUNT LIST changed, rather than because somebody asked. + * + * This is what makes an Officer account created next month reach Video without anyone remembering that a + * plugin needed telling. `users:list` arrives on every create and delete — and on every sidecar + * registration, so a restart is also a reconciliation. + * + * `runProvisioning` logs its plan either way, so a restart prints a few lines describing the world as this + * plugin sees it; only the "something changed" summary below is suppressed when nothing did. + * + * Safe unattended because of what `runProvisioning` already does and does not do: it creates the accounts + * that are missing and gives each a token, and it RECORDS an adopted account without touching it. Minting + * for an adopted account is the destructive path — it resets a password its owner knows — and that stays + * behind `backfillAccountTokens({ includeAdopted: true })`, which only a person can ask for. + * + * Never throws. It runs detached from the command handler, and a plugin failing to provision must not take + * down the socket carrying the message. + */ +export async function provisionForAccountChange(): Promise { + try { + // The server members inherit is the owner's selected one. A member who registered a Jellyfin of their + // own is not a provisioning target — that is their connection, not a shared library. + const creds = await getActiveJellyfinCredentials(OWNER_USER_ID); + if (!creds) return; // nothing connected yet; connecting is itself a provisioning trigger + + const report = await runProvisioning({ + userId: OWNER_USER_ID, + serverId: creds.id, + base: normalizeBase(creds.url), + token: creds.accessToken, + jellyfinUserId: creds.jellyfinUserId, + deviceId: creds.deviceId, + }); + + if (!report.ran) { + console.log(`[jellyfin:provision] not run — ${report.reason}`); + return; + } + if (report.created === 0 && report.linked === 0 && report.failed.length === 0) return; // nothing changed + + console.log( + `[jellyfin:provision] account list changed: ${report.created} created, ${report.linked} linked`, + ); + if (report.linked > 0) { + console.log( + '[jellyfin:provision] a linked account had a Jellyfin login already and has no token yet — ' + + 'POST /_config//provision with { "includeAdopted": true } to give it one.', + ); + } + } catch (error) { + console.error('[jellyfin:provision] automatic run failed:', error instanceof Error ? error.message : error); + } +}