Skip to content

refactor: improve key validation architecture - #54

Merged
loadinglucian merged 6 commits into
mainfrom
refactor/key-validation
Oct 27, 2025
Merged

refactor: improve key validation architecture#54
loadinglucian merged 6 commits into
mainfrom
refactor/key-validation

Conversation

@loadinglucian

@loadinglucian loadinglucian commented Oct 26, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Refactor

    • SSH actions now require an explicit private-key path and key-resolution was centralized for consistent behavior.
  • Bug Fixes

    • Prompts accept an optional public-key path with a default hint and clearly report when a key can’t be found or read.
    • Error messages now surface actual failure details.
  • Enhancement

    • Improved path handling (tilde expansion and first-existing lookup) and validation now recognizes modern SSH key types, including FIDO variants.
  • Documentation

    • Command and trait docs updated to clarify required services and key-resolution helpers.

@coderabbitai

coderabbitai Bot commented Oct 26, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Commands now prompt for a raw public-key path which is resolved via new resolver/fallback helpers; FilesystemService adds tilde-expansion and first-existing lookup; SSHService API now requires explicit private-key paths and no longer performs internal key-path resolution; validation and traits updated to use the new filesystem-backed helpers and surface concrete errors.

Changes

Cohort / File(s) Summary
FilesystemService
app/Services/FilesystemService.php
Added expandPath(string $path): string to expand a leading ~ to the user's home (throws if HOME unknown) and getFirstExisting(array $paths): ?string to expand candidates and return the first existing path or null.
SSHService (API hardening)
app/Services/SSHService.php
Removed EnvService from constructor; public methods now require a non-null privateKeyPath parameter (e.g., assertCanConnect, executeCommand, executeScript, uploadFile, downloadFile); removed internal private-key resolution and tilde-expansion helpers; connection helpers accept explicit privateKeyPath.
KeyHelpersTrait (resolvers)
app/Traits/KeyHelpersTrait.php
Replaced expandKeyPath with resolvePrivateKeyPath(?string); added resolvePublicKeyPath(?string) and resolveKeyWithFallback(?string, array) which build candidate lists and use FilesystemService::getFirstExisting to pick an existing path.
KeyValidationTrait (validation)
app/Traits/KeyValidationTrait.php
Switched to FilesystemService::expandPath and readFile; allows empty input to trigger fallback resolution; checks existence before read; validates public key content against a broader set of prefixes (including FIDO2/U2F), flags obsolete ssh-dss, and returns specific error messages.
DigitalOcean key command
app/Console/Key/KeyAddDigitalOceanCommand.php
Replaced raw $keyPath input with $publicKeyPathRaw; calls resolvePublicKeyPath() and errors if unresolved; prompt marked optional with hint; upload uses resolved public key path; exception messages are forwarded to the user.
DigitalOcean trait docs
app/Traits/DigitalOceanCommandTrait.php
Added PHPDoc @property annotations for required services (EnvService, IOService, DigitalOceanService); no runtime behavior changes.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Cmd as KeyAddDigitalOceanCommand
    participant Trait as KeyHelpersTrait
    participant FS as FilesystemService
    participant SSH as SSHService

    Cmd->>Cmd: Prompt for public key path (optional)
    Cmd->>Trait: resolvePublicKeyPath(publicKeyPathRaw)
    Trait->>Trait: build candidate list (input + fallbacks)
    Trait->>FS: getFirstExisting(candidates)
    FS->>FS: expandPath(candidate) / exists(expanded)
    FS-->>Trait: first existing path or null
    Trait-->>Cmd: resolved publicKeyPath or null

    alt publicKeyPath resolved
        Cmd->>SSH: uploadFile(..., publicKeyPath)
        Note right of SSH #D0F0C0: SSHService requires explicit privateKeyPath\n(no internal resolution)
        SSH-->>Cmd: upload success
    else no publicKeyPath
        Cmd-->>Cmd: emit "public key not found" error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Verify all call sites of SSHService now supply explicit privateKeyPath.
  • Review FilesystemService::expandPath behavior and exception semantics when HOME is unset (cross-platform).
  • Inspect resolveKeyWithFallback candidate ordering and interplay between public/private fallback names.
  • Audit KeyValidationTrait key-prefix list and error messages for correctness (FIDO2/U2F, ssh-dss).
  • Check command prompt wording and propagated exception messages in KeyAddDigitalOceanCommand.

