@microck/canonfig 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/agent/agent-resolution.errors.js +42 -0
  4. package/dist/agent/agent-resolution.layer.js +204 -0
  5. package/dist/agent/agent-resolution.service.js +2259 -0
  6. package/dist/agent/agent-resolution.types.js +1 -0
  7. package/dist/agent/controlled-executor.js +704 -0
  8. package/dist/agent/harness-adapters.js +85 -0
  9. package/dist/cli/cli.js +618 -0
  10. package/dist/cli/exit-codes.js +28 -0
  11. package/dist/cli/follower-commands.js +3 -0
  12. package/dist/cli/render.js +56 -0
  13. package/dist/cli/source-commands.js +5 -0
  14. package/dist/domain/brand.js +29 -0
  15. package/dist/domain/identity.js +31 -0
  16. package/dist/domain/npm-package-spec.js +186 -0
  17. package/dist/domain/profile.js +950 -0
  18. package/dist/domain/recipe-versions.js +297 -0
  19. package/dist/domain/resource.js +259 -0
  20. package/dist/domain/synchronization.js +346 -0
  21. package/dist/enrollment/enrollment.errors.js +43 -0
  22. package/dist/enrollment/enrollment.layer.js +724 -0
  23. package/dist/enrollment/enrollment.service.js +3 -0
  24. package/dist/enrollment/enrollment.types.js +59 -0
  25. package/dist/enrollment/follower-client.js +585 -0
  26. package/dist/enrollment/source-server.js +313 -0
  27. package/dist/machine/linux.layer.js +1183 -0
  28. package/dist/machine/machine-state.errors.js +52 -0
  29. package/dist/machine/machine-state.service.js +3 -0
  30. package/dist/machine/machine-state.types.js +1 -0
  31. package/dist/machine/macos.layer.js +470 -0
  32. package/dist/machine/windows.layer.js +879 -0
  33. package/dist/profile/discovery.js +740 -0
  34. package/dist/profile/profile-catalog.errors.js +50 -0
  35. package/dist/profile/profile-catalog.layer.js +20 -0
  36. package/dist/profile/profile-catalog.service.js +7 -0
  37. package/dist/profile/profile-codec.js +153 -0
  38. package/dist/profile/publication.js +298 -0
  39. package/dist/profile/tool-catalog.js +384 -0
  40. package/dist/runtime/doctor.js +306 -0
  41. package/dist/runtime/layers.js +706 -0
  42. package/dist/runtime/main.js +38 -0
  43. package/dist/schedule/linux-schedule.js +24 -0
  44. package/dist/schedule/macos-schedule.js +25 -0
  45. package/dist/schedule/schedule-manager.errors.js +17 -0
  46. package/dist/schedule/schedule-manager.layer.js +205 -0
  47. package/dist/schedule/schedule-manager.service.js +3 -0
  48. package/dist/schedule/schedule-manager.types.js +114 -0
  49. package/dist/schedule/windows-schedule.js +25 -0
  50. package/dist/state/state-repository.errors.js +55 -0
  51. package/dist/state/state-repository.layer.js +1507 -0
  52. package/dist/state/state-repository.service.js +3 -0
  53. package/dist/state/state-repository.types.js +1 -0
  54. package/dist/state/state-schema.js +298 -0
  55. package/dist/synchronization/config-codec.js +97 -0
  56. package/dist/synchronization/executor.js +700 -0
  57. package/dist/synchronization/follower-orchestration.js +939 -0
  58. package/dist/synchronization/follower-sync-config.js +81 -0
  59. package/dist/synchronization/npm-artifact.js +670 -0
  60. package/dist/synchronization/planner.js +378 -0
  61. package/dist/synchronization/recovery.js +397 -0
  62. package/dist/synchronization/resource-executors.js +1198 -0
  63. package/dist/synchronization/resource-plans.js +645 -0
  64. package/dist/synchronization/synchronization.errors.js +102 -0
  65. package/dist/synchronization/synchronization.layer.js +97 -0
  66. package/dist/synchronization/synchronization.service.js +11 -0
  67. package/dist/synchronization/synchronization.types.js +1 -0
  68. package/package.json +66 -0
