provision when the account list changes, not only when you connect
Same gap as the photos plugin: users:list updated an in-memory array and nothing acted on it, so a person added to Officer later reached Video only if somebody re-ran provisioning by hand. The platform broadcasts on every create and delete and re-sends on every registration — nothing was listening. Safe to run unattended without changing what it does, because runProvisioning already draws the line in the right place: it creates the missing accounts and tokens them, and it RECORDS an adopted account without touching it. Minting for an adopted account resets a password its owner knows, and that stays behind includeAdopted, which only a person can ask for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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<void> {
|
||||
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/<id>/provision with { "includeAdopted": true } to give it one.',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[jellyfin:provision] automatic run failed:', error instanceof Error ? error.message : error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user