1import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
2import {
3 AuthProvidersManageScope,
4 type AuthSessionState,
5 type EnvironmentId,
6 type ProjectId,
7 type ProjectSettingsOverrides,
8 type ProviderInstanceId,
9 sessionGrantsScope,
10 type ServerSettings,
11 type SessionGrantInput,
12} from "@t3tools/contracts";
13
14export interface ProviderEnvironmentOptionLike {
15 readonly environmentId: EnvironmentId;
16 readonly label: string;
17}
18
19export function isProviderSettingsEnvironmentAvailable(input: {
20 readonly connectionPhase: EnvironmentConnectionPhase;
21 readonly hasServerConfig: boolean;
22}): boolean {
23 return input.connectionPhase === "connected" && input.hasServerConfig;
24}
25
26export function buildProviderEnvironmentOptions<T extends ProviderEnvironmentOptionLike>(
27 environments: ReadonlyArray<T>,
28 primaryEnvironmentId: EnvironmentId | null,
29 environmentIds?: readonly EnvironmentId[],
30): ReadonlyArray<T> {
31 const allowed = environmentIds ? new Set(environmentIds) : null;
32 return environments
33 .filter((environment) => !allowed || allowed.has(environment.environmentId))
34 .toSorted((left, right) => {
35 const leftIsPrimary = left.environmentId === primaryEnvironmentId;
36 const rightIsPrimary = right.environmentId === primaryEnvironmentId;
37 if (leftIsPrimary !== rightIsPrimary) {
38 return leftIsPrimary ? -1 : 1;
39 }
40 return (
41 left.label.localeCompare(right.label) ||
42 String(left.environmentId).localeCompare(String(right.environmentId))
43 );
44 });
45}
46
47export function resolveSelectedProviderEnvironmentId(
48 environments: ReadonlyArray<ProviderEnvironmentOptionLike>,
49 selectedEnvironmentId: EnvironmentId | null,
50 primaryEnvironmentId: EnvironmentId | null,
51): EnvironmentId | null {
52 if (
53 selectedEnvironmentId !== null &&
54 environments.some((environment) => environment.environmentId === selectedEnvironmentId)
55 ) {
56 return selectedEnvironmentId;
57 }
58 if (
59 primaryEnvironmentId !== null &&
60 environments.some((environment) => environment.environmentId === primaryEnvironmentId)
61 ) {
62 return primaryEnvironmentId;
63 }
64 return environments[0]?.environmentId ?? null;
65}
66
67export type ProviderEnvironmentAccess =
68 | { readonly kind: "editable" }
69 /** `reason` distinguishes waiting on the device from waiting on permissions. */
70 | { readonly kind: "loading"; readonly reason: "config" | "permissions" }
71 | { readonly kind: "read-only" }
72 | { readonly kind: "unavailable" }
73 | { readonly kind: "error" };
74
75/**
76 * Whether the session may change provider configuration on an environment.
77 * `pending` means the answer is still unknown, which must not be presented as
78 * editable: rendering controls we already know might be rejected only turns a
79 * permission problem into a failed write.
80 */
81export type ProviderOperateAccess = "granted" | "denied" | "pending";
82
83/** Cached grants remain usable during revalidation; unknown or failed lookups grant nothing. */
84function resolveSessionOperateAccess(input: {
85 readonly session: SessionGrantInput | null;
86 readonly isPending: boolean;
87 readonly hasError: boolean;
88}): ProviderOperateAccess {
89 if (input.hasError) return "denied";
90 if (input.session === null) return input.isPending ? "pending" : "denied";
91 return sessionGrantsScope(input.session, AuthProvidersManageScope) ? "granted" : "denied";
92}
93
94export function resolvePrimaryOperateAccess(input: {
95 readonly isPrimary: boolean;
96 readonly hasDesktopBridge: boolean;
97 readonly session: SessionGrantInput | null;
98 readonly isPending: boolean;
99 readonly hasError: boolean;
100}): ProviderOperateAccess {
101 return resolveSessionOperateAccess(input);
102}
103
104export function resolveRemoteOperateAccess(input: {
105 readonly session: SessionGrantInput | null;
106 readonly isPending: boolean;
107 readonly hasError: boolean;
108}): ProviderOperateAccess {
109 return resolveSessionOperateAccess(input);
110}
111
112/**
113 * The patch for one instance's project override, following the envelope
114 * replacement rule for `projectSettingsOverrides`: the returned entry
115 * replaces the project's whole override set, and `null` removes it.
116 * `value: undefined` resets that single instance back to inheriting the
117 * environment's enablement, leaving any other overridden keys untouched.
118 *
119 * `providerInstances` is always sent explicitly, even as `{}`, because the
120 * server keeps a stored `providerInstances` map when an incoming entry omits
121 * the key (so older clients that don't know the field cannot wipe it by
122 * editing another setting). Omitting it here would undo the very clear this
123 * function means to make.
124 */
125/** Every checkout of a project on one environment shares the same choice. */
126export function buildProviderInstanceOverridePatch(
127 settings: Pick<ServerSettings, "projectSettingsOverrides">,
128 projectIds: readonly ProjectId[],
129 instanceId: ProviderInstanceId,
130 value: boolean | undefined,
131): Record<ProjectId, ProjectSettingsOverrides | null> {
132 const patch: Record<ProjectId, ProjectSettingsOverrides | null> = {};
133 for (const projectId of projectIds) {
134 const current = settings.projectSettingsOverrides[projectId];
135 const instances = { ...current?.providerInstances };
136 const instance = { ...instances[instanceId] };
137 if (value === undefined) delete instance.enabled;
138 else instance.enabled = value;
139 if (Object.keys(instance).length > 0) instances[instanceId] = instance;
140 else delete instances[instanceId];
141
142 const { providerInstances: _omit, ...rest } = current ?? {};
143 const nextEntry: ProjectSettingsOverrides = { ...rest, providerInstances: instances };
144 patch[projectId] =
145 Object.keys(rest).length === 0 && Object.keys(instances).length === 0 ? null : nextEntry;
146 }
147 return patch;
148}
149
150export function classifyProviderEnvironmentAccess(input: {
151 readonly connectionPhase: EnvironmentConnectionPhase;
152 readonly hasServerConfig: boolean;
153 readonly operateAccess: ProviderOperateAccess;
154}): ProviderEnvironmentAccess {
155 if (input.connectionPhase === "error") {
156 return { kind: "error" };
157 }
158 if (input.connectionPhase !== "connected") {
159 return { kind: "unavailable" };
160 }
161 if (!input.hasServerConfig) {
162 return { kind: "loading", reason: "config" };
163 }
164 if (input.operateAccess === "pending") {
165 return { kind: "loading", reason: "permissions" };
166 }
167 if (input.operateAccess === "denied") {
168 return { kind: "read-only" };
169 }
170 return { kind: "editable" };
171}