@analyticscli/growth-engineer 0.1.1-preview.24 → 0.1.1-preview.26

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.
@@ -2133,6 +2133,12 @@ function summarizeFailureReason(detail) {
2133
2133
  if (/ASC Setup Admin key auth failed: .*\.p8 key could not be parsed/i.test(text)) {
2134
2134
  return 'ASC Setup Admin key auth failed: the .p8 key could not be parsed';
2135
2135
  }
2136
+ if (/ASC Reports key auth failed: .*\.p8 file permissions are too open/i.test(text)) {
2137
+ return 'ASC Reports key auth failed: .p8 file permissions are too open';
2138
+ }
2139
+ if (/ASC Setup Admin key auth failed: .*\.p8 file permissions are too open/i.test(text)) {
2140
+ return 'ASC Setup Admin key auth failed: .p8 file permissions are too open';
2141
+ }
2136
2142
  if (/ASC .*\.p8 private key is invalid|invalid private key|failed to parse|sequence truncated|malformed|asn1/i.test(text)) {
2137
2143
  return 'ASC auth failed: the .p8 key could not be parsed';
2138
2144
  }
@@ -2178,8 +2184,11 @@ function summarizeFailureFix(connector, blockers) {
2178
2184
  return 'Paste a Coolify base URL and read-only API token from Keys & Tokens / API tokens, then rerun setup.';
2179
2185
  }
2180
2186
  if (connector === 'asc') {
2187
+ if (/file permissions are too open|too permissive|chmod 600/i.test(combined)) {
2188
+ return 'Rerun ASC setup. The wizard saves a secure local copy of AuthKey_<KEY_ID>.p8 with chmod 600 before testing.';
2189
+ }
2181
2190
  if (/Reports key auth failed|Reports key/i.test(combined) && /private key|could not be parsed|failed to parse|asn1/i.test(combined)) {
2182
- return 'Use the original downloaded AuthKey_<KEY_ID>.p8 file for the Reports key. The wizard bypasses old asc keychain credentials during setup.';
2191
+ return 'Use the original downloaded AuthKey_<KEY_ID>.p8 file for the Reports key. The wizard bypasses old asc keychain/config credentials during setup.';
2183
2192
  }
2184
2193
  if (/Setup Admin key auth failed|Admin key/i.test(combined) && /private key|could not be parsed|failed to parse|asn1/i.test(combined)) {
2185
2194
  return 'Use the original downloaded AuthKey_<KEY_ID>.p8 file for the Setup Admin key. This key is temporary and should have the Admin role.';
@@ -2737,6 +2746,15 @@ function inferAscKeyIdFromPrivateKeyPath(filePath) {
2737
2746
  const match = fileName.match(/^AuthKey_([A-Za-z0-9]+)\.p8$/);
2738
2747
  return match?.[1] || '';
2739
2748
  }
2749
+ async function copyAscPrivateKeyToSecurePath(sourcePath, keyId, suffix = '') {
2750
+ const destinationPath = resolveAscPrivateKeyPath(keyId, suffix);
2751
+ await fs.mkdir(path.dirname(destinationPath), { recursive: true, mode: 0o700 });
2752
+ if (path.resolve(sourcePath) !== path.resolve(destinationPath)) {
2753
+ await fs.copyFile(sourcePath, destinationPath);
2754
+ }
2755
+ await fs.chmod(destinationPath, 0o600);
2756
+ return destinationPath;
2757
+ }
2740
2758
  function renderEnvValue(value) {
2741
2759
  return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
2742
2760
  }
@@ -3977,6 +3995,7 @@ async function guideAscConnector(rl, secrets) {
3977
3995
  process.stdout.write(`${bold('Enter the Reports key now:')}\n`);
3978
3996
  printBullets([
3979
3997
  `${bold('.p8 path')} to Apple\'s original ${bold('AuthKey_<KEY_ID>.p8')} file. ${bold('Do not rename it')}; KEY_ID is read from the filename.`,
3998
+ `The wizard saves a secure local copy with ${bold('chmod 600')}.`,
3980
3999
  `${bold('Issuer ID')} from the API keys page. Same value for both keys.`,
3981
4000
  `${bold('Vendor Number')} from Sales and Trends > Reports.`,
3982
4001
  ]);
@@ -3987,11 +4006,13 @@ async function guideAscConnector(rl, secrets) {
3987
4006
  });
3988
4007
  let keyId = normalKeyPath.keyId;
3989
4008
  if (normalKeyPath.privateKeyPath) {
3990
- secrets.ASC_PRIVATE_KEY_PATH = normalKeyPath.privateKeyPath;
4009
+ const securePrivateKeyPath = await copyAscPrivateKeyToSecurePath(normalKeyPath.privateKeyPath, keyId);
4010
+ secrets.ASC_PRIVATE_KEY_PATH = securePrivateKeyPath;
3991
4011
  secrets.ASC_PRIVATE_KEY = DELETE_SECRET;
3992
4012
  secrets.ASC_PRIVATE_KEY_B64 = DELETE_SECRET;
3993
4013
  secrets.ASC_KEY_ID = keyId;
3994
4014
  process.stdout.write(`Inferred ASC_KEY_ID=${keyId} from ${path.basename(normalKeyPath.privateKeyPath)}.\n`);
4015
+ process.stdout.write(`Saved secure Reports key copy to ${securePrivateKeyPath} with chmod 600.\n`);
3995
4016
  }
3996
4017
  const issuerId = await ask(rl, 'ASC_ISSUER_ID (same for both keys, empty = skip)', process.env.ASC_ISSUER_ID || '');
3997
4018
  if (issuerId.trim())
@@ -4025,7 +4046,7 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
4025
4046
  printBullets([
4026
4047
  `${bold('Role must be Admin')} so Apple can create the first App Analytics report request.`,
4027
4048
  `${bold('Use original AuthKey_<KEY_ID>.p8 filename')} so KEY_ID is read automatically.`,
4028
- `${bold('Not saved')} to secrets.env. Revoke this key after setup.`,
4049
+ `${bold('Not saved')} to secrets.env. The temporary secure copy is deleted after setup.`,
4029
4050
  ]);
4030
4051
  const bootstrapKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
4031
4052
  label: 'Setup Admin .p8 path (AuthKey_<KEY_ID>.p8, empty = paste)',
@@ -4038,11 +4059,11 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
4038
4059
  bootstrapIssuerId = await ask(rl, 'ASC_ISSUER_ID (same API keys page)', process.env.ASC_ISSUER_ID || '');
4039
4060
  }
4040
4061
  if (bootstrapKeyPath.privateKeyPath) {
4041
- bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = bootstrapKeyPath.privateKeyPath;
4062
+ const secureBootstrapPath = await copyAscPrivateKeyToSecurePath(bootstrapKeyPath.privateKeyPath, bootstrapKeyId, '_bootstrap_admin');
4063
+ bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = secureBootstrapPath;
4042
4064
  process.stdout.write(`Inferred ASC_BOOTSTRAP_KEY_ID=${bootstrapKeyId} from ${path.basename(bootstrapKeyPath.privateKeyPath)}.\n`);
4043
- const shouldDelete = await askYesNo(rl, 'Delete this temporary Admin .p8 file from this host after the setup check?', true);
4044
- if (shouldDelete)
4045
- bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_DELETE_AFTER_USE = '1';
4065
+ process.stdout.write(`Saved secure temporary Admin key copy to ${secureBootstrapPath} with chmod 600.\n`);
4066
+ bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_DELETE_AFTER_USE = '1';
4046
4067
  }
4047
4068
  else {
4048
4069
  bootstrapKeyId = await ask(rl, 'ASC_BOOTSTRAP_KEY_ID (from AuthKey_<KEY_ID>.p8)', '');