import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; import { AuthOrchestrationOperateScope, type AuthSessionState, type EnvironmentId, } from "@t3tools/contracts"; export interface ProviderEnvironmentOptionLike { readonly environmentId: EnvironmentId; readonly label: string; } export function isProviderSettingsEnvironmentAvailable(input: { readonly connectionPhase: EnvironmentConnectionPhase; readonly hasServerConfig: boolean; }): boolean { return input.connectionPhase === "connected" && input.hasServerConfig; } export function buildProviderEnvironmentOptions( environments: ReadonlyArray, primaryEnvironmentId: EnvironmentId | null, ): ReadonlyArray { return environments.toSorted((left, right) => { const leftIsPrimary = left.environmentId === primaryEnvironmentId; const rightIsPrimary = right.environmentId === primaryEnvironmentId; if (leftIsPrimary !== rightIsPrimary) { return leftIsPrimary ? -1 : 1; } return ( left.label.localeCompare(right.label) || String(left.environmentId).localeCompare(String(right.environmentId)) ); }); } export function resolveSelectedProviderEnvironmentId( environments: ReadonlyArray, selectedEnvironmentId: EnvironmentId | null, primaryEnvironmentId: EnvironmentId | null, ): EnvironmentId | null { if ( selectedEnvironmentId !== null && environments.some((environment) => environment.environmentId === selectedEnvironmentId) ) { return selectedEnvironmentId; } if ( primaryEnvironmentId !== null && environments.some((environment) => environment.environmentId === primaryEnvironmentId) ) { return primaryEnvironmentId; } return environments[0]?.environmentId ?? null; } export type ProviderEnvironmentAccess = | { readonly kind: "editable" } /** `reason` distinguishes waiting on the device from waiting on permissions. */ | { readonly kind: "loading"; readonly reason: "config" | "permissions" } | { readonly kind: "read-only" } | { readonly kind: "unavailable" } | { readonly kind: "error" }; /** * Whether the session may change provider configuration on an environment. * `pending` means the answer is still unknown, which must not be presented as * editable: rendering controls we already know might be rejected only turns a * permission problem into a failed write. */ export type ProviderOperateAccess = "granted" | "denied" | "pending"; /** * Resolve operate access from an environment's `/api/auth/session` answer. * * Cached session data wins over an in-flight revalidation. The session atoms * are SWR-backed, so they report `isPending` on every background refresh; * treating that as unknown would flip a working panel back to loading and * discard in-progress edits. * * `missingScopesAccess` decides the case where the session resolved but did * not report scopes: the primary serves the web app itself so its server * always reports them (absence means denial), while a remote device may run an * older server version that predates scope reporting, where denial would lock * out a legitimate session. The environment RPC layer stays authoritative * either way. */ function resolveSessionOperateAccess(input: { readonly session: Pick | null; readonly isPending: boolean; readonly hasError: boolean; readonly missingScopesAccess: "granted" | "denied"; }): ProviderOperateAccess { if (input.session === null) { if (input.isPending) { return "pending"; } // A failed session fetch is a transport problem, not a permission // decision — locking the panel read-only would misreport it. Stay // optimistic; the environment RPC layer still rejects unauthorized writes. return input.hasError ? "granted" : "denied"; } if (!input.session.authenticated) { return "denied"; } if (input.session.scopes === undefined) { return input.missingScopesAccess; } return input.session.scopes.includes(AuthOrchestrationOperateScope) ? "granted" : "denied"; } /** Operate access for the primary environment's own browser session. */ export function resolvePrimaryOperateAccess(input: { readonly isPrimary: boolean; readonly hasDesktopBridge: boolean; readonly session: Pick | null; readonly isPending: boolean; readonly hasError: boolean; }): ProviderOperateAccess { if (!input.isPrimary || input.hasDesktopBridge) { return "granted"; } return resolveSessionOperateAccess({ session: input.session, isPending: input.isPending, hasError: input.hasError, missingScopesAccess: "denied", }); } /** * Operate access for a non-primary environment, derived from the scopes its * `/api/auth/session` endpoint reports for this client's credential. */ export function resolveRemoteOperateAccess(input: { readonly session: Pick | null; readonly isPending: boolean; readonly hasError: boolean; }): ProviderOperateAccess { return resolveSessionOperateAccess({ ...input, missingScopesAccess: "granted", }); } export function classifyProviderEnvironmentAccess(input: { readonly connectionPhase: EnvironmentConnectionPhase; readonly hasServerConfig: boolean; readonly operateAccess: ProviderOperateAccess; }): ProviderEnvironmentAccess { if (input.connectionPhase === "error") { return { kind: "error" }; } if (input.connectionPhase !== "connected") { return { kind: "unavailable" }; } if (!input.hasServerConfig) { return { kind: "loading", reason: "config" }; } if (input.operateAccess === "pending") { return { kind: "loading", reason: "permissions" }; } if (input.operateAccess === "denied") { return { kind: "read-only" }; } return { kind: "editable" }; }