paratix 0.12.8 → 0.13.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.
package/README.md CHANGED
@@ -127,17 +127,21 @@ Options:
127
127
  --dry-run
128
128
  --env <key=value>
129
129
  --env-file <path>
130
+ --filter <names>
130
131
  --first-run
131
132
  --reconnect-timeout <seconds>
132
133
  --verbose
133
- --version
134
134
  --help
135
135
  ```
136
136
 
137
+ `--filter <names>` restricts the run to the named recipes and modules. Names are matched anywhere in the tree, and every node that is not selected is shown as `skipped` instead of being executed. The option is comma-separated and repeatable, so `--filter rybbit,palamedes-examples` and `--filter rybbit --filter palamedes-examples` are equivalent. Selecting a recipe runs its whole subtree; a recipe that is not selected but contains a selected descendant is still descended into, so only the matching children run while its siblings are skipped. If several nodes share the same name, every one of them is selected. An unknown filter name aborts the run before it connects (exit code 2). The final run summary counts only top-level nodes, so nested skipped nodes are still shown but are not added to the `skipped` tally. `--filter` composes with `--dry-run` and the other flags.
138
+
137
139
  `--first-run` is meant for explicit bootstrap flows where a fresh server must be hardened first and the rest of the system should only be applied later.
138
140
 
139
141
  `--diff` only works together with `--dry-run`. When enabled, modules that opt in print a unified diff below their status line, showing exactly which lines or values would change. Without `--diff`, the dry-run output is unchanged. Diff-producing modules: `file.copy`, `file.template`, `sysctl.set`, `hostname.set`, `swap.file`, `swap.swappiness`, `swap.vfsCachePressure`, `cron.job`, `cron.absent`, `timer.scheduled`, `timer.absent`, `net.hosts`, `quadlet.container`.
140
142
 
143
+ `--reconnect-timeout <seconds>` sets how long Paratix keeps retrying the SSH connection after a module forces the connection to drop — after a `system.reboot` or after an SSH port change. When a module reboots the host, Paratix waits a short grace period and then reconnects within a **300 second window by default**, so hosts that need a few minutes to come back (fsck, cloud-init, slow POST) still succeed. The number of attempts follows the time window rather than a fixed cap, so the window is never cut short. Port-change reconnects use a shorter 120 second default because the port comes back almost immediately. Set `--reconnect-timeout` (up to 86400 seconds) to override both paths for exceptionally slow reboots.
144
+
141
145
  ## Documentation
142
146
 
143
147
  - For project scaffolding, see [`create-paratix`](https://www.npmjs.com/package/create-paratix).
@@ -1568,13 +1568,6 @@ function buildRepositoryUpdateFlag(name, expectedContent) {
1568
1568
  flagPrefix
1569
1569
  };
1570
1570
  }
1571
- function firstNonEmptyApt(text) {
1572
- for (const line of text.split("\n")) {
1573
- const trimmed = line.trim();
1574
- if (trimmed.length > 0) return trimmed;
1575
- }
1576
- return null;
1577
- }
1578
1571
  async function probeAptRepositoryInodeIdentity(ssh2, filePath) {
1579
1572
  const result = await ssh2.exec(`stat -c '%d:%i' ${shellQuote(filePath)}`, {
1580
1573
  ignoreExitCode: true,
@@ -1615,7 +1608,7 @@ async function rollbackRepositoryAfterUpdateFailure(parameters) {
1615
1608
  const failureMessage = `[apt.repository] apt-get update failed for ${name}`;
1616
1609
  if (rollbackUpdate.code !== 0) {
1617
1610
  return failedCommand(
1618
- `${failureMessage}; rollback succeeded but apt-get update on the restored sources also failed (exit code ${String(rollbackUpdate.code)}): ${firstNonEmptyApt(rollbackUpdate.stderr) ?? firstNonEmptyApt(rollbackUpdate.stdout) ?? "no output"}`,
1611
+ `${failureMessage}; rollback succeeded but apt-get update on the restored sources also failed (exit code ${String(rollbackUpdate.code)}): ${firstNonEmptyLine(rollbackUpdate.stderr) ?? firstNonEmptyLine(rollbackUpdate.stdout) ?? "no output"}`,
1619
1612
  updateResult
1620
1613
  );
1621
1614
  }
@@ -3323,10 +3316,12 @@ var EXEC_OPTS4 = { ignoreExitCode: true, silent: true };
3323
3316
  var UNIT_NAME_PATTERN = new RegExp("^[\\w@.\\-]+$", "v");
3324
3317
  var COMPOSE_CONFIG_MODE = "0600";
3325
3318
  var COMPOSE_CONFIG_STAGING_PREFIX = ".compose.yml.paratix-staging";
3319
+ var COMPOSE_IMAGE_INSPECT_FORMAT = "{{.Id}}\\n{{range .RepoDigests}}{{.}}\\n{{end}}";
3326
3320
  var SYSTEMD_UNIT_MODE = "0644";
3327
3321
  var SYSTEMD_UNIT_STAGING_PREFIX = ".compose-systemd-unit.paratix-staging";
3328
3322
  var C0_CONTROL_MAX_CODE_POINT = 31;
3329
3323
  var DELETE_CONTROL_CODE_POINT2 = 127;
3324
+ var COMPOSE_CONFIG_JSON_MAX_BYTES = 1048576;
3330
3325
  function requireComposeSsh(ssh2, action, projectDirectory) {
3331
3326
  return ssh2 ?? failed(`[compose.${action}] SSH connection is required for ${projectDirectory}`);
3332
3327
  }
@@ -3351,10 +3346,6 @@ var COMPOSE_UP_ACTION_KEYWORDS_REGEX = new RegExp("^(?:Creating|Recreating|Start
3351
3346
  function composeUpReportedChange(composeOutput) {
3352
3347
  return COMPOSE_UP_ACTION_KEYWORDS_REGEX.test(composeOutput);
3353
3348
  }
3354
- var COMPOSE_PULL_ACTION_REGEX = new RegExp("^(?:Pulling|Downloaded)\\s", "mv");
3355
- function composePullReportedChange(composeOutput) {
3356
- return COMPOSE_PULL_ACTION_REGEX.test(composeOutput);
3357
- }
3358
3349
  function validateComposeUpServices(services) {
3359
3350
  for (const service2 of services ?? []) {
3360
3351
  if (service2 === "") {
@@ -3392,6 +3383,148 @@ function parseComposeProjectName(stdout, projectDirectory) {
3392
3383
  return null;
3393
3384
  }
3394
3385
  }
3386
+ function isRecord(value) {
3387
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3388
+ }
3389
+ function readComposeServiceImage(service2) {
3390
+ if (!isRecord(service2)) return null;
3391
+ const { image } = service2;
3392
+ return typeof image === "string" && image.trim() !== "" ? image : null;
3393
+ }
3394
+ function collectComposeConfigImages(services) {
3395
+ const images = /* @__PURE__ */ new Set();
3396
+ for (const service2 of Object.values(services)) {
3397
+ const image = readComposeServiceImage(service2);
3398
+ if (image !== null) images.add(image);
3399
+ }
3400
+ return [...images].sort();
3401
+ }
3402
+ function parseComposeConfigImages(stdout) {
3403
+ if (Buffer.byteLength(stdout, "utf8") > COMPOSE_CONFIG_JSON_MAX_BYTES) return null;
3404
+ try {
3405
+ const parsed = JSON.parse(stdout);
3406
+ if (!isRecord(parsed)) return null;
3407
+ const { services } = parsed;
3408
+ if (!isRecord(services)) return null;
3409
+ return collectComposeConfigImages(services);
3410
+ } catch {
3411
+ return null;
3412
+ }
3413
+ }
3414
+ function buildComposeImageInspectCommand(runtime, image) {
3415
+ return `${runtime} image inspect --format '${COMPOSE_IMAGE_INSPECT_FORMAT}' -- ${shellQuote(image)}`;
3416
+ }
3417
+ function getComposeImageRepository(image) {
3418
+ const digestSeparatorIndex = image.indexOf("@");
3419
+ if (digestSeparatorIndex !== -1) return image.slice(0, digestSeparatorIndex);
3420
+ const lastSlashIndex = image.lastIndexOf("/");
3421
+ const lastColonIndex = image.lastIndexOf(":");
3422
+ return lastColonIndex > lastSlashIndex ? image.slice(0, lastColonIndex) : image;
3423
+ }
3424
+ function readComposeImageIdentifierFromInspectOutput(image, output) {
3425
+ const lines = output.split("\n").filter((line) => line.length > 0);
3426
+ if (lines.length === 0) return null;
3427
+ const [id, ...repoDigests] = lines;
3428
+ const repository = getComposeImageRepository(image);
3429
+ for (const repoDigest of repoDigests) {
3430
+ const separatorIndex = repoDigest.indexOf("@");
3431
+ if (separatorIndex === -1) continue;
3432
+ const repo = repoDigest.slice(0, separatorIndex);
3433
+ const digest = repoDigest.slice(separatorIndex + 1);
3434
+ if (repo === repository && digest.length > 0) return digest;
3435
+ }
3436
+ return id.length > 0 ? id : null;
3437
+ }
3438
+ async function resolveComposeConfigImages(parameters) {
3439
+ const result = await parameters.ssh.exec(
3440
+ `${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
3441
+ EXEC_OPTS4
3442
+ );
3443
+ if (result.code !== 0) {
3444
+ return failedCommand(
3445
+ `[compose.pull] failed to resolve images for ${parameters.projectDirectory}`,
3446
+ result
3447
+ );
3448
+ }
3449
+ const images = parseComposeConfigImages(result.stdout);
3450
+ if (images === null) {
3451
+ return failed(
3452
+ `[compose.pull] failed to parse compose config images for ${parameters.projectDirectory}`
3453
+ );
3454
+ }
3455
+ return images;
3456
+ }
3457
+ async function inspectComposeImageBeforePull(parameters) {
3458
+ const result = await parameters.ssh.exec(
3459
+ buildComposeImageInspectCommand(parameters.runtime, parameters.image),
3460
+ EXEC_OPTS4
3461
+ );
3462
+ if (result.code !== 0) return null;
3463
+ return readComposeImageIdentifierFromInspectOutput(parameters.image, result.stdout);
3464
+ }
3465
+ async function inspectComposeImageAfterPull(parameters) {
3466
+ const result = await parameters.ssh.exec(
3467
+ buildComposeImageInspectCommand(parameters.runtime, parameters.image),
3468
+ EXEC_OPTS4
3469
+ );
3470
+ if (result.code !== 0) {
3471
+ return failedCommand(
3472
+ `[compose.pull] image inspect failed for ${parameters.image} in ${parameters.projectDirectory}`,
3473
+ result
3474
+ );
3475
+ }
3476
+ const imageIdentifier = readComposeImageIdentifierFromInspectOutput(
3477
+ parameters.image,
3478
+ result.stdout
3479
+ );
3480
+ if (imageIdentifier === null) {
3481
+ return failed(
3482
+ `[compose.pull] image inspect returned no digest or image ID for ${parameters.image} in ${parameters.projectDirectory}`
3483
+ );
3484
+ }
3485
+ return imageIdentifier;
3486
+ }
3487
+ async function snapshotComposeImagesBeforePull(parameters) {
3488
+ return new Map(
3489
+ await Promise.all(
3490
+ parameters.images.map(
3491
+ async (image) => [
3492
+ image,
3493
+ await inspectComposeImageBeforePull({
3494
+ image,
3495
+ runtime: parameters.runtime,
3496
+ ssh: parameters.ssh
3497
+ })
3498
+ ]
3499
+ )
3500
+ )
3501
+ );
3502
+ }
3503
+ async function snapshotComposeImagesAfterPull(parameters) {
3504
+ const entries = await Promise.all(
3505
+ parameters.images.map(async (image) => ({
3506
+ image,
3507
+ imageIdentifier: await inspectComposeImageAfterPull({
3508
+ image,
3509
+ projectDirectory: parameters.projectDirectory,
3510
+ runtime: parameters.runtime,
3511
+ ssh: parameters.ssh
3512
+ })
3513
+ }))
3514
+ );
3515
+ const snapshot = /* @__PURE__ */ new Map();
3516
+ for (const { image, imageIdentifier } of entries) {
3517
+ if (typeof imageIdentifier !== "string") return imageIdentifier;
3518
+ snapshot.set(image, imageIdentifier);
3519
+ }
3520
+ return snapshot;
3521
+ }
3522
+ function composeImageSnapshotChanged(parameters) {
3523
+ for (const [image, afterIdentifier] of parameters.after) {
3524
+ if (parameters.before.get(image) !== afterIdentifier) return true;
3525
+ }
3526
+ return false;
3527
+ }
3395
3528
  async function resolveComposeProjectName(parameters) {
3396
3529
  const result = await parameters.ssh.exec(
3397
3530
  `${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
@@ -4050,13 +4183,33 @@ var compose = {
4050
4183
  ssh: connection
4051
4184
  });
4052
4185
  if (typeof runtime !== "string") return runtime;
4186
+ const images = await resolveComposeConfigImages({
4187
+ projectDirectory,
4188
+ runtime,
4189
+ ssh: connection
4190
+ });
4191
+ if (!Array.isArray(images)) return images;
4192
+ const beforePull = await snapshotComposeImagesBeforePull({
4193
+ images,
4194
+ runtime,
4195
+ ssh: connection
4196
+ });
4053
4197
  const result = await connection.exec(
4054
4198
  `${composeCommand(runtime, projectDirectory)} pull 2>&1`,
4055
4199
  EXEC_OPTS4
4056
4200
  );
4057
4201
  if (result.code !== 0)
4058
4202
  return failedCommand(`[compose.pull] failed for ${projectDirectory}`, result);
4059
- return { status: composePullReportedChange(result.stdout) ? "changed" : "ok" };
4203
+ const afterPull = await snapshotComposeImagesAfterPull({
4204
+ images,
4205
+ projectDirectory,
4206
+ runtime,
4207
+ ssh: connection
4208
+ });
4209
+ if (!(afterPull instanceof Map)) return afterPull;
4210
+ return {
4211
+ status: composeImageSnapshotChanged({ after: afterPull, before: beforePull }) ? "changed" : "ok"
4212
+ };
4060
4213
  },
4061
4214
  // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
4062
4215
  async check() {
@@ -6185,7 +6338,7 @@ function describeType(value) {
6185
6338
  function isHostKeyMode(value) {
6186
6339
  return value === "accept-new" || value === "no" || value === "yes";
6187
6340
  }
6188
- function isRecord(value) {
6341
+ function isRecord2(value) {
6189
6342
  return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
6190
6343
  }
6191
6344
  function collectOptionalBooleanErrors(object, key, errors) {
@@ -6304,7 +6457,7 @@ function collectSshConfigErrors(value) {
6304
6457
  errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
6305
6458
  return errors;
6306
6459
  }
6307
- if (!isRecord(value)) {
6460
+ if (!isRecord2(value)) {
6308
6461
  errors.push("Invalid property 'ssh' (expected object, got object)");
6309
6462
  return errors;
6310
6463
  }
@@ -6329,7 +6482,8 @@ function normalizeServerDefinitionSshError(error) {
6329
6482
  function validateSshConfig(ssh2) {
6330
6483
  const errors = collectSshConfigErrors(ssh2);
6331
6484
  if (errors.length === 0) return;
6332
- throw new Error(`ServerDefinition: ${normalizeServerDefinitionSshError(errors[0])}`);
6485
+ const message = errors.map((error) => normalizeServerDefinitionSshError(error)).join("; ");
6486
+ throw new Error(`ServerDefinition: ${message}`);
6333
6487
  }
6334
6488
 
6335
6489
  // src/meta.ts
@@ -6372,7 +6526,7 @@ function normalizeMetaValueResolver(value) {
6372
6526
  return value;
6373
6527
  };
6374
6528
  }
6375
- function isRecord2(value) {
6529
+ function isRecord3(value) {
6376
6530
  return typeof value === "object" && value !== null;
6377
6531
  }
6378
6532
  function assertValidEnvironmentMetaEntry(candidate) {
@@ -6460,7 +6614,7 @@ function isSystemRebootMetaEntry(entry) {
6460
6614
  return entry.kind === SYSTEM_REBOOT_KIND;
6461
6615
  }
6462
6616
  function assertValidModuleMetaEntry(entry) {
6463
- if (!isRecord2(entry)) {
6617
+ if (!isRecord3(entry)) {
6464
6618
  throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
6465
6619
  }
6466
6620
  switch (entry.kind) {
@@ -13000,7 +13154,20 @@ async function ensureSshDirectoryAndAuthorizedKeysAreNotSymlinks(conn, parameter
13000
13154
  const keysPath = shellQuote(authorizedKeysPath);
13001
13155
  const quotedUser = shellQuote(user2);
13002
13156
  const quotedPrimaryGroup = shellQuote(primaryGroup);
13003
- const command2 = `set -e; [ ! -L ${directory} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }; if [ -e ${directory} ]; then [ -d ${directory} ] || { echo '.ssh must be a directory' >&2; exit 1; }; else mkdir -p ${directory}; fi; [ -d ${directory} ] && [ ! -L ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }; ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?; chmod 700 ${directory} && chown ${quotedUser}:${quotedPrimaryGroup} ${directory}; [ ! -L ${directory} ] && [ -d ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }; post_ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?; [ "$post_ssh_directory_identity" = "$ssh_directory_identity" ] || { echo '.ssh directory changed during metadata update' >&2; exit 1; }; [ ! -L ${keysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }`;
13157
+ const command2 = `set -e
13158
+ [ ! -L ${directory} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }
13159
+ if [ -e ${directory} ]; then
13160
+ [ -d ${directory} ] || { echo '.ssh must be a directory' >&2; exit 1; }
13161
+ else
13162
+ mkdir -p ${directory}
13163
+ fi
13164
+ [ -d ${directory} ] && [ ! -L ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }
13165
+ ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?
13166
+ chmod 700 ${directory} && chown ${quotedUser}:${quotedPrimaryGroup} ${directory}
13167
+ [ ! -L ${directory} ] && [ -d ${directory} ] || { echo '.ssh must be a real directory' >&2; exit 1; }
13168
+ post_ssh_directory_identity=$(stat -c '%d:%i' ${directory}) || exit $?
13169
+ [ "$post_ssh_directory_identity" = "$ssh_directory_identity" ] || { echo '.ssh directory changed during metadata update' >&2; exit 1; }
13170
+ [ ! -L ${keysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }`;
13004
13171
  const result = await conn.exec(command2, MUTATION_EXEC_OPTS);
13005
13172
  if (result.code !== 0) {
13006
13173
  const label = result.stderr.includes("authorized_keys must not be a symlink") ? "authorized_keys symlink check failed" : "failed to prepare .ssh directory";
@@ -13030,10 +13197,25 @@ async function stageAuthorizedKeysContent(conn, parameters) {
13030
13197
  const filterScratchPath = `${temporaryPath}.filter`;
13031
13198
  const quotedFilterScratchPath = shellQuote(filterScratchPath);
13032
13199
  const quotedKey = shellQuote(key);
13033
- const existingAuthorizedKeysGuard = `[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }; [ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }`;
13200
+ const existingAuthorizedKeysGuard = `[ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
13201
+ [ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }`;
13034
13202
  const readExistingAuthorizedKeys = `dd if=${quotedAuthorizedKeysPath} iflag=nofollow status=none of=${quotedTemporaryPath}`;
13035
13203
  const stage = state === "present" ? await conn.exec(
13036
- `{ if [ -e ${quotedAuthorizedKeysPath} ]; then ${existingAuthorizedKeysGuard}; ${readExistingAuthorizedKeys} || exit $?; grep -qxF -- ${quotedKey} ${quotedTemporaryPath}; grep_status=$?; if [ "$grep_status" -eq 0 ]; then :; elif [ "$grep_status" -eq 1 ]; then printf '%s\\n' ${quotedKey} >> ${quotedTemporaryPath}; else exit "$grep_status"; fi; else printf '%s\\n' ${quotedKey} > ${quotedTemporaryPath}; fi; }`,
13204
+ `{ if [ -e ${quotedAuthorizedKeysPath} ]; then
13205
+ ${existingAuthorizedKeysGuard}
13206
+ ${readExistingAuthorizedKeys} || exit $?
13207
+ grep -qxF -- ${quotedKey} ${quotedTemporaryPath}
13208
+ grep_status=$?
13209
+ if [ "$grep_status" -eq 0 ]; then
13210
+ :
13211
+ elif [ "$grep_status" -eq 1 ]; then
13212
+ printf '%s\\n' ${quotedKey} >> ${quotedTemporaryPath}
13213
+ else
13214
+ exit "$grep_status"
13215
+ fi
13216
+ else
13217
+ printf '%s\\n' ${quotedKey} > ${quotedTemporaryPath}
13218
+ fi; }`,
13037
13219
  MUTATION_EXEC_OPTS
13038
13220
  ) : (
13039
13221
  // R-0000044: use `grep -vxF` (whole-line match) to mirror the present
@@ -13045,7 +13227,20 @@ async function stageAuthorizedKeysContent(conn, parameters) {
13045
13227
  // TOCTOU where the path could become a symlink between the
13046
13228
  // `[ ! -L ]` guard and grep's open() call.
13047
13229
  await conn.exec(
13048
- `{ if [ -e ${quotedAuthorizedKeysPath} ]; then ${existingAuthorizedKeysGuard}; ${readExistingAuthorizedKeys} || exit $?; grep -vxF -- ${quotedKey} ${quotedTemporaryPath} > ${quotedFilterScratchPath}; grep_status=$?; if [ "$grep_status" -eq 0 ] || [ "$grep_status" -eq 1 ]; then mv -T -- ${quotedFilterScratchPath} ${quotedTemporaryPath} || exit $?; else rm -f -- ${quotedFilterScratchPath}; exit "$grep_status"; fi; else : > ${quotedTemporaryPath}; fi; }`,
13230
+ `{ if [ -e ${quotedAuthorizedKeysPath} ]; then
13231
+ ${existingAuthorizedKeysGuard}
13232
+ ${readExistingAuthorizedKeys} || exit $?
13233
+ grep -vxF -- ${quotedKey} ${quotedTemporaryPath} > ${quotedFilterScratchPath}
13234
+ grep_status=$?
13235
+ if [ "$grep_status" -eq 0 ] || [ "$grep_status" -eq 1 ]; then
13236
+ mv -T -- ${quotedFilterScratchPath} ${quotedTemporaryPath} || exit $?
13237
+ else
13238
+ rm -f -- ${quotedFilterScratchPath}
13239
+ exit "$grep_status"
13240
+ fi
13241
+ else
13242
+ : > ${quotedTemporaryPath}
13243
+ fi; }`,
13049
13244
  MUTATION_EXEC_OPTS
13050
13245
  )
13051
13246
  );
@@ -13066,7 +13261,38 @@ async function replaceAuthorizedKeysAtomically(conn, parameters) {
13066
13261
  const expectedAuthorizedKeysState = shellQuote(`600 ${user2} ${primaryGroup} regular file`);
13067
13262
  const quotedBackupPath = shellQuote(`${authorizedKeysPath}.paratix-backup`);
13068
13263
  const replace2 = await conn.exec(
13069
- `chmod 600 ${quotedTemporaryPath} && chown ${shellQuote(user2)}:${shellQuote(primaryGroup)} ${quotedTemporaryPath} && { expected_authorized_keys_hash=$(sha256sum ${quotedTemporaryPath} | cut -d' ' -f1) || exit $?; [ ! -L ${quotedSshDirectoryPath} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }; [ -d ${quotedSshDirectoryPath} ] || { echo '.ssh must be a directory' >&2; exit 1; }; ssh_directory_state=$(stat -c '%a %U %G %F' ${quotedSshDirectoryPath}) || exit $?; [ "$ssh_directory_state" = ${expectedSshDirectoryState} ] || { echo '.ssh ownership changed before authorized_keys replace' >&2; exit 1; }; [ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }; rm -f -- ${quotedBackupPath}; backup_created=0; if [ -e ${quotedAuthorizedKeysPath} ]; then [ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }; [ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }; ln -P -- ${quotedAuthorizedKeysPath} ${quotedBackupPath} || { echo 'failed to create authorized_keys backup hardlink' >&2; exit 1; }; backup_created=1; fi; [ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }; if ! mv -T -- ${quotedTemporaryPath} ${quotedAuthorizedKeysPath}; then if [ "$backup_created" = 1 ]; then mv -T -- ${quotedBackupPath} ${quotedAuthorizedKeysPath} || echo 'authorized_keys backup restore failed; backup is at '${quotedBackupPath} >&2; fi; echo 'authorized_keys replace failed' >&2; exit 1; fi; [ ! -e ${quotedTemporaryPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys replace did not consume temporary file' >&2; exit 1; }; [ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }; [ -f ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must be a regular file' >&2; exit 1; }; authorized_keys_state=$(stat -c '%a %U %G %F' ${quotedAuthorizedKeysPath}) || { rm -f -- ${quotedBackupPath}; exit 1; }; [ "$authorized_keys_state" = ${expectedAuthorizedKeysState} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys metadata changed during replace' >&2; exit 1; }; authorized_keys_hash=$(sha256sum ${quotedAuthorizedKeysPath} | cut -d' ' -f1) || { rm -f -- ${quotedBackupPath}; exit 1; }; [ "$authorized_keys_hash" = "$expected_authorized_keys_hash" ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys content changed during replace' >&2; exit 1; }; rm -f -- ${quotedBackupPath}; }`,
13264
+ `chmod 600 ${quotedTemporaryPath} && chown ${shellQuote(user2)}:${shellQuote(primaryGroup)} ${quotedTemporaryPath} && {
13265
+ expected_authorized_keys_hash=$(sha256sum ${quotedTemporaryPath} | cut -d' ' -f1) || exit $?
13266
+ [ ! -L ${quotedSshDirectoryPath} ] || { echo '.ssh must not be a symlink' >&2; exit 1; }
13267
+ [ -d ${quotedSshDirectoryPath} ] || { echo '.ssh must be a directory' >&2; exit 1; }
13268
+ ssh_directory_state=$(stat -c '%a %U %G %F' ${quotedSshDirectoryPath}) || exit $?
13269
+ [ "$ssh_directory_state" = ${expectedSshDirectoryState} ] || { echo '.ssh ownership changed before authorized_keys replace' >&2; exit 1; }
13270
+ [ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
13271
+ rm -f -- ${quotedBackupPath}
13272
+ backup_created=0
13273
+ if [ -e ${quotedAuthorizedKeysPath} ]; then
13274
+ [ -f ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must be a regular file' >&2; exit 1; }
13275
+ [ ! -L ${quotedAuthorizedKeysPath} ] || { echo 'authorized_keys must not be a symlink' >&2; exit 1; }
13276
+ ln -P -- ${quotedAuthorizedKeysPath} ${quotedBackupPath} || { echo 'failed to create authorized_keys backup hardlink' >&2; exit 1; }
13277
+ backup_created=1
13278
+ fi
13279
+ [ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }
13280
+ if ! mv -T -- ${quotedTemporaryPath} ${quotedAuthorizedKeysPath}; then
13281
+ if [ "$backup_created" = 1 ]; then
13282
+ mv -T -- ${quotedBackupPath} ${quotedAuthorizedKeysPath} || echo 'authorized_keys backup restore failed; backup is at '${quotedBackupPath} >&2
13283
+ fi
13284
+ echo 'authorized_keys replace failed' >&2
13285
+ exit 1
13286
+ fi
13287
+ [ ! -e ${quotedTemporaryPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys replace did not consume temporary file' >&2; exit 1; }
13288
+ [ ! -L ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must not be a symlink' >&2; exit 1; }
13289
+ [ -f ${quotedAuthorizedKeysPath} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys must be a regular file' >&2; exit 1; }
13290
+ authorized_keys_state=$(stat -c '%a %U %G %F' ${quotedAuthorizedKeysPath}) || { rm -f -- ${quotedBackupPath}; exit 1; }
13291
+ [ "$authorized_keys_state" = ${expectedAuthorizedKeysState} ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys metadata changed during replace' >&2; exit 1; }
13292
+ authorized_keys_hash=$(sha256sum ${quotedAuthorizedKeysPath} | cut -d' ' -f1) || { rm -f -- ${quotedBackupPath}; exit 1; }
13293
+ [ "$authorized_keys_hash" = "$expected_authorized_keys_hash" ] || { rm -f -- ${quotedBackupPath}; echo 'authorized_keys content changed during replace' >&2; exit 1; }
13294
+ rm -f -- ${quotedBackupPath}
13295
+ }`,
13070
13296
  MUTATION_EXEC_OPTS
13071
13297
  );
13072
13298
  if (replace2.code !== 0) {
@@ -19070,4 +19296,3 @@ export {
19070
19296
  ufw,
19071
19297
  user
19072
19298
  };
19073
- //# sourceMappingURL=chunk-M3ZQJNKM.js.map