1import {
2 ManagedPolicyClient,
3 ManagedPolicyDocument,
4 SERVER_EXIT_CODE_MANAGED_POLICY,
5} from "@t3tools/contracts";
6import * as Cause from "effect/Cause";
7import * as Context from "effect/Context";
8import * as Duration from "effect/Duration";
9import * as Effect from "effect/Effect";
10import * as Exit from "effect/Exit";
11import * as FileSystem from "effect/FileSystem";
12import * as Layer from "effect/Layer";
13import * as Runtime from "effect/Runtime";
14import * as Schema from "effect/Schema";
15import * as Stream from "effect/Stream";
16import { ChildProcess, ChildProcessSpawner } from "effect/process";
17
18import * as HostProcess from "./HostProcess.ts";
19
20export class ManagedPolicyFileError extends Schema.TaggedError<ManagedPolicyFileError>()(
21 "ManagedPolicyFileError",
22 {
23 path: Schema.String,
24 reason: Schema.String,
25 cause: Schema.optional(Schema.Defect()),
26 },
27) {
28 override readonly [Runtime.errorExitCode] = SERVER_EXIT_CODE_MANAGED_POLICY;
29
30 override get message(): string {
31 return `T3 Code cannot use the managed policy at ${this.path}: ${this.reason}. It will not start until an administrator fixes this.`;
32 }
33}
34
35/** The system path an administrator deploys the policy to on `platform`. */
36export function managedPolicyPath(platform: NodeJS.Platform): string {
37 switch (platform) {
38 case "darwin":
39 return "/Library/Application Support/T3Code/policy.json";
40 case "win32":
41 // Not ProgramData, where a user can create the folder first and own it; and not
42 // %ProgramFiles%, which a user can point somewhere they write.
43 return "C:\\Program Files\\T3Code\\policy.json";
44 default:
45 return "/etc/t3code/policy.json";
46 }
47}
48
49// Unknown keys may be restrictions from a newer release; refusing them keeps a file this
50// build cannot fully enforce from looking enforced.
51const decodeDocumentJsonExit = Schema.decodeUnknownExit(
52 Schema.fromJsonString(ManagedPolicyDocument),
53 { onExcessProperty: "error" },
54);
55const decodeDocumentExit = Schema.decodeUnknownExit(ManagedPolicyDocument, {
56 onExcessProperty: "error",
57});
58
59/** Decode a policy from JSON text, or from a value a profile already parsed. */
60const decodeDocument = (path: string, raw: unknown) => {
61 const decoded = typeof raw === "string" ? decodeDocumentJsonExit(raw) : decodeDocumentExit(raw);
62 if (Exit.isSuccess(decoded)) return Effect.succeed(decoded.value);
63 const error = Cause.squash(decoded.cause);
64 // The schema message names the offending key, e.g. `Expected no excess property at ["limits"]`.
65 const detail = Schema.isSchemaError(error) ? ` (${error.message.replace(/\s*\n\s*/g, " ")})` : "";
66 return Effect.fail(
67 new ManagedPolicyFileError({
68 path,
69 reason: `it is not valid JSON or does not match the policy schema${detail}`,
70 cause: decoded.cause,
71 }),
72 );
73};
74
75/** Read the policy document at `path`; a missing file means no policy. */
76export const readManagedPolicyFile = Effect.fn("readManagedPolicyFile")(function* (path: string) {
77 const fs = yield* FileSystem.FileSystem;
78 const exists = yield* fs
79 .exists(path)
80 .pipe(
81 Effect.mapError(
82 (cause) =>
83 new ManagedPolicyFileError({ path, reason: "the file cannot be checked", cause }),
84 ),
85 );
86 if (!exists) return undefined;
87 const raw = yield* fs
88 .readFileString(path)
89 .pipe(
90 Effect.mapError(
91 (cause) => new ManagedPolicyFileError({ path, reason: "the file cannot be read", cause }),
92 ),
93 );
94 return yield* decodeDocument(path, raw);
95});
96
97/** Where a configuration profile's managed preferences land on macOS. */
98export const MACOS_MANAGED_PREFERENCES_PATH =
99 "/Library/Managed Preferences/com.t3tools.t3code.plist";
100const WINDOWS_POLICY_SUBKEY = "SOFTWARE\\Policies\\T3Tools\\T3Code";
101/**
102 * The registry key Group Policy and Intune write to on Windows. It holds either the Group Policy
103 * template's values, one per control, or a `Policy` string with the whole policy as JSON.
104 */
105export const WINDOWS_POLICY_KEY = `HKLM\\${WINDOWS_POLICY_SUBKEY}`;
106
107/** Each value a Group Policy control writes, and where it lands in the document. */
108export const WINDOWS_POLICY_VALUES = {
109 Organization: { type: "string", path: ["organization"] },
110 SupportUrl: { type: "string", path: ["supportUrl"] },
111 Settings: { type: "json", path: ["settings"] },
112 MaxRuntimeMode: { type: "string", path: ["limits", "maxRuntimeMode"] },
113 Connect: { type: "boolean", path: ["limits", "connect"] },
114 WebhookTasks: { type: "boolean", path: ["limits", "webhookTasks"] },
115 Telemetry: { type: "boolean", path: ["limits", "telemetry"] },
116 BrowserCookieImport: { type: "boolean", path: ["limits", "browserCookieImport"] },
117 ProviderInstances: { type: "json", path: ["limits", "providers", "instances"] },
118 UpdatesMode: { type: "string", path: ["limits", "updates", "mode"] },
119 UpdatesFeedUrl: { type: "string", path: ["limits", "updates", "feedUrl"] },
120 AllowNightly: { type: "boolean", path: ["limits", "updates", "allowNightly"] },
121} as const satisfies Record<
122 string,
123 { readonly type: "string" | "json" | "boolean"; readonly path: ReadonlyArray<string> }
124>;
125
126/** Subkeys of checkboxes: every value set to 1 is one entry of the list at `path`. */
127export const WINDOWS_POLICY_LISTS = {
128 Clients: ["limits", "clients"],
129 AllowedDrivers: ["limits", "providers", "allowedDrivers"],
130} as const satisfies Record<string, ReadonlyArray<string>>;
131// A disabled policy still writes its checkboxes' falseValue; its own DWORD disambiguates it.
132export const WINDOWS_POLICY_GROUPS = {
133 ...WINDOWS_POLICY_LISTS,
134 Updates: ["limits", "updates"],
135} as const satisfies Record<string, ReadonlyArray<string>>;
136// An absolute path: an earlier PATH entry could otherwise answer that there is no policy.
137const PLUTIL = "/usr/bin/plutil";
138
139export class WindowsRegistryError extends Schema.TaggedError<WindowsRegistryError>()(
140 "WindowsRegistryError",
141 {
142 /** The Win32 error, such as 5 for access denied; absent when the registry API cannot load. */
143 code: Schema.optional(Schema.Number),
144 cause: Schema.optional(Schema.Defect()),
145 },
146) {
147 override get message(): string {
148 return this.code === undefined ? "the registry API cannot load" : `Windows error ${this.code}`;
149 }
150}
151
152/** A registry value; REG_EXPAND_SZ and every other type read as `other`. */
153export type RegistryValue =
154 | { readonly name: string; readonly type: "REG_SZ"; readonly data: string }
155 | { readonly name: string; readonly type: "REG_DWORD"; readonly data: number }
156 | { readonly name: string; readonly type: "other" };
157
158export interface RegistryKey {
159 readonly values: ReadonlyArray<RegistryValue>;
160 readonly subkeys: ReadonlyArray<string>;
161}
162
163/**
164 * Reads the registry through the Win32 API in this process. A missing key and a failed read come
165 * back apart, and blocked registry tools such as reg.exe do not matter.
166 */
167export class WindowsRegistry extends Context.Service<
168 WindowsRegistry,
169 {
170 /** The values and subkey names of HKLM\\`subkey` in the 64-bit view; undefined when absent. */
171 readonly readMachineKey: (
172 subkey: string,
173 ) => Effect.Effect<RegistryKey | undefined, WindowsRegistryError>;
174 }
175>()("@t3tools/shared/managedPolicyFile/WindowsRegistry") {}
176
177// (HKEY)(LONG)0x80000002, sign-extended to 64 bits.
178const HKEY_LOCAL_MACHINE = -0x7ffffffen;
179const KEY_READ = 0x20019;
180const KEY_WOW64_64KEY = 0x100;
181const REG_SZ = 1;
182const REG_DWORD = 4;
183const ERROR_FILE_NOT_FOUND = 2;
184const ERROR_MORE_DATA = 234;
185const ERROR_NO_MORE_ITEMS = 259;
186// Value and key names are at most 16,383 characters.
187const MAX_NAME_CHARS = 16_384;
188
189class Win32Failure extends Schema.TaggedError<Win32Failure>()("Win32Failure", {
190 code: Schema.Number,
191}) {
192 override get message(): string {
193 return `Windows error ${this.code}`;
194 }
195}
196
197const isWin32Failure = Schema.is(Win32Failure);
198
199/**
200 * Each app passes how it loads ffi-rs: a server single-executable resolves packages on disk only
201 * through `require`, and the desktop's CommonJS bundle loads them with `import()`.
202 */
203export const layerWindowsRegistry = (loadFfi: () => Promise<typeof import("ffi-rs")>) =>
204 Layer.effect(
205 WindowsRegistry,
206 Effect.gen(function* () {
207 const architecture = yield* HostProcess.Architecture;
208 const loadApi = yield* Effect.cached(
209 Effect.tryPromise({
210 try: async () => {
211 // HKEY_LOCAL_MACHINE, key handles, and the null pointers below are 64-bit.
212 if (architecture !== "x64" && architecture !== "arm64") {
213 throw new Error(`no registry access on ${architecture}`);
214 }
215 const { DataType, load, open } = await loadFfi();
216 open({ library: "t3-advapi32", path: "advapi32.dll" });
217 const call = (
218 funcName: string,
219 paramsType: Array<(typeof DataType)[keyof typeof DataType]>,
220 paramsValue: Array<unknown>,
221 ): number =>
222 load({
223 library: "t3-advapi32",
224 funcName,
225 retType: DataType.I32,
226 paramsType,
227 paramsValue,
228 } as Parameters<typeof load>[0]) as number;
229 const { BigInt: Handle, U32, U8Array: Out, WString } = DataType;
230 return {
231 open: (subkey: string, handle: Buffer) =>
232 call(
233 "RegOpenKeyExW",
234 [Handle, WString, U32, U32, Out],
235 [HKEY_LOCAL_MACHINE, subkey, 0, KEY_READ | KEY_WOW64_64KEY, handle],
236 ),
237 enumValue: (
238 key: bigint,
239 index: number,
240 name: Buffer,
241 nameChars: Buffer,
242 type: Buffer,
243 data: Buffer,
244 dataBytes: Buffer,
245 ) =>
246 call(
247 "RegEnumValueW",
248 [Handle, U32, Out, Out, Handle, Out, Out, Out],
249 [key, index, name, nameChars, 0n, type, data, dataBytes],
250 ),
251 enumKey: (key: bigint, index: number, name: Buffer, nameChars: Buffer) =>
252 call(
253 "RegEnumKeyExW",
254 [Handle, U32, Out, Out, Handle, Handle, Handle, Handle],
255 [key, index, name, nameChars, 0n, 0n, 0n, 0n],
256 ),
257 close: (key: bigint) => call("RegCloseKey", [Handle], [key]),
258 };
259 },
260 catch: (cause) => new WindowsRegistryError({ cause }),
261 }),
262 );
263
264 const readKey = (api: Effect.Success<typeof loadApi>, subkey: string) => {
265 const handle = Buffer.alloc(8);
266 const opened = api.open(subkey, handle);
267 if (opened === ERROR_FILE_NOT_FOUND) return undefined;
268 if (opened !== 0) throw new Win32Failure({ code: opened });
269 const key = handle.readBigInt64LE();
270 try {
271 const name = Buffer.alloc(MAX_NAME_CHARS * 2);
272 const chars = Buffer.alloc(4);
273 const values: Array<RegistryValue> = [];
274 for (let index = 0; ; index++) {
275 let data = Buffer.alloc(64 * 1024);
276 const type = Buffer.alloc(4);
277 const bytes = Buffer.alloc(4);
278 const attempt = () => {
279 chars.writeUInt32LE(MAX_NAME_CHARS);
280 bytes.writeUInt32LE(data.length);
281 return api.enumValue(key, index, name, chars, type, data, bytes);
282 };
283 let code = attempt();
284 // The value is larger than the buffer; the API reported the size it needs.
285 if (code === ERROR_MORE_DATA) {
286 data = Buffer.alloc(bytes.readUInt32LE());
287 code = attempt();
288 }
289 if (code === ERROR_NO_MORE_ITEMS) break;
290 if (code !== 0) throw new Win32Failure({ code: code });
291 const valueName = name.subarray(0, chars.readUInt32LE() * 2).toString("utf16le");
292 const stored = data.subarray(0, bytes.readUInt32LE());
293 const valueType = type.readUInt32LE();
294 values.push(
295 valueType === REG_SZ
296 ? {
297 name: valueName,
298 type: "REG_SZ",
299 data: stored.toString("utf16le").replace(/\0+$/, ""),
300 }
301 : valueType === REG_DWORD && stored.length === 4
302 ? { name: valueName, type: "REG_DWORD", data: stored.readUInt32LE() }
303 : { name: valueName, type: "other" },
304 );
305 }
306 const subkeys: Array<string> = [];
307 for (let index = 0; ; index++) {
308 chars.writeUInt32LE(MAX_NAME_CHARS);
309 const code = api.enumKey(key, index, name, chars);
310 if (code === ERROR_NO_MORE_ITEMS) break;
311 if (code !== 0) throw new Win32Failure({ code: code });
312 subkeys.push(name.subarray(0, chars.readUInt32LE() * 2).toString("utf16le"));
313 }
314 return { values, subkeys };
315 } finally {
316 api.close(key);
317 }
318 };
319
320 return WindowsRegistry.of({
321 readMachineKey: Effect.fn("WindowsRegistry.readMachineKey")(function* (subkey) {
322 const api = yield* loadApi;
323 return yield* Effect.try({
324 try: () => readKey(api, subkey),
325 catch: (cause) =>
326 new WindowsRegistryError(
327 isWin32Failure(cause) ? { code: cause.code, cause } : { cause },
328 ),
329 });
330 }),
331 });
332 }),
333 );
334
335// A hung query must not hang startup; a timeout reads as a failed query.
336const run = Effect.fnUntraced(
337 function* (command: string, args: ReadonlyArray<string>) {
338 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
339 const handle = yield* spawner.spawn(
340 ChildProcess.make(command, args, { stdin: "ignore", stderr: "ignore" }),
341 );
342 const [stdout, exitCode] = yield* Effect.all(
343 [handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.exitCode],
344 { concurrency: 2 },
345 );
346 return { stdout, exitCode: Number(exitCode) };
347 },
348 Effect.scoped,
349 Effect.timeout(Duration.seconds(10)),
350);
351
352const decodeJsonExit = Schema.decodeUnknownExit(Schema.fromJsonString(Schema.Unknown));
353type PolicyRegistryValue = RegistryValue & {
354 /** The list subkey the value sits in, or "" for the policy key itself. */
355 readonly key: string;
356};
357
358const setPath = (target: Record<string, unknown>, path: ReadonlyArray<string>, value: unknown) => {
359 let node = target;
360 for (const segment of path.slice(0, -1)) {
361 node = (node[segment] ??= {}) as Record<string, unknown>;
362 }
363 node[path.at(-1)!] = value;
364};
365
366const dwordBoolean = (value: RegistryValue) =>
367 value.type !== "REG_DWORD"
368 ? undefined
369 : value.data === 0
370 ? false
371 : value.data === 1
372 ? true
373 : undefined;
374
375/**
376 * The document the Group Policy template's values describe, undefined when they describe none, or
377 * why they do not describe one.
378 */
379function windowsPolicyDocument(
380 values: ReadonlyArray<PolicyRegistryValue>,
381 knownDrivers: ReadonlyArray<string>,
382): { readonly document: unknown } | { readonly error: string } {
383 // Registry names ignore case; a hand-written `organization` is still Organization.
384 const canonical = new Map(
385 [...Object.keys(WINDOWS_POLICY_VALUES), ...Object.keys(WINDOWS_POLICY_GROUPS)].map((name) => [
386 name.toLowerCase(),
387 name,
388 ]),
389 );
390 values = values.map((value) =>
391 value.key === ""
392 ? { ...value, name: canonical.get(value.name.toLowerCase()) ?? value.name }
393 : value,
394 );
395 const document: Record<string, unknown> = { schemaVersion: 1 };
396 const lists = new Map<string, Array<string>>();
397 const disabled: Array<ReadonlyArray<string>> = [];
398 for (const value of values) {
399 if (value.key !== "" || !Object.hasOwn(WINDOWS_POLICY_GROUPS, value.name)) continue;
400 const enabled = dwordBoolean(value);
401 if (enabled === undefined) return { error: `${value.name} must be a DWORD of 0 or 1` };
402 if (!enabled) {
403 disabled.push(WINDOWS_POLICY_GROUPS[value.name as keyof typeof WINDOWS_POLICY_GROUPS]);
404 } else if (Object.hasOwn(WINDOWS_POLICY_LISTS, value.name)) {
405 lists.set(value.name, []);
406 }
407 }
408 const put = (path: ReadonlyArray<string>, value: unknown) => {
409 if (!disabled.some((prefix) => prefix.every((segment, index) => path[index] === segment))) {
410 setPath(document, path, value);
411 }
412 };
413 for (const value of values) {
414 const where = value.key === "" ? value.name : `${value.key}\\${value.name}`;
415 if (value.key !== "") {
416 const checked = dwordBoolean(value);
417 if (checked === undefined) return { error: `${where} must be a DWORD of 0 or 1` };
418 const list = lists.get(value.key) ?? [];
419 lists.set(value.key, list);
420 if (checked) {
421 const names = value.key === "Clients" ? ManagedPolicyClient.literals : knownDrivers;
422 list.push(
423 names.find((name) => name.toLowerCase() === value.name.toLowerCase()) ?? value.name,
424 );
425 }
426 continue;
427 }
428 if (Object.hasOwn(WINDOWS_POLICY_GROUPS, value.name)) continue;
429 if (!Object.hasOwn(WINDOWS_POLICY_VALUES, value.name)) {
430 return { error: `${value.name} is not a T3 Code policy value` };
431 }
432 const field = WINDOWS_POLICY_VALUES[value.name as keyof typeof WINDOWS_POLICY_VALUES];
433 if (field.type === "boolean") {
434 const flag = dwordBoolean(value);
435 if (flag === undefined) return { error: `${where} must be a DWORD of 0 or 1` };
436 put(field.path, flag);
437 continue;
438 }
439 if (value.type !== "REG_SZ") return { error: `${where} must be a string (REG_SZ)` };
440 // Group Policy writes an empty string for a box left blank.
441 if (value.data === "") continue;
442 if (field.type === "string") {
443 put(field.path, value.data);
444 continue;
445 }
446 const parsed = decodeJsonExit(value.data);
447 if (Exit.isFailure(parsed)) return { error: `${where} is not valid JSON` };
448 put(field.path, parsed.value);
449 }
450 for (const [key, list] of lists) {
451 put(WINDOWS_POLICY_LISTS[key as keyof typeof WINDOWS_POLICY_LISTS], list);
452 }
453 // Policies set to Disabled leave values behind that restrict nothing.
454 if (Object.keys(document).length === 1) return { document: undefined };
455 // The schema would name `["organization"]`, which an admin cannot map to a Group Policy setting.
456 if (document.organization === undefined) {
457 return { error: "set the Organization policy whenever another T3 Code policy is set" };
458 }
459 return { document };
460}
461
462// Strict: keys beside the wrapper, or more than one entry, decode as a plain document and fail.
463const decodeForcedExit = Schema.decodeUnknownExit(
464 Schema.Struct({
465 Forced: Schema.Tuple([Schema.Struct({ mcx_preference_settings: Schema.Unknown })]),
466 }),
467 { onExcessProperty: "error" },
468);
469
470// macOS writes `PayloadUUID` and `_manualProfile` beside the profile's own keys; no policy key
471// starts with `Payload` or `_`, so dropping them cannot hide a restriction.
472const withoutProfileKeys = (value: unknown) =>
473 value === null || typeof value !== "object" || Array.isArray(value)
474 ? value
475 : Object.fromEntries(
476 Object.entries(value).filter(([key]) => !key.startsWith("Payload") && !key.startsWith("_")),
477 );
478
479/**
480 * Installed managed preferences hold the profile's keys directly, but MCX-style profiles keep
481 * them under `Forced[0].mcx_preference_settings`; read either rather than refuse a valid profile.
482 */
483function unwrapForced(json: string): unknown {
484 const parsed = decodeJsonExit(json);
485 if (Exit.isFailure(parsed)) return json;
486 const preferences = withoutProfileKeys(parsed.value);
487 const forced = decodeForcedExit(preferences);
488 return Exit.isSuccess(forced)
489 ? withoutProfileKeys(forced.value.Forced[0].mcx_preference_settings)
490 : preferences;
491}
492
493/** Read the policy a configuration profile or Group Policy set; absent means no such policy. */
494const readPlatformPolicy = Effect.fnUntraced(function* (
495 platform: NodeJS.Platform,
496 knownDrivers: ReadonlyArray<string>,
497) {
498 if (platform === "darwin") {
499 const path = MACOS_MANAGED_PREFERENCES_PATH;
500 const exists = yield* (yield* FileSystem.FileSystem)
501 .exists(path)
502 .pipe(
503 Effect.mapError(
504 (cause) =>
505 new ManagedPolicyFileError({ path, reason: "the profile cannot be checked", cause }),
506 ),
507 );
508 if (!exists) return undefined;
509 const converted = yield* run(PLUTIL, ["-convert", "json", "-o", "-", path]).pipe(
510 Effect.mapError(
511 (cause) =>
512 new ManagedPolicyFileError({ path, reason: "the profile cannot be read", cause }),
513 ),
514 );
515 if (converted.exitCode !== 0) {
516 return yield* new ManagedPolicyFileError({ path, reason: "the profile cannot be read" });
517 }
518 return { source: path, document: yield* decodeDocument(path, unwrapForced(converted.stdout)) };
519 }
520 if (platform === "win32") {
521 const path = WINDOWS_POLICY_KEY;
522 const registry = yield* WindowsRegistry;
523 const readKey = (subkey: string) =>
524 registry.readMachineKey(subkey).pipe(
525 Effect.mapError(
526 (cause) =>
527 new ManagedPolicyFileError({
528 path,
529 reason:
530 cause.code === undefined
531 ? "this T3 Code install cannot load its registry reader; reinstall T3 Code"
532 : `the registry cannot be read (${cause.message})`,
533 cause,
534 }),
535 ),
536 );
537 const refuse = (reason: string) => new ManagedPolicyFileError({ path, reason });
538 const root = yield* readKey(WINDOWS_POLICY_SUBKEY);
539 if (root === undefined) return undefined;
540 const json = root.values.find((value) => value.name.toLowerCase() === "policy");
541 if (json !== undefined) {
542 // Not Configured clears a list policy's values but can leave its empty subkey behind.
543 let templateSettings = root.values.length > 1;
544 for (const subkey of root.subkeys) {
545 if (templateSettings) break;
546 const child = yield* readKey(`${WINDOWS_POLICY_SUBKEY}\\${subkey}`);
547 templateSettings =
548 child !== undefined && (child.values.length > 0 || child.subkeys.length > 0);
549 }
550 if (templateSettings) {
551 return yield* refuse(
552 "it holds both a Policy value and Group Policy settings; deploy the policy one way",
553 );
554 }
555 if (json.type !== "REG_SZ") return yield* refuse("its Policy value is not a string (REG_SZ)");
556 return { source: path, document: yield* decodeDocument(path, json.data) };
557 }
558 const values: Array<PolicyRegistryValue> = root.values.map((value) => ({ ...value, key: "" }));
559 for (const subkey of root.subkeys) {
560 // Registry names ignore case.
561 const list = Object.keys(WINDOWS_POLICY_LISTS).find(
562 (name) => name.toLowerCase() === subkey.toLowerCase(),
563 );
564 if (list === undefined) return yield* refuse(`${subkey} is not a T3 Code policy key`);
565 const checkboxes = yield* readKey(`${WINDOWS_POLICY_SUBKEY}\\${subkey}`);
566 if (checkboxes === undefined) continue;
567 if (checkboxes.subkeys.length > 0) {
568 return yield* refuse(`${subkey}\\${checkboxes.subkeys[0]} is not a T3 Code policy key`);
569 }
570 values.push(...checkboxes.values.map((value) => ({ ...value, key: list })));
571 }
572 const built = windowsPolicyDocument(values, knownDrivers);
573 if ("error" in built) return yield* refuse(built.error);
574 // An empty key, or one holding only policies set to Disabled, sets nothing.
575 if (built.document === undefined) return undefined;
576 return { source: path, document: yield* decodeDocument(path, built.document) };
577 }
578 return undefined;
579});
580
581/**
582 * Read the machine's policy from wherever an administrator put it: a configuration profile on
583 * macOS, the Policies registry key on Windows, or the policy file. Two sources are refused rather
584 * than merged, so what applies is never a mix nobody wrote.
585 */
586export const readManagedPolicy = Effect.fn("readManagedPolicy")(function* (
587 platform: NodeJS.Platform,
588 knownDrivers: ReadonlyArray<string> = [],
589) {
590 const filePath = managedPolicyPath(platform);
591 const [platformPolicy, file] = yield* Effect.all([
592 readPlatformPolicy(platform, knownDrivers),
593 readManagedPolicyFile(filePath),
594 ]);
595 if (platformPolicy !== undefined && file !== undefined) {
596 return yield* new ManagedPolicyFileError({
597 path: platformPolicy.source,
598 reason: `a policy file at ${filePath} also exists; deploy the policy one way`,
599 });
600 }
601 return platformPolicy ?? (file === undefined ? undefined : { source: filePath, document: file });
602});