paratix 0.12.6 → 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).
@@ -648,6 +648,8 @@ import { Readable } from "stream";
648
648
  // src/terminal.ts
649
649
  import { createInterface } from "readline";
650
650
  import { Writable } from "stream";
651
+ var ASCII_ESC = 27;
652
+ var ANSI_SHOW_CURSOR = `${String.fromCharCode(ASCII_ESC)}[?25h`;
651
653
 
652
654
  // src/ssh.ts
653
655
  function assertValidMktempDirectory(directory) {
@@ -1566,13 +1568,6 @@ function buildRepositoryUpdateFlag(name, expectedContent) {
1566
1568
  flagPrefix
1567
1569
  };
1568
1570
  }
1569
- function firstNonEmptyApt(text) {
1570
- for (const line of text.split("\n")) {
1571
- const trimmed = line.trim();
1572
- if (trimmed.length > 0) return trimmed;
1573
- }
1574
- return null;
1575
- }
1576
1571
  async function probeAptRepositoryInodeIdentity(ssh2, filePath) {
1577
1572
  const result = await ssh2.exec(`stat -c '%d:%i' ${shellQuote(filePath)}`, {
1578
1573
  ignoreExitCode: true,
@@ -1613,7 +1608,7 @@ async function rollbackRepositoryAfterUpdateFailure(parameters) {
1613
1608
  const failureMessage = `[apt.repository] apt-get update failed for ${name}`;
1614
1609
  if (rollbackUpdate.code !== 0) {
1615
1610
  return failedCommand(
1616
- `${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"}`,
1617
1612
  updateResult
1618
1613
  );
1619
1614
  }
@@ -3321,10 +3316,12 @@ var EXEC_OPTS4 = { ignoreExitCode: true, silent: true };
3321
3316
  var UNIT_NAME_PATTERN = new RegExp("^[\\w@.\\-]+$", "v");
3322
3317
  var COMPOSE_CONFIG_MODE = "0600";
3323
3318
  var COMPOSE_CONFIG_STAGING_PREFIX = ".compose.yml.paratix-staging";
3319
+ var COMPOSE_IMAGE_INSPECT_FORMAT = "{{.Id}}\\n{{range .RepoDigests}}{{.}}\\n{{end}}";
3324
3320
  var SYSTEMD_UNIT_MODE = "0644";
3325
3321
  var SYSTEMD_UNIT_STAGING_PREFIX = ".compose-systemd-unit.paratix-staging";
3326
3322
  var C0_CONTROL_MAX_CODE_POINT = 31;
3327
3323
  var DELETE_CONTROL_CODE_POINT2 = 127;
3324
+ var COMPOSE_CONFIG_JSON_MAX_BYTES = 1048576;
3328
3325
  function requireComposeSsh(ssh2, action, projectDirectory) {
3329
3326
  return ssh2 ?? failed(`[compose.${action}] SSH connection is required for ${projectDirectory}`);
3330
3327
  }
@@ -3349,10 +3346,6 @@ var COMPOSE_UP_ACTION_KEYWORDS_REGEX = new RegExp("^(?:Creating|Recreating|Start
3349
3346
  function composeUpReportedChange(composeOutput) {
3350
3347
  return COMPOSE_UP_ACTION_KEYWORDS_REGEX.test(composeOutput);
3351
3348
  }
3352
- var COMPOSE_PULL_ACTION_REGEX = new RegExp("^(?:Pulling|Downloaded)\\s", "mv");
3353
- function composePullReportedChange(composeOutput) {
3354
- return COMPOSE_PULL_ACTION_REGEX.test(composeOutput);
3355
- }
3356
3349
  function validateComposeUpServices(services) {
3357
3350
  for (const service2 of services ?? []) {
3358
3351
  if (service2 === "") {
@@ -3376,7 +3369,7 @@ async function getRuntime(ssh2, action, explicit) {
3376
3369
  return detectRuntime(ssh2);
3377
3370
  }
3378
3371
  function composeCommand(runtime, projectDirectory) {
3379
- return `${runtime} compose --project-directory ${shellQuote(projectDirectory)}`;
3372
+ return `cd ${shellQuote(projectDirectory)} && ${runtime} compose`;
3380
3373
  }
3381
3374
  function parseComposeProjectName(stdout, projectDirectory) {
3382
3375
  try {
@@ -3390,6 +3383,148 @@ function parseComposeProjectName(stdout, projectDirectory) {
3390
3383
  return null;
3391
3384
  }
3392
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
+ }
3393
3528
  async function resolveComposeProjectName(parameters) {
3394
3529
  const result = await parameters.ssh.exec(
3395
3530
  `${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
@@ -4048,13 +4183,33 @@ var compose = {
4048
4183
  ssh: connection
4049
4184
  });
4050
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
+ });
4051
4197
  const result = await connection.exec(
4052
4198
  `${composeCommand(runtime, projectDirectory)} pull 2>&1`,
4053
4199
  EXEC_OPTS4
4054
4200
  );
4055
4201
  if (result.code !== 0)
4056
4202
  return failedCommand(`[compose.pull] failed for ${projectDirectory}`, result);
4057
- 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
+ };
4058
4213
  },
4059
4214
  // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
4060
4215
  async check() {
@@ -6183,7 +6338,7 @@ function describeType(value) {
6183
6338
  function isHostKeyMode(value) {
6184
6339
  return value === "accept-new" || value === "no" || value === "yes";
6185
6340
  }
6186
- function isRecord(value) {
6341
+ function isRecord2(value) {
6187
6342
  return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
6188
6343
  }
6189
6344
  function collectOptionalBooleanErrors(object, key, errors) {
@@ -6302,7 +6457,7 @@ function collectSshConfigErrors(value) {
6302
6457
  errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
6303
6458
  return errors;
6304
6459
  }
6305
- if (!isRecord(value)) {
6460
+ if (!isRecord2(value)) {
6306
6461
  errors.push("Invalid property 'ssh' (expected object, got object)");
6307
6462
  return errors;
6308
6463
  }
@@ -6327,7 +6482,8 @@ function normalizeServerDefinitionSshError(error) {
6327
6482
  function validateSshConfig(ssh2) {
6328
6483
  const errors = collectSshConfigErrors(ssh2);
6329
6484
  if (errors.length === 0) return;
6330
- throw new Error(`ServerDefinition: ${normalizeServerDefinitionSshError(errors[0])}`);
6485
+ const message = errors.map((error) => normalizeServerDefinitionSshError(error)).join("; ");
6486
+ throw new Error(`ServerDefinition: ${message}`);
6331
6487
  }
6332
6488
 
6333
6489
  // src/meta.ts
@@ -6370,7 +6526,7 @@ function normalizeMetaValueResolver(value) {
6370
6526
  return value;
6371
6527
  };
6372
6528
  }
6373
- function isRecord2(value) {
6529
+ function isRecord3(value) {
6374
6530
  return typeof value === "object" && value !== null;
6375
6531
  }
6376
6532
  function assertValidEnvironmentMetaEntry(candidate) {
@@ -6458,7 +6614,7 @@ function isSystemRebootMetaEntry(entry) {
6458
6614
  return entry.kind === SYSTEM_REBOOT_KIND;
6459
6615
  }
6460
6616
  function assertValidModuleMetaEntry(entry) {
6461
- if (!isRecord2(entry)) {
6617
+ if (!isRecord3(entry)) {
6462
6618
  throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
6463
6619
  }
6464
6620
  switch (entry.kind) {
@@ -12998,7 +13154,20 @@ async function ensureSshDirectoryAndAuthorizedKeysAreNotSymlinks(conn, parameter
12998
13154
  const keysPath = shellQuote(authorizedKeysPath);
12999
13155
  const quotedUser = shellQuote(user2);
13000
13156
  const quotedPrimaryGroup = shellQuote(primaryGroup);
13001
- 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; }`;
13002
13171
  const result = await conn.exec(command2, MUTATION_EXEC_OPTS);
13003
13172
  if (result.code !== 0) {
13004
13173
  const label = result.stderr.includes("authorized_keys must not be a symlink") ? "authorized_keys symlink check failed" : "failed to prepare .ssh directory";
@@ -13028,10 +13197,25 @@ async function stageAuthorizedKeysContent(conn, parameters) {
13028
13197
  const filterScratchPath = `${temporaryPath}.filter`;
13029
13198
  const quotedFilterScratchPath = shellQuote(filterScratchPath);
13030
13199
  const quotedKey = shellQuote(key);
13031
- 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; }`;
13032
13202
  const readExistingAuthorizedKeys = `dd if=${quotedAuthorizedKeysPath} iflag=nofollow status=none of=${quotedTemporaryPath}`;
13033
13203
  const stage = state === "present" ? await conn.exec(
13034
- `{ 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; }`,
13035
13219
  MUTATION_EXEC_OPTS
13036
13220
  ) : (
13037
13221
  // R-0000044: use `grep -vxF` (whole-line match) to mirror the present
@@ -13043,7 +13227,20 @@ async function stageAuthorizedKeysContent(conn, parameters) {
13043
13227
  // TOCTOU where the path could become a symlink between the
13044
13228
  // `[ ! -L ]` guard and grep's open() call.
13045
13229
  await conn.exec(
13046
- `{ 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; }`,
13047
13244
  MUTATION_EXEC_OPTS
13048
13245
  )
13049
13246
  );
@@ -13064,7 +13261,38 @@ async function replaceAuthorizedKeysAtomically(conn, parameters) {
13064
13261
  const expectedAuthorizedKeysState = shellQuote(`600 ${user2} ${primaryGroup} regular file`);
13065
13262
  const quotedBackupPath = shellQuote(`${authorizedKeysPath}.paratix-backup`);
13066
13263
  const replace2 = await conn.exec(
13067
- `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
+ }`,
13068
13296
  MUTATION_EXEC_OPTS
13069
13297
  );
13070
13298
  if (replace2.code !== 0) {
@@ -19068,4 +19296,3 @@ export {
19068
19296
  ufw,
19069
19297
  user
19070
19298
  };
19071
- //# sourceMappingURL=chunk-CWGQ2MYN.js.map