Possibly related PRs

  • bigpixelrocket/deployer-php#53 — Directly overlaps changes to KeyAddDigitalOceanCommand, resolver helpers, and FilesystemService utilities touched here.
  • bigpixelrocket/deployer-php#21 — Related to SSHService key-path handling and previous internal resolution behavior that this PR replaces.
  • bigpixelrocket/deployer-php#51 — Connected to DigitalOcean SSH key upload flow and FilesystemService-based key path resolution used by the command.

Poem

🐰 I nibbled tilde, sniffed the file,
I hopped the paths, I ran a mile.
Keys now found with clearer maps,
No secrets lost in hidden gaps.
A happy hop — the rabbit claps!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "refactor: improve key validation architecture" is directly related to the primary changes in this pull request. The changeset includes significant architectural improvements to key handling across multiple components: refactored key path resolution in KeyHelpersTrait (renamed expandKeyPath to resolvePrivateKeyPath, added resolvePublicKeyPath and resolveKeyWithFallback methods), enhanced key validation in KeyValidationTrait with FilesystemService integration and support for modern SSH key formats, and major restructuring of SSHService to require explicit privateKeyPath parameters while removing automatic resolution logic. The title captures the essence of these architectural improvements without being vague or overly broad, and a developer scanning history would clearly understand this is about improving the key validation and path handling architecture.
Docstring Coverage ✅ Passed Docstring coverage is 94.44% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/key-validation

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Traits/KeyValidationTrait.php (1)

27-37: Critical: Catch RuntimeException from expandPath to honor validation contract.

The call to $this->fs->expandPath($path) on line 32 can throw RuntimeException when the HOME environment variable is not set. This violates the coding guidelines for validation methods, which state: "Validation methods for prompts/options must accept mixed and return ?string error or null (do not throw exceptions)."

Apply this diff to catch and handle the exception:

     // Allow empty paths (will trigger default key resolution)
     if (trim($path) === '') {
         return null;
     }
 
