diff --git a/src/Commands/SecretsSetup.php b/src/Commands/SecretsSetup.php
index a027ef1..bdf2dbc 100644
--- a/src/Commands/SecretsSetup.php
+++ b/src/Commands/SecretsSetup.php
@@ -56,11 +56,15 @@ protected function configure(): void
With an argument, stores the provided key on this node.
Example: protocol secrets:setup "your-hex-key-here"
+ The storage location is determined by the environment set via
+ config:env. Production environments store the key globally in
+ protocol's .node/key. All other environments (local, dev, staging)
+ store the key per-project in the current working directory's
+ .protocol/key.
+
The key is also read automatically from the PROTOCOL_ENCRYPTION_KEY
environment variable, so CI/CD workflows can pass it from GitHub secrets.
- The key is stored at ~/.protocol/.node/key with 0600 permissions.
-
HELP)
;
$this
@@ -86,8 +90,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
+ $keyPath = Secrets::keyPath();
+ $scope = Secrets::isGlobal() ? 'global' : 'project';
+ $output->writeln("Environment scope: {$scope}");
+
if (Secrets::hasKey() && !$providedKey) {
- $output->writeln('Encryption key already exists at: ' . Secrets::keyPath() . '');
+ $output->writeln('Encryption key already exists at: ' . $keyPath . '');
$output->writeln('To replace it, delete the existing key first.');
return Command::SUCCESS;
}
@@ -101,7 +109,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
if (Secrets::storeKey($providedKey)) {
- $output->writeln('Encryption key stored at: ' . Secrets::keyPath() . '');
+ $output->writeln('Encryption key stored at: ' . $keyPath . '');
} else {
$output->writeln('Failed to store encryption key.');
return Command::FAILURE;
@@ -117,7 +125,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('Copy this key to other nodes:');
$output->writeln(" protocol secrets:setup \"{$hexKey}\"");
$output->writeln('');
- $output->writeln('Stored at: ' . Secrets::keyPath());
+ $output->writeln('Stored at: ' . $keyPath);
} else {
$output->writeln('Failed to generate encryption key.');
return Command::FAILURE;
diff --git a/src/Helpers/Secrets.php b/src/Helpers/Secrets.php
index 8fbd4ae..b0f837c 100644
--- a/src/Helpers/Secrets.php
+++ b/src/Helpers/Secrets.php
@@ -11,12 +11,33 @@ class Secrets
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
+ /**
+ * Determine whether secrets should be stored globally (production)
+ * or per-project (all other environments).
+ */
+ public static function isGlobal(): bool
+ {
+ return !IncidentDetector::isDev();
+ }
+
+ /**
+ * Path to the project-local .protocol directory.
+ */
+ public static function projectDataDir(): string
+ {
+ return WORKING_DIR . '.protocol' . DIRECTORY_SEPARATOR;
+ }
+
/**
* Path to the encryption key file.
+ * Production stores globally in NODE_DATA_DIR, all other envs store per-project.
*/
public static function keyPath(): string
{
- return NODE_DATA_DIR . 'key';
+ if (self::isGlobal()) {
+ return NODE_DATA_DIR . 'key';
+ }
+ return self::projectDataDir() . 'key';
}
/**
@@ -37,17 +58,24 @@ public static function generateKey(): string
/**
* Store the encryption key to disk.
+ * Location is determined by the current environment (config:env).
*/
public static function storeKey(string $hexKey): bool
{
- $dir = dirname(self::keyPath());
+ $path = self::keyPath();
+ $dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
- $written = file_put_contents(self::keyPath(), $hexKey);
+ $written = file_put_contents($path, $hexKey);
if ($written !== false) {
- chmod(self::keyPath(), 0600);
+ chmod($path, 0600);
+ }
+
+ if (!self::isGlobal()) {
+ self::ensureGitignore();
}
+
return $written !== false;
}
@@ -167,4 +195,27 @@ public static function decryptToTempFile(string $inputPath): ?string
chmod($tmpFile, 0600);
return $tmpFile;
}
+
+ /**
+ * Ensure .protocol/ is listed in the project's .gitignore.
+ */
+ public static function ensureGitignore(): void
+ {
+ $gitignorePath = WORKING_DIR . '.gitignore';
+ $entry = '.protocol/';
+
+ if (is_file($gitignorePath)) {
+ $contents = file_get_contents($gitignorePath);
+ // Check if already present (exact line match)
+ $lines = array_map('trim', explode("\n", $contents));
+ if (in_array($entry, $lines)) {
+ return;
+ }
+ // Append with a newline if file doesn't end with one
+ $append = (substr($contents, -1) === "\n" ? '' : "\n") . $entry . "\n";
+ file_put_contents($gitignorePath, $contents . $append);
+ } else {
+ file_put_contents($gitignorePath, $entry . "\n");
+ }
+ }
}