@@ -0,0 +1,879 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { access, lstat, mkdir, open, readFile, realpath, rename, rm, stat, symlink, unlink, writeFile, } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { win32 } from "node:path";
5
+ import { Effect, Layer, Redacted, Schema } from "effect";
6
+ import { CredentialReference, } from "../domain/brand.js";
7
+ import { CredentialStorageError, ExecutableNotFoundError, HumanActionRequiredError, InvalidMachinePathError, InvalidSchedulerJobError, MachineFilesystemError, } from "./machine-state.errors.js";
8
+ import { MachineState } from "./machine-state.service.js";
9
+ import { linuxMachineStateLayer } from "./linux.layer.js";
10
+ const decode = Schema.decodeUnknownSync;
11
+ class MissingPermissionIntent extends Error {
12
+ }
13
+ const environmentEntries = () => Object.entries(process.env).flatMap(([name, value]) => value === undefined ? [] : [{ name, value }]);
14
+ const environmentValue = (environment, name) => environment.find((entry) => entry.name.toUpperCase() === name.toUpperCase())?.value;
15
+ const errorCode = (cause) => cause instanceof Error && "code" in cause
16
+ ? String(cause.code)
17
+ : undefined;
18
+ const windowsPath = (absolute) => ({
19
+ platform: "windows",
20
+ absolute: win32.normalize(absolute),
21
+ });
22
+ const linuxPath = (path) => ({
23
+ platform: "linux",
24
+ absolute: path.absolute,
25
+ });
26
+ const requireWindowsPath = (path) => path.platform === "windows"
27
+ && win32.isAbsolute(path.absolute)
28
+ && !path.absolute.includes("\0")
29
+ ? Effect.succeed(linuxPath(path))
30
+ : Effect.fail(new InvalidMachinePathError({
31
+ path: path.absolute,
32
+ message: path.platform === "windows"
33
+ ? "a normalized absolute Windows path without NUL bytes is required"
34
+ : `expected a Windows path, received ${path.platform}`,
35
+ }));
36
+ export const windowsPrivateAclArguments = (path, user, directory) => [
37
+ path,
38
+ "/inheritance:r",
39
+ "/grant:r",
40
+ `${user}:${directory ? "(OI)(CI)" : ""}(F)`,
41
+ "/remove:g",
42
+ "*S-1-1-0",
43
+ "*S-1-5-11",
44
+ "*S-1-5-32-545",
45
+ ];
46
+ const normalizedPath = (input, home) => {
47
+ if (input.path.length === 0 || input.path.includes("\0")) {
48
+ return Effect.fail(new InvalidMachinePathError({
49
+ path: input.path,
50
+ message: "path must not be empty or contain NUL bytes",
51
+ }));
52
+ }
53
+ if (input.base !== undefined && input.base.platform !== "windows") {
54
+ return Effect.fail(new InvalidMachinePathError({
55
+ path: input.path,
56
+ message: `relative Windows paths cannot use a ${input.base.platform} base`,
57
+ }));
58
+ }
59
+ const expanded = input.path === "~"
60
+ ? home
61
+ : /^~[\\/]/u.test(input.path)
62
+ ? win32.join(home, input.path.slice(2))
63
+ : input.path;
64
+ return Effect.succeed(windowsPath(win32.resolve(input.base?.absolute ?? process.cwd(), expanded)));
65
+ };
66
+ const validateSingleLine = (value, field) => value.trim().length > 0 && !/[\n\r\0]/u.test(value)
67
+ ? Effect.succeed(value)
68
+ : Effect.fail(new InvalidSchedulerJobError({
69
+ field,
70
+ message: `${field} must be non-empty, single-line, and contain no NUL bytes`,
71
+ }));
72
+ const powershellLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
73
+ const weekdayMasks = {
74
+ Sun: 1,
75
+ Mon: 2,
76
+ Tue: 4,
77
+ Wed: 8,
78
+ Thu: 16,
79
+ Fri: 32,
80
+ Sat: 64,
81
+ };
82
+ const windowsCommandLineArgument = (value) => {
83
+ if (value.length > 0 && !/[\s"]/u.test(value))
84
+ return value;
85
+ let output = "\"";
86
+ let backslashes = 0;
87
+ for (const character of value) {
88
+ if (character === "\\") {
89
+ backslashes += 1;
90
+ continue;
91
+ }
92
+ if (character === "\"") {
93
+ output += "\\".repeat(backslashes * 2 + 1) + "\"";
94
+ backslashes = 0;
95
+ continue;
96
+ }
97
+ output += "\\".repeat(backslashes) + character;
98
+ backslashes = 0;
99
+ }
100
+ return output + "\\".repeat(backslashes * 2) + "\"";
101
+ };
102
+ const taskCalendar = (calendar) => {
103
+ if (calendar.kind === "systemd-on-calendar") {
104
+ return Effect.fail(new InvalidSchedulerJobError({
105
+ field: "calendar.kind",
106
+ message: "systemd calendar expressions are not supported by Task Scheduler",
107
+ }));
108
+ }
109
+ if (!/^([01]\d|2[0-3]):[0-5]\d$/u.test(calendar.localTime)) {
110
+ return Effect.fail(new InvalidSchedulerJobError({
111
+ field: "calendar.localTime",
112
+ message: "local time must use 24-hour HH:mm format",
113
+ }));
114
+ }
115
+ return Effect.succeed(calendar.kind === "daily"
116
+ ? `New-ScheduledTaskTrigger -Daily -At ${powershellLiteral(calendar.localTime)}`
117
+ : `New-ScheduledTaskTrigger -Weekly -DaysOfWeek ${calendar.weekdays.join(",")} `
118
+ + `-At ${powershellLiteral(calendar.localTime)}`);
119
+ };
120
+ const renderTaskSchedulerJob = (job) => Effect.gen(function* () {
121
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/u.test(job.name)) {
122
+ return yield* new InvalidSchedulerJobError({
123
+ field: "name",
124
+ message: "job name must be a portable Task Scheduler name",
125
+ });
126
+ }
127
+ const description = yield* validateSingleLine(job.description, "description");
128
+ const executable = yield* requireWindowsPath(job.executable);
129
+ const arguments_ = yield* Effect.forEach(job.arguments, (argument, index) => validateSingleLine(argument, `arguments[${index}]`));
130
+ const trigger = yield* taskCalendar(job.calendar);
131
+ const commandLine = arguments_.map(windowsCommandLineArgument).join(" ");
132
+ const taskName = `Canonfig\\${job.name}`;
133
+ const fingerprint = createHash("sha256")
134
+ .update(JSON.stringify({
135
+ executable: executable.absolute,
136
+ arguments: arguments_,
137
+ trigger,
138
+ }))
139
+ .digest("hex");
140
+ const ownedDescription = `${description} [canonfig:${fingerprint}]`;
141
+ return {
142
+ platform: "windows",
143
+ mechanism: "task-scheduler",
144
+ serviceName: taskName,
145
+ service: [
146
+ `$Action = New-ScheduledTaskAction -Execute ${powershellLiteral(executable.absolute)} `
147
+ + `-Argument ${powershellLiteral(commandLine)}`,
148
+ `$Principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive`,
149
+ `$Settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1)`,
150
+ ].join("\r\n"),
151
+ schedule: [
152
+ `$Trigger = ${trigger}`,
153
+ `Register-ScheduledTask -TaskName ${powershellLiteral(taskName)} `
154
+ + `-Description ${powershellLiteral(ownedDescription)} -Action $Action `
155
+ + "-Trigger $Trigger -Principal $Principal -Settings $Settings",
156
+ ].join("\r\n"),
157
+ };
158
+ });
159
+ const credentialKey = (reference) => {
160
+ const prefix = "credential-manager:";
161
+ const value = String(reference);
162
+ if (!value.startsWith(prefix) || value.length === prefix.length) {
163
+ return Effect.fail(new CredentialStorageError({
164
+ operation: "resolve credential reference",
165
+ reference: value,
166
+ message: "credential reference is not owned by Windows Credential Manager",
167
+ }));
168
+ }
169
+ return Effect.succeed(value.slice(prefix.length));
170
+ };
171
+ const localCredentialPath = (reference, root) => {
172
+ const prefix = "local-file:";
173
+ const value = String(reference);
174
+ if (!value.startsWith(prefix)) {
175
+ return Effect.fail(new CredentialStorageError({
176
+ operation: "resolve credential reference",
177
+ reference: value,
178
+ message: "credential reference is not owned by the local-file provider",
179
+ }));
180
+ }
181
+ const path = win32.resolve(value.slice(prefix.length));
182
+ if (win32.dirname(path).toLowerCase() !== root.toLowerCase()) {
183
+ return Effect.fail(new CredentialStorageError({
184
+ operation: "resolve credential reference",
185
+ reference: value,
186
+ message: "credential reference is outside the configured credential directory",
187
+ }));
188
+ }
189
+ return Effect.succeed(path);
190
+ };
191
+ const credentialScript = {
192
+ store: [
193
+ "$vault = New-Object Windows.Security.Credentials.PasswordVault",
194
+ "$credential = New-Object Windows.Security.Credentials.PasswordCredential("
195
+ + "$env:CANONFIG_TARGET,'canonfig',$env:CANONFIG_SECRET)",
196
+ "$vault.Add($credential)",
197
+ ].join(";"),
198
+ load: [
199
+ "$vault = New-Object Windows.Security.Credentials.PasswordVault",
200
+ "$credential = $vault.Retrieve($env:CANONFIG_TARGET,'canonfig')",
201
+ "$credential.RetrievePassword()",
202
+ "[Console]::Out.Write($credential.Password)",
203
+ ].join(";"),
204
+ remove: [
205
+ "$vault = New-Object Windows.Security.Credentials.PasswordVault",
206
+ "$credential = $vault.Retrieve($env:CANONFIG_TARGET,'canonfig')",
207
+ "$vault.Remove($credential)",
208
+ ].join(";"),
209
+ };
210
+ export const windowsMachineStateLayer = (options = {}) => {
211
+ const environment = options.environment ?? environmentEntries();
212
+ const home = environmentValue(environment, "USERPROFILE")
213
+ ?? environmentValue(environment, "HOME")
214
+ ?? homedir();
215
+ const policy = options.credentialPolicy ?? { kind: "secure-store" };
216
+ const localCredentialRoot = policy.kind === "local-file"
217
+ ? win32.resolve(policy.path)
218
+ : undefined;
219
+ const powershell = environmentValue(environment, "CANONFIG_POWERSHELL")
220
+ ?? win32.join(environmentValue(environment, "SystemRoot") ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
221
+ const icacls = environmentValue(environment, "CANONFIG_ICACLS")
222
+ ?? win32.join(environmentValue(environment, "SystemRoot") ?? "C:\\Windows", "System32", "icacls.exe");
223
+ const userName = environmentValue(environment, "USERNAME")
224
+ ?? process.env.USERNAME
225
+ ?? win32.basename(home);
226
+ const userDomain = environmentValue(environment, "USERDOMAIN")
227
+ ?? process.env.USERDOMAIN;
228
+ const currentUser = userDomain === undefined
229
+ ? userName
230
+ : `${userDomain}\\${userName}`;
231
+ const base = linuxMachineStateLayer({
232
+ credentialPolicy: policy,
233
+ environment,
234
+ });
235
+ return Layer.effect(MachineState, Effect.gen(function* () {
236
+ const machine = yield* MachineState;
237
+ const semanticModes = new Map();
238
+ const filesystemFailure = (operation, path, cause) => new MachineFilesystemError({
239
+ operation,
240
+ path,
241
+ message: cause instanceof Error ? cause.message : String(cause),
242
+ });
243
+ const setPrivateAcl = (path, directory) => machine.runProcess({
244
+ executable: { platform: "linux", absolute: icacls },
245
+ arguments: windowsPrivateAclArguments(path, currentUser, directory),
246
+ timeoutMilliseconds: 10_000,
247
+ maximumOutputBytes: 1024 * 1024,
248
+ }).pipe(Effect.flatMap((result) => result.exitCode === 0
249
+ ? Effect.void
250
+ : Effect.fail(new MachineFilesystemError({
251
+ operation: "restrict Windows ACL",
252
+ path,
253
+ message: "icacls did not apply the requested current-user ACL",
254
+ }))));
255
+ const writeSemanticMode = (path, mode) => Effect.tryPromise({
256
+ try: () => writeFile(`${path}:canonfig.mode`, mode.toString(8), "utf8"),
257
+ catch: (cause) => filesystemFailure("record Windows permission intent", path, cause),
258
+ }).pipe(Effect.tap(() => Effect.sync(() => semanticModes.set(win32.normalize(path), mode))));
259
+ const readSemanticMode = (path, fallback) => {
260
+ const remembered = semanticModes.get(win32.normalize(path));
261
+ if (remembered !== undefined)
262
+ return Effect.succeed(remembered);
263
+ return Effect.tryPromise({
264
+ try: () => readFile(`${path}:canonfig.mode`, "utf8"),
265
+ catch: (cause) => {
266
+ const code = cause instanceof Error && "code" in cause
267
+ ? String(cause.code)
268
+ : "";
269
+ return code === "ENOENT"
270
+ ? new MissingPermissionIntent()
271
+ : filesystemFailure("read Windows permission intent", path, cause);
272
+ },
273
+ }).pipe(Effect.catchIf((cause) => cause instanceof MissingPermissionIntent, () => Effect.succeed(undefined)), Effect.flatMap((encoded) => {
274
+ if (encoded === undefined)
275
+ return Effect.succeed(fallback);
276
+ const mode = Number.parseInt(encoded, 8);
277
+ return Number.isSafeInteger(mode) && mode >= 0 && mode <= 0o7777
278
+ ? Effect.succeed(mode)
279
+ : Effect.fail(new MachineFilesystemError({
280
+ operation: "read Windows permission intent",
281
+ path,
282
+ message: "stored permission intent is invalid",
283
+ }));
284
+ }));
285
+ };
286
+ const secureDirectory = (path, mode) => Effect.tryPromise({
287
+ try: () => mkdir(path, { recursive: true }).then(() => undefined),
288
+ catch: (cause) => filesystemFailure("ensure Windows directory", path, cause),
289
+ }).pipe(Effect.andThen(setPrivateAcl(path, true)), Effect.andThen(writeSemanticMode(path, mode)));
290
+ const secureAtomicWrite = (path, content, mode) => {
291
+ const parent = win32.dirname(path);
292
+ const temporary = win32.join(parent, `.${win32.basename(path)}.canonfig-${randomBytes(12).toString("hex")}`);
293
+ return Effect.gen(function* () {
294
+ yield* secureDirectory(parent, semanticModes.get(parent) ?? 0o700);
295
+ yield* Effect.tryPromise({
296
+ try: async () => {
297
+ let handle;
298
+ try {
299
+ handle = await open(temporary, "wx");
300
+ await handle.writeFile(content);
301
+ await handle.sync().catch((cause) => {
302
+ if (cause.code !== "EPERM" && cause.code !== "EINVAL")
303
+ throw cause;
304
+ });
305
+ await handle.close();
306
+ handle = undefined;
307
+ }
308
+ finally {
309
+ if (handle !== undefined) {
310
+ await handle.close().catch(() => undefined);
311
+ }
312
+ }
313
+ },
314
+ catch: (cause) => filesystemFailure("atomically write Windows file", path, cause),
315
+ });
316
+ yield* setPrivateAcl(temporary, false);
317
+ yield* Effect.tryPromise({
318
+ try: () => rename(temporary, path),
319
+ catch: (cause) => filesystemFailure("replace Windows file", path, cause),
320
+ });
321
+ yield* setPrivateAcl(path, false);
322
+ yield* writeSemanticMode(path, mode);
323
+ }).pipe(Effect.ensuring(Effect.promise(() => rm(temporary, { force: true }).catch(() => undefined))));
324
+ };
325
+ const isWithinRoot = (root, candidate) => {
326
+ const remainder = win32.relative(root.toLowerCase(), candidate.toLowerCase());
327
+ return remainder === ""
328
+ || (!remainder.startsWith(`..${win32.sep}`)
329
+ && remainder !== ".."
330
+ && !win32.isAbsolute(remainder));
331
+ };
332
+ const validatePathWithinRoot = (root, path) => {
333
+ if (!isWithinRoot(root, path) || path.toLowerCase() === root.toLowerCase()) {
334
+ return Effect.fail(new MachineFilesystemError({
335
+ operation: "validate managed path containment",
336
+ path,
337
+ message: `path is not a descendant of managed root ${root}`,
338
+ }));
339
+ }
340
+ return Effect.tryPromise({
341
+ try: async () => {
342
+ const rootBefore = await lstat(root);
343
+ const actualRoot = await realpath(root);
344
+ const rootAfter = await lstat(root);
345
+ if (rootBefore.dev !== rootAfter.dev || rootBefore.ino !== rootAfter.ino) {
346
+ throw new Error("managed root identity changed during validation");
347
+ }
348
+ const ancestors = [];
349
+ for (let ancestor = win32.dirname(path);; ancestor = win32.dirname(ancestor)) {
350
+ ancestors.push(ancestor);
351
+ if (ancestor.toLowerCase() === root.toLowerCase())
352
+ break;
353
+ if (ancestor === win32.dirname(ancestor)) {
354
+ throw new Error(`managed path ancestry did not reach root ${root}`);
355
+ }
356
+ }
357
+ ancestors.reverse();
358
+ for (const ancestor of ancestors) {
359
+ let before;
360
+ try {
361
+ before = await lstat(ancestor);
362
+ }
363
+ catch (cause) {
364
+ if (errorCode(cause) === "ENOENT")
365
+ break;
366
+ throw cause;
367
+ }
368
+ const actualAncestor = await realpath(ancestor);
369
+ const after = await lstat(ancestor);
370
+ if (before.dev !== after.dev || before.ino !== after.ino) {
371
+ throw new Error(`ancestor identity changed during validation: ${ancestor}`);
372
+ }
373
+ if (!isWithinRoot(actualRoot, actualAncestor)) {
374
+ throw new Error(`ancestor resolves outside managed root ${root}: ${ancestor}`);
375
+ }
376
+ }
377
+ },
378
+ catch: (cause) => filesystemFailure("validate managed path containment", path, cause),
379
+ });
380
+ };
381
+ const mutateWithinRoot = (input) => Effect.gen(function* () {
382
+ const rootPath = yield* requireWindowsPath(input.root);
383
+ const targetPath = yield* requireWindowsPath(input.path);
384
+ const linkTarget = input.mutation.kind === "symlink"
385
+ ? yield* requireWindowsPath(input.mutation.target)
386
+ : undefined;
387
+ const root = rootPath.absolute;
388
+ const path = targetPath.absolute;
389
+ if (!isWithinRoot(root, path)
390
+ || path.toLowerCase() === root.toLowerCase()) {
391
+ return yield* new MachineFilesystemError({
392
+ operation: "mutate managed path",
393
+ path,
394
+ message: `path is not a descendant of managed root ${root}`,
395
+ });
396
+ }
397
+ yield* Effect.tryPromise({
398
+ try: async () => {
399
+ const rootBefore = await lstat(root);
400
+ if (rootBefore.isSymbolicLink()) {
401
+ throw new Error("managed root must not be a reparse point");
402
+ }
403
+ await options.beforeSafeRootMutation?.();
404
+ const rootAfter = await lstat(root);
405
+ if (rootAfter.isSymbolicLink()
406
+ || rootBefore.dev !== rootAfter.dev
407
+ || rootBefore.ino !== rootAfter.ino) {
408
+ throw new Error("managed root identity changed before mutation");
409
+ }
410
+ const relativePath = win32.relative(root, path);
411
+ const [topName, ...tail] = relativePath.split(/[\\/]/u);
412
+ const guard = win32.join(root, `.canonfig-guard-${randomBytes(12).toString("hex")}`);
413
+ const visibleTop = win32.join(root, topName);
414
+ const heldTop = win32.join(guard, topName);
415
+ let held = false;
416
+ await mkdir(guard);
417
+ try {
418
+ try {
419
+ const top = await lstat(visibleTop);
420
+ if (tail.length > 0 && top.isSymbolicLink()) {
421
+ throw new Error(`managed ancestor is a reparse point: ${visibleTop}`);
422
+ }
423
+ await rename(visibleTop, heldTop);
424
+ held = true;
425
+ if (tail.length > 0 && (await lstat(heldTop)).isSymbolicLink()) {
426
+ throw new Error(`managed ancestor is a reparse point: ${visibleTop}`);
427
+ }
428
+ }
429
+ catch (cause) {
430
+ if (errorCode(cause) !== "ENOENT")
431
+ throw cause;
432
+ if (input.mutation.kind === "remove")
433
+ return;
434
+ if (tail.length > 0) {
435
+ await mkdir(heldTop, { recursive: true });
436
+ held = true;
437
+ }
438
+ }
439
+ const guardedTarget = tail.length === 0
440
+ ? heldTop
441
+ : win32.join(heldTop, ...tail);
442
+ if (tail.length > 0) {
443
+ await Effect.runPromise(validatePathWithinRoot(heldTop, guardedTarget));
444
+ }
445
+ if (input.mutation.kind === "remove") {
446
+ await rm(guardedTarget, { force: true });
447
+ if (tail.length === 0)
448
+ held = false;
449
+ }
450
+ else if (input.mutation.kind === "write") {
451
+ await Effect.runPromise(secureAtomicWrite(guardedTarget, input.mutation.content, input.mutation.mode ?? 0o600));
452
+ held = true;
453
+ }
454
+ else {
455
+ await mkdir(win32.dirname(guardedTarget), { recursive: true });
456
+ const temporary = win32.join(win32.dirname(guardedTarget), `.${win32.basename(guardedTarget)}.canonfig-${randomBytes(12).toString("hex")}`);
457
+ try {
458
+ await symlink(linkTarget.absolute, temporary);
459
+ await rename(temporary, guardedTarget);
460
+ }
461
+ finally {
462
+ await unlink(temporary).catch(() => undefined);
463
+ }
464
+ held = true;
465
+ }
466
+ const visibleRoot = await lstat(root);
467
+ if (visibleRoot.isSymbolicLink()
468
+ || visibleRoot.dev !== rootBefore.dev
469
+ || visibleRoot.ino !== rootBefore.ino) {
470
+ throw new Error("managed root identity changed during mutation");
471
+ }
472
+ if (held) {
473
+ await rename(heldTop, visibleTop);
474
+ held = false;
475
+ }
476
+ }
477
+ finally {
478
+ if (!held) {
479
+ await rm(guard, { recursive: true, force: true }).catch(() => undefined);
480
+ }
481
+ }
482
+ },
483
+ catch: (cause) => filesystemFailure("mutate managed path", path, cause),
484
+ });
485
+ });
486
+ const secureStoreAvailable = Effect.promise(() => options.credentialStoreAccess !== "unavailable"
487
+ && process.platform === "win32"
488
+ ? access(powershell).then(() => true).catch(() => false)
489
+ : Promise.resolve(false));
490
+ const requirePowerShell = Effect.gen(function* () {
491
+ if (yield* secureStoreAvailable)
492
+ return powershell;
493
+ return yield* new HumanActionRequiredError({
494
+ action: "configure Windows credential storage",
495
+ recovery: "Run on Windows with Credential Manager available, or explicitly select the local-file credential policy.",
496
+ });
497
+ });
498
+ const runCredentialScript = (script, additions) => machine.runProcess({
499
+ executable: { platform: "linux", absolute: powershell },
500
+ arguments: [
501
+ "-NoLogo",
502
+ "-NoProfile",
503
+ "-NonInteractive",
504
+ "-Command",
505
+ script,
506
+ ],
507
+ environment: additions,
508
+ timeoutMilliseconds: 5_000,
509
+ maximumOutputBytes: 1024 * 1024,
510
+ });
511
+ const runSchedulerScript = (script) => machine.runProcess({
512
+ executable: { platform: "linux", absolute: powershell },
513
+ arguments: [
514
+ "-NoLogo",
515
+ "-NoProfile",
516
+ "-NonInteractive",
517
+ "-Command",
518
+ script,
519
+ ],
520
+ timeoutMilliseconds: 10_000,
521
+ maximumOutputBytes: 1024 * 1024,
522
+ });
523
+ const nativeScheduler = {
524
+ inspect: (expected) => {
525
+ const fingerprint = /\[canonfig:([a-f0-9]{64})\]/u.exec(expected.schedule)?.[1];
526
+ const action = /-Execute '((?:''|[^'])*)' -Argument '((?:''|[^'])*)'/u
527
+ .exec(expected.service);
528
+ const daily = /-Daily -At '([0-2]\d:[0-5]\d)'/u.exec(expected.schedule);
529
+ const weekly = /-Weekly -DaysOfWeek ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)(?:,(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))*) -At '([0-2]\d:[0-5]\d)'/u
530
+ .exec(expected.schedule);
531
+ const taskName = powershellLiteral(expected.serviceName);
532
+ const script = [
533
+ `$Task = Get-ScheduledTask -TaskName ${taskName} -ErrorAction SilentlyContinue`,
534
+ "if ($null -eq $Task) { [Console]::Out.Write('missing') } else {",
535
+ " $Trigger = $Task.Triggers[0]",
536
+ " [ordered]@{",
537
+ " Description = $Task.Description",
538
+ " Enabled = $Task.Settings.Enabled",
539
+ " Execute = $Task.Actions[0].Execute",
540
+ " Arguments = $Task.Actions[0].Arguments",
541
+ " TriggerType = $Trigger.CimClass.CimClassName",
542
+ " StartBoundary = $Trigger.StartBoundary",
543
+ " DaysOfWeek = $Trigger.DaysOfWeek",
544
+ " } | ConvertTo-Json -Compress",
545
+ "}",
546
+ ].join("\r\n");
547
+ return runSchedulerScript(script).pipe(Effect.flatMap((result) => {
548
+ const output = Buffer.from(result.standardOutput).toString("utf8");
549
+ if (result.exitCode === 0 && output.trim() === "missing") {
550
+ return Effect.succeed({ installed: false, enabled: false, matches: false });
551
+ }
552
+ if (result.exitCode !== 0) {
553
+ return Effect.fail(new HumanActionRequiredError({
554
+ action: "inspect the Canonfig scheduled task",
555
+ recovery: "Task Scheduler inspection failed; ensure per-user Task Scheduler access is available and retry.",
556
+ }));
557
+ }
558
+ let actual;
559
+ try {
560
+ actual = JSON.parse(output);
561
+ }
562
+ catch {
563
+ return Effect.fail(new HumanActionRequiredError({
564
+ action: "inspect the Canonfig scheduled task",
565
+ recovery: "Task Scheduler inspection returned invalid data; ensure per-user Task Scheduler access is available and retry.",
566
+ }));
567
+ }
568
+ const expectedExecutable = action?.[1]?.replaceAll("''", "'");
569
+ const expectedArguments = action?.[2]?.replaceAll("''", "'");
570
+ const expectedTime = daily?.[1] ?? weekly?.[2];
571
+ const weekdayMask = weekly === null
572
+ ? undefined
573
+ : weekly[1].split(",").reduce((mask, weekday) => {
574
+ if (!Object.hasOwn(weekdayMasks, weekday))
575
+ return mask;
576
+ // SAFETY: Object.hasOwn above narrows this regex-captured weekday
577
+ // to one of the seven literal Task Scheduler weekday keys.
578
+ const normalizedWeekday = weekday;
579
+ return mask + weekdayMasks[normalizedWeekday];
580
+ }, 0);
581
+ const triggerMatches = expectedTime !== undefined
582
+ && actual.StartBoundary?.includes(`T${expectedTime}:00`) === true
583
+ && (daily !== null
584
+ ? actual.TriggerType?.includes("Daily") === true
585
+ : actual.TriggerType?.includes("Weekly") === true
586
+ && actual.DaysOfWeek === weekdayMask);
587
+ return Effect.succeed({
588
+ installed: true,
589
+ enabled: actual.Enabled === true,
590
+ matches: fingerprint !== undefined
591
+ && actual.Description?.includes(`[canonfig:${fingerprint}]`) === true
592
+ && actual.Execute === expectedExecutable
593
+ && actual.Arguments === expectedArguments
594
+ && triggerMatches,
595
+ });
596
+ }));
597
+ },
598
+ snapshot: (expected) => {
599
+ const taskName = powershellLiteral(expected.serviceName);
600
+ const script = [
601
+ `$Task = Get-ScheduledTask -TaskName ${taskName} -ErrorAction SilentlyContinue`,
602
+ "if ($null -eq $Task) { [Console]::Out.Write('missing') } else {",
603
+ " $Xml = Export-ScheduledTask -TaskName $Task.TaskName -TaskPath $Task.TaskPath",
604
+ " [ordered]@{ Enabled = $Task.Settings.Enabled; Xml = $Xml } | ConvertTo-Json -Compress",
605
+ "}",
606
+ ].join("\r\n");
607
+ return runSchedulerScript(script).pipe(Effect.flatMap((result) => {
608
+ const output = Buffer.from(result.standardOutput).toString("utf8");
609
+ if (result.exitCode === 0 && output.trim() === "missing") {
610
+ return Effect.succeed({
611
+ state: "absent",
612
+ platform: expected.platform,
613
+ mechanism: expected.mechanism,
614
+ serviceName: expected.serviceName,
615
+ });
616
+ }
617
+ if (result.exitCode !== 0) {
618
+ return Effect.fail(new HumanActionRequiredError({
619
+ action: "capture the Canonfig scheduled task",
620
+ recovery: "Task Scheduler inspection failed; ensure per-user Task Scheduler access is available and retry.",
621
+ }));
622
+ }
623
+ try {
624
+ const actual = Schema.decodeUnknownSync(Schema.Struct({
625
+ Enabled: Schema.optional(Schema.Boolean),
626
+ Xml: Schema.optional(Schema.String),
627
+ }))(JSON.parse(output));
628
+ if (actual.Xml === undefined || actual.Enabled === undefined) {
629
+ throw new Error("Task Scheduler export was incomplete");
630
+ }
631
+ return Effect.succeed({
632
+ state: "present",
633
+ platform: expected.platform,
634
+ mechanism: expected.mechanism,
635
+ serviceName: expected.serviceName,
636
+ enabled: actual.Enabled,
637
+ active: actual.Enabled,
638
+ servicePresent: false,
639
+ schedulePresent: false,
640
+ native: Buffer.from(actual.Xml, "utf8").toString("base64"),
641
+ });
642
+ }
643
+ catch (error) {
644
+ return Effect.fail(new HumanActionRequiredError({
645
+ action: "capture the Canonfig scheduled task",
646
+ recovery: `Task Scheduler export was invalid: ${String(error)}`,
647
+ }));
648
+ }
649
+ }));
650
+ },
651
+ install: (definition) => runSchedulerScript(`${definition.service}\r\n${definition.schedule}`).pipe(Effect.flatMap((result) => result.exitCode === 0
652
+ ? Effect.void
653
+ : Effect.fail(new HumanActionRequiredError({
654
+ action: "register the Canonfig scheduled task",
655
+ recovery: "Sign in interactively and ensure per-user Task Scheduler access is available, then retry.",
656
+ })))),
657
+ remove: (definition) => runSchedulerScript(`Unregister-ScheduledTask -TaskName ${powershellLiteral(definition.serviceName)} -Confirm:$false -ErrorAction SilentlyContinue`).pipe(Effect.flatMap((result) => result.exitCode === 0
658
+ ? Effect.void
659
+ : Effect.fail(new HumanActionRequiredError({
660
+ action: "remove the Canonfig scheduled task",
661
+ recovery: "Sign in interactively and ensure per-user Task Scheduler access is available, then retry.",
662
+ })))),
663
+ restore: (expected, snapshot) => {
664
+ const taskName = powershellLiteral(expected.serviceName);
665
+ return Effect.gen(function* () {
666
+ yield* runSchedulerScript(`Unregister-ScheduledTask -TaskName ${taskName} -Confirm:$false -ErrorAction SilentlyContinue`).pipe(Effect.ignore);
667
+ if (snapshot.state === "absent")
668
+ return;
669
+ if (snapshot.native === undefined) {
670
+ return yield* new HumanActionRequiredError({
671
+ action: "restore the Canonfig scheduled task",
672
+ recovery: "The captured Task Scheduler export was incomplete; inspect the task manually.",
673
+ });
674
+ }
675
+ const script = [
676
+ `$Xml = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${snapshot.native}'))`,
677
+ `Register-ScheduledTask -TaskName ${taskName} -Xml $Xml -Force`,
678
+ ].join("\r\n");
679
+ const result = yield* runSchedulerScript(script);
680
+ if (result.exitCode !== 0) {
681
+ return yield* new HumanActionRequiredError({
682
+ action: "restore the Canonfig scheduled task",
683
+ recovery: "Sign in interactively and ensure per-user Task Scheduler access is available, then retry.",
684
+ });
685
+ }
686
+ });
687
+ },
688
+ };
689
+ const scheduler = options.schedulerBackend ?? nativeScheduler;
690
+ return MachineState.of({
691
+ normalizePath: (input) => normalizedPath(input, home),
692
+ userDirectories: () => Effect.succeed({
693
+ home: windowsPath(home),
694
+ config: windowsPath(environmentValue(environment, "APPDATA")
695
+ ?? win32.join(home, "AppData", "Roaming")),
696
+ data: windowsPath(environmentValue(environment, "LOCALAPPDATA")
697
+ ?? win32.join(home, "AppData", "Local")),
698
+ cache: windowsPath(environmentValue(environment, "LOCALAPPDATA")
699
+ ?? win32.join(home, "AppData", "Local")),
700
+ }),
701
+ ensureDirectory: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => secureDirectory(path.absolute, input.mode ?? 0o700))),
702
+ atomicWrite: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => secureAtomicWrite(path.absolute, input.content, input.mode ?? 0o600))),
703
+ readFile: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => machine.readFile({ ...input, path }))),
704
+ removeFile: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => machine.removeFile({ ...input, path }))),
705
+ removeEmptyDirectory: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => machine.removeEmptyDirectory({ ...input, path }))),
706
+ validatePathWithinRoot: (input) => Effect.all({
707
+ root: requireWindowsPath(input.root),
708
+ path: requireWindowsPath(input.path),
709
+ }).pipe(Effect.flatMap(({ root, path }) => validatePathWithinRoot(root.absolute, path.absolute))),
710
+ mutateWithinRoot,
711
+ replaceSymlink: (input) => Effect.all({
712
+ path: requireWindowsPath(input.path),
713
+ target: requireWindowsPath(input.target),
714
+ }).pipe(Effect.flatMap(machine.replaceSymlink)),
715
+ readSymlink: (path) => requireWindowsPath(path).pipe(Effect.flatMap(machine.readSymlink), Effect.map((target) => windowsPath(target.absolute))),
716
+ inspectPath: (path) => requireWindowsPath(path).pipe(Effect.flatMap(machine.inspectPath)),
717
+ setPermissions: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => Effect.tryPromise({
718
+ try: () => stat(path.absolute),
719
+ catch: (cause) => filesystemFailure("inspect Windows file type", path.absolute, cause),
720
+ }).pipe(Effect.flatMap((metadata) => setPrivateAcl(path.absolute, metadata.isDirectory())), Effect.andThen(writeSemanticMode(path.absolute, input.mode))))),
721
+ permissions: (path) => requireWindowsPath(path).pipe(Effect.flatMap((nativePath) => Effect.tryPromise({
722
+ try: () => lstat(nativePath.absolute),
723
+ catch: (cause) => filesystemFailure("inspect Windows permissions", nativePath.absolute, cause),
724
+ }).pipe(Effect.flatMap((metadata) => readSemanticMode(nativePath.absolute, metadata.isDirectory() ? 0o700 : 0o600)), Effect.map((mode) => ({
725
+ mode,
726
+ executableByOwner: (mode & 0o100) !== 0,
727
+ }))))),
728
+ findExecutable: (query) => {
729
+ if (query.name.length === 0
730
+ || /[\\/\0]/u.test(query.name)) {
731
+ return Effect.fail(new ExecutableNotFoundError({ name: query.name }));
732
+ }
733
+ const names = win32.extname(query.name).length > 0
734
+ ? [query.name]
735
+ : (environmentValue(environment, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD")
736
+ .split(";")
737
+ .map((extension) => `${query.name}${extension.toLowerCase()}`);
738
+ const directories = query.searchPath
739
+ ?? (environmentValue(environment, "PATH") ?? "")
740
+ .split(";")
741
+ .filter((entry) => entry.length > 0)
742
+ .map(windowsPath);
743
+ return Effect.gen(function* () {
744
+ for (const directory of directories) {
745
+ yield* requireWindowsPath(directory);
746
+ for (const name of names) {
747
+ const candidate = win32.join(directory.absolute, name);
748
+ const available = yield* Effect.promise(() => access(candidate).then(() => true).catch(() => false));
749
+ if (available) {
750
+ return { name: query.name, path: windowsPath(candidate) };
751
+ }
752
+ }
753
+ }
754
+ return yield* new ExecutableNotFoundError({ name: query.name });
755
+ });
756
+ },
757
+ runProcess: (invocation) => Effect.all({
758
+ executable: requireWindowsPath(invocation.executable),
759
+ workingDirectory: invocation.workingDirectory === undefined
760
+ ? Effect.succeed(undefined)
761
+ : requireWindowsPath(invocation.workingDirectory),
762
+ }).pipe(Effect.flatMap(({ executable, workingDirectory }) => machine.runProcess({ ...invocation, executable, workingDirectory }))),
763
+ digestFile: (input) => requireWindowsPath(input.path).pipe(Effect.flatMap((path) => machine.digestFile({ ...input, path }))),
764
+ credentialCapability: () => {
765
+ if (localCredentialRoot !== undefined) {
766
+ return Effect.succeed({
767
+ kind: "local-file",
768
+ path: windowsPath(localCredentialRoot),
769
+ });
770
+ }
771
+ return secureStoreAvailable.pipe(Effect.map((available) => available
772
+ ? {
773
+ kind: "secure-noninteractive",
774
+ provider: "credential-manager",
775
+ }
776
+ : {
777
+ kind: "unavailable",
778
+ recovery: "Run on Windows with Credential Manager available, or explicitly select the local-file credential policy.",
779
+ }));
780
+ },
781
+ storeCredential: (input) => {
782
+ if (localCredentialRoot !== undefined) {
783
+ if (input.name.trim().length === 0) {
784
+ return Effect.fail(new CredentialStorageError({
785
+ operation: "store credential",
786
+ reference: "local-file",
787
+ message: "credential name must not be empty",
788
+ }));
789
+ }
790
+ const name = createHash("sha256").update(input.name).digest("hex");
791
+ const path = win32.join(localCredentialRoot, `${name}.credential`);
792
+ return secureAtomicWrite(path, new TextEncoder().encode(Redacted.value(input.value)), 0o600).pipe(Effect.as(decode(CredentialReference)(`local-file:${path}`)));
793
+ }
794
+ if (input.name.trim().length === 0) {
795
+ return Effect.fail(new CredentialStorageError({
796
+ operation: "store credential",
797
+ reference: "credential-manager",
798
+ message: "credential name must not be empty",
799
+ }));
800
+ }
801
+ const key = createHash("sha256").update(input.name).digest("hex");
802
+ return requirePowerShell.pipe(Effect.flatMap(() => runCredentialScript(credentialScript.store, [
803
+ { name: "CANONFIG_TARGET", value: `dev.canonfig.${key}` },
804
+ { name: "CANONFIG_SECRET", value: Redacted.value(input.value) },
805
+ ])), Effect.flatMap((result) => result.exitCode === 0
806
+ ? Effect.succeed(decode(CredentialReference)(`credential-manager:${key}`))
807
+ : Effect.fail(new HumanActionRequiredError({
808
+ action: "unlock Windows Credential Manager",
809
+ recovery: "Sign in interactively and make Credential Manager available, then retry.",
810
+ }))));
811
+ },
812
+ loadCredential: (input) => {
813
+ if (localCredentialRoot !== undefined) {
814
+ return Effect.gen(function* () {
815
+ const path = yield* localCredentialPath(input.reference, localCredentialRoot);
816
+ const metadata = yield* Effect.tryPromise({
817
+ try: () => stat(path),
818
+ catch: (cause) => filesystemFailure("inspect local credential", path, cause),
819
+ });
820
+ if (metadata.size > 1024 * 1024) {
821
+ return yield* new CredentialStorageError({
822
+ operation: "load credential",
823
+ reference: String(input.reference),
824
+ message: "credential exceeds the local-file size limit",
825
+ });
826
+ }
827
+ const content = yield* Effect.tryPromise({
828
+ try: () => readFile(path),
829
+ catch: (cause) => filesystemFailure("read local credential", path, cause),
830
+ });
831
+ return Redacted.make(new TextDecoder().decode(content));
832
+ });
833
+ }
834
+ return Effect.gen(function* () {
835
+ const key = yield* credentialKey(input.reference);
836
+ yield* requirePowerShell;
837
+ const result = yield* runCredentialScript(credentialScript.load, [
838
+ { name: "CANONFIG_TARGET", value: `dev.canonfig.${key}` },
839
+ ]);
840
+ if (result.exitCode !== 0) {
841
+ return yield* new HumanActionRequiredError({
842
+ action: "provide Windows credential",
843
+ recovery: "Store the required credential in Windows Credential Manager, then retry.",
844
+ });
845
+ }
846
+ return Redacted.make(Buffer.from(result.standardOutput).toString("utf8"));
847
+ });
848
+ },
849
+ removeCredential: (reference) => {
850
+ if (localCredentialRoot !== undefined) {
851
+ return localCredentialPath(reference, localCredentialRoot).pipe(Effect.flatMap((path) => Effect.tryPromise({
852
+ try: () => rm(path, { force: true }),
853
+ catch: (cause) => filesystemFailure("remove local credential", path, cause),
854
+ })));
855
+ }
856
+ return Effect.gen(function* () {
857
+ const key = yield* credentialKey(reference);
858
+ yield* requirePowerShell;
859
+ const result = yield* runCredentialScript(credentialScript.remove, [
860
+ { name: "CANONFIG_TARGET", value: `dev.canonfig.${key}` },
861
+ ]);
862
+ if (result.exitCode !== 0) {
863
+ return yield* new CredentialStorageError({
864
+ operation: "remove credential",
865
+ reference: String(reference),
866
+ message: "Windows Credential Manager did not remove the credential",
867
+ });
868
+ }
869
+ });
870
+ },
871
+ renderSchedulerJob: renderTaskSchedulerJob,
872
+ inspectSchedulerJob: scheduler.inspect,
873
+ snapshotSchedulerJob: scheduler.snapshot,
874
+ installSchedulerJob: scheduler.install,
875
+ removeSchedulerJob: scheduler.remove,
876
+ restoreSchedulerJob: scheduler.restore,
877
+ });
878
+ }).pipe(Effect.provide(base)));
879
+ };