-    $expandedPath = $this->fs->expandPath($path);
+    try {
+        $expandedPath = $this->fs->expandPath($path);
+    } catch (\RuntimeException $e) {
+        return $e->getMessage();
+    }
 
     // Check if file exists
     if (!$this->fs->exists($expandedPath)) {
🧹 Nitpick comments (1)
app/Services/FilesystemService.php (1)

133-143: Consider resilience to expandPath failures for fallback resolution.

Currently, if expandPath throws RuntimeException (e.g., HOME not set) while processing a tilde path, the entire method fails. This prevents trying subsequent non-tilde paths in the candidates array.

For example, if a user provides an absolute path and the fallback list includes ~/.ssh/id_ed25519, the current implementation would throw when HOME is not set instead of using the user's absolute path.

Consider catching and skipping paths that fail to expand:

 public function getFirstExisting(array $paths): ?string
 {
     foreach ($paths as $path) {
-        $expandedPath = $this->expandPath($path);
-        if ($this->exists($expandedPath)) {
-            return $expandedPath;
+        try {
+            $expandedPath = $this->expandPath($path);
+            if ($this->exists($expandedPath)) {
+                return $expandedPath;
+            }
+        } catch (\RuntimeException) {
+            // Skip paths that cannot be expanded (e.g., ~ paths when HOME not set)
+            continue;
         }
     }
 
     return null;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60539a8 and a5f605a.

📒 Files selected for processing (5)
  • app/Console/Key/KeyAddDigitalOceanCommand.php (3 hunks)
  • app/Services/FilesystemService.php (1 hunks)
  • app/Services/SSHService.php (9 hunks)
  • app/Traits/KeyHelpersTrait.php (2 hunks)
  • app/Traits/KeyValidationTrait.php (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/Services/FilesystemService.php
  • app/Console/Key/KeyAddDigitalOceanCommand.php
  • app/Traits/KeyValidationTrait.php
  • app/Services/SSHService.php
  • app/Traits/KeyHelpersTrait.php
**/*Service.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Service.php: Services must perform no console I/O and should accept/return plain PHP types
Services are dependency-injected via constructor and encapsulate business logic, external APIs, and file operations
Stateful services should use lazy loading and explicit initialization methods (e.g., load(), initialize()) and document requirements

Files:

  • app/Services/FilesystemService.php
  • app/Services/SSHService.php
**/*{Command,Service}.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

All dependencies should be expressed in constructor signatures; avoid circular dependencies

Files:

  • app/Services/FilesystemService.php
  • app/Console/Key/KeyAddDigitalOceanCommand.php
  • app/Services/SSHService.php
**/*Command.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Command.php: Commands handle user interaction (I/O), orchestration, styling, error formatting, and prompts using SymfonyStyle
Commands must not contain business logic; delegate to Services
Commands must not invoke other commands (no proxy commands)

Files:

  • app/Console/Key/KeyAddDigitalOceanCommand.php
**/*ValidationTrait.php

📄 CodeRabbit inference engine (.cursor/rules/03-commands.mdc)

**/*ValidationTrait.php: Validation methods for prompts/options must accept mixed and return ?string error or null (do not throw exceptions).
Naming: use validateInput() for prompt/option validators returning ?string; use validate() to throw exceptions for heavy I/O validations.

Files:

  • app/Traits/KeyValidationTrait.php
🧠 Learnings (1)
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/BaseCommand.php : If an IO helper is missing, add it to BaseCommand with modern styling instead of using Symfony IO directly.

Applied to files:

  • app/Traits/KeyHelpersTrait.php
🧬 Code graph analysis (4)
app/Console/Key/KeyAddDigitalOceanCommand.php (4)
app/Services/IOService.php (5)
  • getValidatedOptionOrPrompt (159-182)
  • promptText (200-218)
  • error (498-501)
  • success (482-485)
  • writeln (463-469)
app/Traits/KeyValidationTrait.php (1)
  • validateKeyPathInput (21-79)
app/Traits/KeyHelpersTrait.php (1)
  • resolvePublicKeyPath (55-61)
app/Services/DigitalOcean/DigitalOceanKeyService.php (1)
  • uploadKey (31-46)
app/Traits/KeyValidationTrait.php (1)
app/Services/FilesystemService.php (1)
  • expandPath (112-124)
app/Services/SSHService.php (1)
app/Services/FilesystemService.php (2)
  • exists (41-44)
  • readFile (51-54)
app/Traits/KeyHelpersTrait.php (1)
app/Services/FilesystemService.php (1)
  • getFirstExisting (133-143)
🔇 Additional comments (7)
app/Services/FilesystemService.php (1)

112-124: LGTM: Clean tilde expansion with appropriate error handling.

The implementation correctly handles empty paths and non-tilde paths, and throws a clear exception when HOME is not set for tilde paths.

app/Traits/KeyHelpersTrait.php (1)

32-85: LGTM: Clean key path resolution with sensible defaults.

The refactoring successfully centralizes key path resolution with a clear priority order. The implementation correctly:

  • Prioritizes user-provided paths over defaults
  • Uses modern SSH key types (Ed25519) as the first fallback
  • Delegates path expansion and existence checking to the filesystem service
  • Returns null when no valid path is found, allowing callers to handle appropriately
app/Console/Key/KeyAddDigitalOceanCommand.php (2)

62-82: LGTM: Clear separation of validation and resolution.

The refactored flow correctly:

  • Validates user input without modifying it
  • Resolves to an actual path with fallback defaults
  • Provides clear feedback when resolution fails
  • The prompt explicitly lists the default paths, improving user experience

The explicit null check on line 78 ensures users get a clear error when no valid key is found, rather than a cryptic failure downstream.


108-119: Good improvement: Using actual exception message.

Line 115 now uses $e->getMessage() instead of a generic error string, providing more actionable error information to users.

app/Services/SSHService.php (3)

17-38: Excellent documentation update.

The updated examples clearly demonstrate the new explicit path requirement and helpfully note that "commands resolve key paths before calling," making the architectural change clear to future maintainers.


42-60: LGTM: Cleaner separation of concerns.

Removing the EnvService dependency and requiring explicit privateKeyPath parameters simplifies the service's responsibilities. Path resolution is now correctly handled by callers (via KeyHelpersTrait), making the service stateless and focused solely on SSH operations.


248-267: LGTM: Simplified private key loading.

The loadPrivateKey method is now focused purely on loading and validating a provided path, with clear error messages for each failure mode. The removal of internal path resolution and fallback logic aligns with the PR's architectural goals.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/Traits/KeyValidationTrait.php (1)

32-36: Consider narrowing exception handling.

Catching \Throwable is very broad and can mask unexpected errors like OutOfMemoryError or ParseError. Since FilesystemService::expandPath() throws \RuntimeException (per the code snippet), consider catching \Exception instead.

However, for user-facing validation that must not crash, the current approach is defensible.

Apply this diff if you prefer narrower exception handling:

         try {
             $expandedPath = $this->fs->expandPath($path);
-        } catch (\Throwable $e) {
+        } catch (\Exception $e) {
             return $e->getMessage();
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5f605a and 1037d47.

📒 Files selected for processing (1)
  • app/Traits/KeyValidationTrait.php (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/Traits/KeyValidationTrait.php
**/*ValidationTrait.php

📄 CodeRabbit inference engine (.cursor/rules/03-commands.mdc)

**/*ValidationTrait.php: Validation methods for prompts/options must accept mixed and return ?string error or null (do not throw exceptions).
Naming: use validateInput() for prompt/option validators returning ?string; use validate() to throw exceptions for heavy I/O validations.

Files:

  • app/Traits/KeyValidationTrait.php
🧬 Code graph analysis (1)
app/Traits/KeyValidationTrait.php (1)
app/Services/FilesystemService.php (1)
  • expandPath (112-124)
🔇 Additional comments (2)
app/Traits/KeyValidationTrait.php (2)

27-30: LGTM: Empty path handling enables flexible default resolution.

Allowing empty paths to pass validation (returning null) is a sensible design that enables commands to implement their own default key resolution logic.


49-77: LGTM: Comprehensive SSH key type validation.

The expanded list of valid SSH key prefixes covers modern key types including FIDO2/U2F security keys, and explicitly rejects obsolete DSA keys with a clear error message. This aligns with current SSH security best practices.

Note: ssh-rsa is included but being phased out in newer OpenSSH versions. However, it remains widely used, so its inclusion is appropriate for compatibility.

Comment thread app/Traits/KeyValidationTrait.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/Services/FilesystemService.php (1)

133-148: Return a canonicalized path and accept iterable for flexibility.

Small ergonomic tweaks: accept iterable to allow generators, and normalize the returned path for cross-platform consistency.

-    /**
-     * @param array<int, string> $paths Array of file paths to check
-     * @return string|null First existing path (expanded), or null if none exist
-     */
-    public function getFirstExisting(array $paths): ?string
+    /**
+     * @param iterable<string> $paths Candidates to check (strings only)
+     * @return string|null First existing (expanded, canonicalized) path or null
+     */
+    public function getFirstExisting(iterable $paths): ?string
     {
         foreach ($paths as $path) {
             try {
                 $expandedPath = $this->expandPath($path);
                 if ($this->exists($expandedPath)) {
-                    return $expandedPath;
+                    return \Symfony\Component\Filesystem\Path::canonicalize($expandedPath);
                 }
             } catch (\RuntimeException) {
                 // Skip paths that cannot be expanded (e.g., ~ paths when HOME not set)
                 continue;
             }
         }
 
         return null;
     }

Optional: add unit tests for edge cases: "", "/", "~user", Windows env fallbacks, and canonicalization on mixed separators.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1037d47 and 041ade4.

📒 Files selected for processing (1)
  • app/Services/FilesystemService.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/Services/FilesystemService.php
**/*Service.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Service.php: Services must perform no console I/O and should accept/return plain PHP types
Services are dependency-injected via constructor and encapsulate business logic, external APIs, and file operations
Stateful services should use lazy loading and explicit initialization methods (e.g., load(), initialize()) and document requirements

Files:

  • app/Services/FilesystemService.php
**/*{Command,Service}.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

All dependencies should be expressed in constructor signatures; avoid circular dependencies

Files:

  • app/Services/FilesystemService.php

Comment thread app/Services/FilesystemService.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/Traits/KeyHelpersTrait.php (2)

44-66: Consider extracting hardcoded fallback paths to constants.

The fallback paths are currently hardcoded in each method. Extracting them to class constants would improve maintainability and make it easier to adjust the fallback order in the future.

Example refactor:

+    /** @var array<int, string> */
+    private const PRIVATE_KEY_FALLBACKS = [
+        '~/.ssh/id_ed25519',
+        '~/.ssh/id_rsa',
+    ];
+
+    /** @var array<int, string> */
+    private const PUBLIC_KEY_FALLBACKS = [
+        '~/.ssh/id_ed25519.pub',
+        '~/.ssh/id_rsa.pub',
+    ];
+
     protected function resolvePrivateKeyPath(?string $path): ?string
     {
-        return $this->resolveKeyWithFallback($path, [
-            '~/.ssh/id_ed25519',
-            '~/.ssh/id_rsa',
-        ]);
+        return $this->resolveKeyWithFallback($path, self::PRIVATE_KEY_FALLBACKS);
     }
 
     protected function resolvePublicKeyPath(?string $path): ?string
     {
-        return $this->resolveKeyWithFallback($path, [
-            '~/.ssh/id_ed25519.pub',
-            '~/.ssh/id_rsa.pub',
-        ]);
+        return $this->resolveKeyWithFallback($path, self::PUBLIC_KEY_FALLBACKS);
     }

83-84: Simplify redundant type check.

The is_string($path) check is redundant given the ?string type hint. PHP's type system guarantees that if $path is not null, it's already a string.

As per coding guidelines.

Apply this diff to simplify the check:

-        if (is_string($path) && $path !== '') {
+        if ($path !== null && $path !== '') {
             $candidates[] = $path;
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 041ade4 and 06c6bb9.

📒 Files selected for processing (3)
  • app/Traits/DigitalOceanCommandTrait.php (1 hunks)
  • app/Traits/KeyHelpersTrait.php (2 hunks)
  • app/Traits/KeyValidationTrait.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Traits/KeyValidationTrait.php
🧰 Additional context used
📓 Path-based instructions (1)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/Traits/DigitalOceanCommandTrait.php
  • app/Traits/KeyHelpersTrait.php
🧠 Learnings (1)
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/BaseCommand.php : If an IO helper is missing, add it to BaseCommand with modern styling instead of using Symfony IO directly.

Applied to files:

  • app/Traits/KeyHelpersTrait.php
🧬 Code graph analysis (1)
app/Traits/KeyHelpersTrait.php (1)
app/Services/FilesystemService.php (1)
  • getFirstExisting (133-148)

Comment thread app/Traits/DigitalOceanCommandTrait.php
Comment thread app/Traits/KeyHelpersTrait.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
app/Traits/DigitalOceanCommandTrait.php (1)

7-9: Imports added for annotated properties — looks good.

Resolves prior feedback about missing imports. Improves IDE/tooling support.

app/Traits/KeyHelpersTrait.php (1)

7-7: Import for FilesystemService — good.

Addresses earlier feedback to import classes referenced by @Property.

app/Traits/KeyValidationTrait.php (1)

7-15: FilesystemService dependency is now documented and imported — good.

Matches the ValidationTrait guidelines for declaring dependencies.

🧹 Nitpick comments (6)
app/Traits/DigitalOceanCommandTrait.php (1)

35-41: Remove redundant token null/empty check or make get(..., required: false).

EnvService::get throws \InvalidArgumentException when required (default). The manual null/empty check won’t run. Prefer one path:

  • Option A (simpler): rely on exception and drop the manual check.
  • Option B: call get([...], false) and keep the explicit check.

Diff for Option A:

         try {
-            $apiToken = $this->env->get(['DIGITALOCEAN_API_TOKEN', 'DO_API_TOKEN']);
-
-            if ($apiToken === null || $apiToken === '') {
-                throw new \InvalidArgumentException('DigitalOcean API token not found in environment');
-            }
+            $apiToken = $this->env->get(['DIGITALOCEAN_API_TOKEN', 'DO_API_TOKEN']);
app/Traits/KeyHelpersTrait.php (3)

46-52: Consider adding ECDSA private-key fallback.

Some setups still use ~/.ssh/id_ecdsa. Non-breaking addition:

 protected function resolvePrivateKeyPath(?string $path): ?string
 {
     return $this->resolveKeyWithFallback($path, [
         '~/.ssh/id_ed25519',
         '~/.ssh/id_rsa',
+        '~/.ssh/id_ecdsa',
     ]);
 }

62-68: Public key UX: try ".pub" first when a private key path is supplied.

Improves ergonomics if users paste a private key path by habit.

 protected function resolvePublicKeyPath(?string $path): ?string
 {
-    return $this->resolveKeyWithFallback($path, [
-        '~/.ssh/id_ed25519.pub',
-        '~/.ssh/id_rsa.pub',
-    ]);
+    $fallback = ['~/.ssh/id_ed25519.pub', '~/.ssh/id_rsa.pub'];
+
+    if (is_string($path) && $path !== '' && !str_ends_with($path, '.pub')) {
+        // Prefer "<path>.pub" if user provided a private key path
+        return $this->resolveKeyWithFallback(null, [$path . '.pub', $path, ...$fallback]);
+    }
+
+    return $this->resolveKeyWithFallback($path, $fallback);
 }

81-92: Ensure returned path is a file, not a directory.

FilesystemService::getFirstExisting considers directories. For key resolution we only want files. Two options:

  • Short term: add a file-only helper and use it here (preferred).
  • Alternative: reimplement the candidate scan locally checking is_dir/is_file.

If you want, I can draft a FilesystemService::getFirstExistingFile(array $paths): ?string and update callers.

app/Traits/KeyValidationTrait.php (2)

33-36: Improve diagnostics: show expanded path and preserve error cause.

  • Consider reporting the expanded path for not-found errors (helps when ~ expands unexpectedly).
  • Preserve underlying read errors to aid troubleshooting.
-        if (!$this->fs->exists($expandedPath)) {
-            return "SSH key file not found: {$path}";
-        }
+        if (!$this->fs->exists($expandedPath)) {
+            return "SSH key file not found: {$expandedPath}" . ($expandedPath !== $path ? " (from {$path})" : "");
+        }
@@
-        } catch (\Throwable) {
-            return 'Could not read SSH key file';
+        } catch (\Throwable $e) {
+            return 'Could not read SSH key file: ' . $e->getMessage();
         }

Also applies to: 38-42, 45-47


49-67: Validate the base64 blob, not just the key-type prefix.

Extra guard to reject corrupted keys while keeping the validate*Input contract:

-            $isValid = false;
-            foreach ($validPrefixes as $prefix) {
-                if (str_starts_with($publicKey, $prefix)) {
-                    $isValid = true;
-                    break;
-                }
-            }
-
-            if (!$isValid) {
+            // Split into "<type> <base64> [comment...]"
+            $parts = preg_split('/\s+/', $publicKey, 3);
+            if (!$parts || count($parts) < 2) {
+                return 'Invalid SSH public key format';
+            }
+            [$type, $b64] = $parts;
+
+            if (!in_array($type, $validPrefixes, true)) {
                 // Explicit error for obsolete DSA keys
-                if (str_starts_with($publicKey, 'ssh-dss')) {
+                if ($type === 'ssh-dss') {
                     return 'DSA (ssh-dss) keys are obsolete and insecure';
                 }
 
                 return 'Invalid SSH public key format';
             }
+
+            if (base64_decode($b64, true) === false) {
+                return 'Invalid SSH public key (corrupted base64 data)';
+            }

Also applies to: 68-84

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be8fae5 and 4d2d0d8.

📒 Files selected for processing (3)
  • app/Traits/DigitalOceanCommandTrait.php (1 hunks)
  • app/Traits/KeyHelpersTrait.php (2 hunks)
  • app/Traits/KeyValidationTrait.php (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use methods: inline if a method is called only once
Cache computed values: initialize expensive calculations in the constructor
Avoid method call overhead: prefer direct property access when appropriate

**/*.php: Adhere to PSR-12, enable strict_types, and leverage PHP 8.x features (union types, match, attributes, readonly)
Use explicit return types, including generic-like annotations where applicable (e.g., Collection<int, User>)
Prefer Symfony components (e.g., Filesystem, Process) over native PHP functions for testability
Always import classes via use statements; only use root FQDNs for core exceptions (e.g., \InvalidArgumentException, \RuntimeException). Do not use inline FQDNs for non-root namespaces
Create objects via $container->build(ClassName::class) everywhere except DTOs, value objects, and pure data structures
Use minimalist DocBlocks documenting description, parameters, and return types for classes and functions
Follow the specified comment structure with section headers/subheaders and spacing; remove obsolete comments with removed code

Files:

  • app/Traits/KeyValidationTrait.php
  • app/Traits/KeyHelpersTrait.php
  • app/Traits/DigitalOceanCommandTrait.php
**/*ValidationTrait.php

📄 CodeRabbit inference engine (.cursor/rules/03-commands.mdc)

**/*ValidationTrait.php: Validation methods for prompts/options must accept mixed and return ?string error or null (do not throw exceptions).
Naming: use validateInput() for prompt/option validators returning ?string; use validate() to throw exceptions for heavy I/O validations.

Files:

  • app/Traits/KeyValidationTrait.php
🧠 Learnings (1)
📚 Learning: 2025-10-24T20:01:06.209Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/03-commands.mdc:0-0
Timestamp: 2025-10-24T20:01:06.209Z
Learning: Applies to **/BaseCommand.php : If an IO helper is missing, add it to BaseCommand with modern styling instead of using Symfony IO directly.

Applied to files:

  • app/Traits/KeyHelpersTrait.php
🧬 Code graph analysis (3)
app/Traits/KeyValidationTrait.php (1)
app/Services/FilesystemService.php (2)
  • FilesystemService (28-168)
  • expandPath (113-143)
app/Traits/KeyHelpersTrait.php (1)
app/Services/FilesystemService.php (2)
  • FilesystemService (28-168)
  • getFirstExisting (152-167)
app/Traits/DigitalOceanCommandTrait.php (3)
app/Services/DigitalOceanService.php (1)
  • DigitalOceanService (17-144)
app/Services/EnvService.php (1)
  • EnvService (12-134)
app/Services/IOService.php (1)
  • IOService (30-583)
🔇 Additional comments (4)
app/Traits/DigitalOceanCommandTrait.php (1)

14-19: Property annotations clarify trait requirements — good.

Clear contract for EnvService, IOService, and DigitalOceanService on consumers.

app/Traits/KeyHelpersTrait.php (2)

102-136: Key selection flow reads well and matches IOService contract.

Return shape and constants are clear; prompt is only invoked when needed.


1-136: All callers have successfully migrated off the old expandKeyPath API.

Comprehensive search across the entire codebase confirms zero references to expandKeyPath exist—in PHP code, documentation, or any other file type. The migration to the new key resolution methods (resolvePrivateKeyPath, resolvePublicKeyPath, resolveKeyWithFallback) is complete.

app/Traits/KeyValidationTrait.php (1)

19-26: Naming and contract match guidelines.

validate*Input methods accept mixed and return ?string without throwing. Keep as-is.

If any heavy I/O validations are added later, mirror them in a throwing validatePublicKeyPath() and keep this method light per guidelines.

@loadinglucian
loadinglucian merged commit 18972ab into main Oct 27, 2025
4 of 5 checks passed
@loadinglucian
loadinglucian deleted the refactor/key-validation branch October 27, 2025 12:07
@coderabbitai coderabbitai Bot mentioned this pull request Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant