@analyticscli/growth-engineer 0.1.1-preview.25 → 0.1.1-preview.27

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,14 @@ 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 (/invalid value: ['"]?state|parameter has an invalid value: ['"]?state|--state/i.test(combined)) {
2188
+ return 'Update Growth Engineer and rerun ASC setup. Setup no longer uses the flaky ASC analytics request state filter.';
2189
+ }
2190
+ if (/file permissions are too open|too permissive|chmod 600/i.test(combined)) {
2191
+ return 'Rerun ASC setup. The wizard saves a secure local copy of AuthKey_<KEY_ID>.p8 with chmod 600 before testing.';
2192
+ }
2181
2193
  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.';
2194
+ 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
2195
  }
2184
2196
  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
2197
  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 +2749,15 @@ function inferAscKeyIdFromPrivateKeyPath(filePath) {
2737
2749
  const match = fileName.match(/^AuthKey_([A-Za-z0-9]+)\.p8$/);
2738
2750
  return match?.[1] || '';
2739
2751
  }
2752
+ async function copyAscPrivateKeyToSecurePath(sourcePath, keyId, suffix = '') {
2753
+ const destinationPath = resolveAscPrivateKeyPath(keyId, suffix);
2754
+ await fs.mkdir(path.dirname(destinationPath), { recursive: true, mode: 0o700 });
2755
+ if (path.resolve(sourcePath) !== path.resolve(destinationPath)) {
2756
+ await fs.copyFile(sourcePath, destinationPath);
2757
+ }
2758
+ await fs.chmod(destinationPath, 0o600);
2759
+ return destinationPath;
2760
+ }
2740
2761
  function renderEnvValue(value) {
2741
2762
  return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
2742
2763
  }
@@ -3977,6 +3998,7 @@ async function guideAscConnector(rl, secrets) {
3977
3998
  process.stdout.write(`${bold('Enter the Reports key now:')}\n`);
3978
3999
  printBullets([
3979
4000
  `${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.`,
4001
+ `The wizard saves a secure local copy with ${bold('chmod 600')}.`,
3980
4002
  `${bold('Issuer ID')} from the API keys page. Same value for both keys.`,
3981
4003
  `${bold('Vendor Number')} from Sales and Trends > Reports.`,
3982
4004
  ]);
@@ -3987,11 +4009,13 @@ async function guideAscConnector(rl, secrets) {
3987
4009
  });
3988
4010
  let keyId = normalKeyPath.keyId;
3989
4011
  if (normalKeyPath.privateKeyPath) {
3990
- secrets.ASC_PRIVATE_KEY_PATH = normalKeyPath.privateKeyPath;
4012
+ const securePrivateKeyPath = await copyAscPrivateKeyToSecurePath(normalKeyPath.privateKeyPath, keyId);
4013
+ secrets.ASC_PRIVATE_KEY_PATH = securePrivateKeyPath;
3991
4014
  secrets.ASC_PRIVATE_KEY = DELETE_SECRET;
3992
4015
  secrets.ASC_PRIVATE_KEY_B64 = DELETE_SECRET;
3993
4016
  secrets.ASC_KEY_ID = keyId;
3994
4017
  process.stdout.write(`Inferred ASC_KEY_ID=${keyId} from ${path.basename(normalKeyPath.privateKeyPath)}.\n`);
4018
+ process.stdout.write(`Saved secure Reports key copy to ${securePrivateKeyPath} with chmod 600.\n`);
3995
4019
  }
3996
4020
  const issuerId = await ask(rl, 'ASC_ISSUER_ID (same for both keys, empty = skip)', process.env.ASC_ISSUER_ID || '');
3997
4021
  if (issuerId.trim())
@@ -4025,7 +4049,7 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
4025
4049
  printBullets([
4026
4050
  `${bold('Role must be Admin')} so Apple can create the first App Analytics report request.`,
4027
4051
  `${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.`,
4052
+ `${bold('Not saved')} to secrets.env. The temporary secure copy is deleted after setup.`,
4029
4053
  ]);
4030
4054
  const bootstrapKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
4031
4055
  label: 'Setup Admin .p8 path (AuthKey_<KEY_ID>.p8, empty = paste)',
@@ -4038,11 +4062,11 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
4038
4062
  bootstrapIssuerId = await ask(rl, 'ASC_ISSUER_ID (same API keys page)', process.env.ASC_ISSUER_ID || '');
4039
4063
  }
4040
4064
  if (bootstrapKeyPath.privateKeyPath) {
4041
- bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = bootstrapKeyPath.privateKeyPath;
4065
+ const secureBootstrapPath = await copyAscPrivateKeyToSecurePath(bootstrapKeyPath.privateKeyPath, bootstrapKeyId, '_bootstrap_admin');
4066
+ bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = secureBootstrapPath;
4042
4067
  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';
4068
+ process.stdout.write(`Saved secure temporary Admin key copy to ${secureBootstrapPath} with chmod 600.\n`);
4069
+ bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_DELETE_AFTER_USE = '1';
4046
4070
  }
4047
4071
  else {
4048
4072
  bootstrapKeyId = await ask(rl, 'ASC_BOOTSTRAP_KEY_ID (from AuthKey_<KEY_ID>.p8)', '');