Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Aug 20, 2025

Remove the confusing API keys that were auto-generated for the CLI & Connect. Instead, support authentication via a custom local-session header.

Summary by CodeRabbit

  • New Features

    • Local-session authentication for internal/CLI requests (x-local-session) with generation, validation, on-disk persistence, and lifecycle init.
    • Internal client gains multi-strategy auth (local session, cookie, or API key), supports subscriptions/origin, and can be cleared/recreated.
  • Security

    • Embedded development API keys removed from the repository.
  • Refactor

    • Canonical internal client introduced; consumers migrated from legacy CLI key services.
  • Tests / Chores

    • Tests, env, and gitignore updated for local-session and canonical-client changes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 20, 2025

Walkthrough

Removed API-key-based CLI/admin key services and dev keys; introduced local-session authentication (service, strategy, lifecycle, guard and AuthService integration); added a canonical tokenized internal GraphQL client and generalized factory to accept API-key, cookie, or local-session auth; updated DI tokens, modules, and tests.

Changes

Cohort / File(s) Summary
Removed CLI/admin API-key services & dev keys
api/src/unraid-api/cli/admin-key.service.ts, api/src/unraid-api/cli/internal-client.service.ts, api/src/unraid-api/cli/internal-client.service.spec.ts, api/dev/keys/*fc91da7b-...*.json, api/dev/keys/*b5b4aa3d-...*.json
Deleted AdminKeyService, old CliInternalClientService and its spec, and two committed dev API key JSON files — removes prior API-key CLI/admin key management and stored dev credentials.
Local-session auth added
api/src/unraid-api/auth/local-session.service.ts, api/src/unraid-api/auth/local-session.strategy.ts, api/src/unraid-api/auth/local-session-lifecycle.service.ts
Added LocalSessionService (generate/get/validate/delete on-disk token), LocalSessionStrategy (reads x-local-session header), and LocalSessionLifecycleService (OnModuleInit → generateLocalSession).
Auth integration and guard changes
api/src/unraid-api/auth/auth.module.ts, api/src/unraid-api/auth/auth.service.ts, api/src/unraid-api/auth/auth.service.spec.ts, api/src/unraid-api/auth/authentication.guard.ts
Wire LocalSessionStrategy/Service into AuthModule and exports; inject LocalSessionService into AuthService; add validateLocalSession() and getLocalSessionUser(); include LocalSessionStrategy in AuthenticationGuard; update tests to include localSessionService mocks.
Cookie/session utilities
api/src/unraid-api/auth/cookie.service.ts
Extracted isSessionValid(), exported SessionCookieConfig type, and added getActiveSession() to discover an active session file.
Canonical internal client & factory generalization
api/src/unraid-api/shared/internal-client.service.ts, api/src/unraid-api/shared/internal-graphql-client.factory.ts, api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts, packages/unraid-shared/src/types/...factory.ts, packages/unraid-shared/src/types/canonical-internal-client.interface.ts, packages/unraid-shared/src/index.ts, packages/unraid-shared/src/tokens.ts
Added CanonicalInternalClientService interface and CANONICAL_INTERNAL_CLIENT_TOKEN; implemented InternalClientService using LocalSessionService; getClient accepts { enableSubscriptions?, origin? } and clearClient(); InternalGraphQLClientFactory.createClient now accepts optional getApiKey, getCookieAuth, and getLocalSession, and branches auth headers/WS params accordingly. Tokens renamed/added (INTERNAL_CLIENT_FACTORY_TOKEN, COOKIE_SERVICE_TOKEN, CANONICAL_INTERNAL_CLIENT_TOKEN).
Global deps & provider wiring
api/src/unraid-api/plugin/global-deps.module.ts
Replace internal client service token with factory token; register SESSION_COOKIE_CONFIG and COOKIE_SERVICE_TOKEN (CookieService); provide LocalSessionService and InternalClientService; export COOKIE_SERVICE_TOKEN, CANONICAL_INTERNAL_CLIENT_TOKEN, and INTERNAL_CLIENT_FACTORY_TOKEN.
CLI modules & services updated to canonical client
api/src/unraid-api/cli/cli-services.module.ts, api/src/unraid-api/cli/cli.module.ts, api/src/unraid-api/cli/cli.module.spec.ts, api/src/unraid-api/cli/api-report.service.ts, api/src/unraid-api/cli/developer/developer-tools.service.ts, api/src/unraid-api/cli/sso/validate-token.command.ts, api/src/unraid-api/cli/__test__/*
Removed AdminKeyService and CliInternalClientService from CLI providers/exports; switched injections to @Inject(CANONICAL_INTERNAL_CLIENT_TOKEN) CanonicalInternalClientService; CLI callers request client with { enableSubscriptions: false }; tests updated to token-based DI.
Connect plugin: remove ConnectApiKeyService & adapt modules
packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts, packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts, packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts, packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts, packages/unraid-api-plugin-connect/src/mothership-proxy/*, packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
Deleted ConnectApiKeyService and its tests; removed provider/export from ConnectModule; removed apiKey usage from ConnectSettingsService.signIn; adapted Mothership subscription handler to inject canonical client token; removed old InternalClientService/ConnectApiKeyService providers from mothership module.
Tests & integrations updated
api/src/unraid-api/cli/__test__/*.test.ts, api/src/unraid-api/rest/__test__/rest-module.integration.test.ts, api/src/unraid-api/graph/resolvers/api-key/*.spec.ts, api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
Updated tests to inject CANONICAL_INTERNAL_CLIENT_TOKEN and INTERNAL_CLIENT_FACTORY_TOKEN, bound SESSION_COOKIE_CONFIG in tests, and adjusted AuthService test constructors to include localSessionService mocks.
Env / ignore / environment constant
api/.env.development, api/.env.test, api/.gitignore, api/src/environment.ts
Added PATHS_LOCAL_SESSION_FILE env entries and PATHS_LOCAL_SESSION_FILE constant; ignored dev/local-session in .gitignore.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI as CLI / internal caller
  participant Canonical as CanonicalInternalClientService
  participant Local as LocalSessionService
  participant Factory as InternalGraphQLClientFactory

  CLI->>Canonical: getClient({ enableSubscriptions?, origin? })
  alt cached
    Canonical-->>CLI: ApolloClient (cached)
  else create
    Canonical->>Local: getLocalSession()
    Local-->>Canonical: token | null
    Canonical->>Factory: createClient({ getLocalSession, getCookieAuth?, getApiKey?, enableSubscriptions, origin })
    Factory-->>Canonical: ApolloClient
    Canonical-->>CLI: ApolloClient
  end
Loading
sequenceDiagram
  autonumber
  participant Request as Incoming HTTP request
  participant Guard as AuthenticationGuard
  participant LSS as LocalSessionStrategy
  participant AS as AuthService
  participant LSV as LocalSessionService

  Request->>Guard: authenticate (ServerHeader, LocalSession, UserCookie)
  Guard->>LSS: validate(request)
  LSS->>AS: validateLocalSession(token)
  AS->>LSV: validateLocalSession(token)
  LSV-->>AS: boolean
  AS->>AS: getLocalSessionUser(), sync roles
  AS-->>LSS: UserAccount | null
  LSS-->>Guard: UserAccount | null
  Guard-->>Request: Continue or 401
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Poem

I hop through code with nimble cheer,
Keys tucked away, a token near.
A tiny file hums soft and bright,
Canonical client wakes at night.
The rabbit guards the session light 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/memory-keys

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@claude
Copy link

claude bot commented Aug 20, 2025

Claude finished @elibosley's task —— View job


Code Review in Progress

Reviewing the diff/changeset for critical issues only:

  • Read CLAUDE.md for repository guidelines
  • Examine the actual diff/changeset
  • Look for critical bugs, security issues, breaking changes, and data loss risks in changed lines only
  • Post review findings

Review Findings

✅ No critical issues found in changes

The diff shows only one file modified (api/src/unraid-api/auth/local-session.service.ts) with improvements to the validateLocalSession method:

  • Security Enhancement: Lines 64-80 add proper input validation and length checking before calling timingSafeEqual(), which prevents potential TypeErrors that could crash the authentication process.
  • Defensive Programming: Added null/undefined coercion and early returns for empty inputs.
  • Same Logic: The core authentication logic remains unchanged - still uses constant-time comparison to prevent timing attacks.

The changes are defensive improvements that make the authentication more robust without introducing breaking changes or security vulnerabilities.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (8)
api/src/unraid-api/cli/internal-client.service.ts (2)

18-21: Cache the CLI key to avoid repeated disk I/O in getApiKey callbacks

getApiKey is invoked for each client request; repeatedly hitting the filesystem is unnecessary since the CLI key is stable for the process lifetime. Cache after first successful read (file or service), and reuse.

Apply this diff:

 export class CliInternalClientService {
     private readonly logger = new Logger(CliInternalClientService.name);
     private client: ApolloClient<NormalizedCacheObject> | null = null;
     private creatingClient: Promise<ApolloClient<NormalizedCacheObject>> | null = null;
+    // Cache to avoid repeated FS reads per request
+    private cachedApiKey: string | null = null;

     /**
      * Get the admin API key for CLI operations.
      * First tries to read from /var/run/unraid-api/cli.key, then falls back to AdminKeyService.
      */
     private async getLocalApiKey(): Promise<string> {
+        if (this.cachedApiKey) {
+            return this.cachedApiKey;
+        }
         // Try to read ephemeral key from /var/run first
         try {
             const ephemeralKey = await readFile('/var/run/unraid-api/cli.key', 'utf-8');
             if (ephemeralKey) {
                 this.logger.debug('Using ephemeral CLI key from runtime directory');
-                return ephemeralKey.trim();
+                this.cachedApiKey = ephemeralKey.trim();
+                return this.cachedApiKey;
             }
         } catch (error) {
             // File doesn't exist or not readable, fall back to service
             this.logger.debug('Ephemeral key file not found, falling back to AdminKeyService');
         }

         // Fall back to AdminKeyService
         try {
-            return await this.adminKeyService.getOrCreateLocalAdminKey();
+            const key = await this.adminKeyService.getOrCreateLocalAdminKey();
+            this.cachedApiKey = key;
+            return key;
         } catch (error) {
             this.logger.error('Failed to get admin API key:', error);
             throw new Error(
                 'Unable to get admin API key for internal client. Ensure the API server is running.'
             );
         }
     }

Also applies to: 33-53


41-43: Improve debug signal for read failures

The log implies “file not found,” but permission or other IO errors will also land here. Include error.code to aid triage without increasing verbosity.

Apply this diff:

-        } catch (error) {
-            // File doesn't exist or not readable, fall back to service
-            this.logger.debug('Ephemeral key file not found, falling back to AdminKeyService');
+        } catch (error: any) {
+            // File missing or unreadable; fall back to service
+            this.logger.debug(
+                `Ephemeral key file unavailable (reason: ${error?.code ?? 'unknown'}), falling back to AdminKeyService`
+            );
         }
api/src/unraid-api/cli/admin-key.service.ts (2)

4-4: Remove unused import

join is not used.

Apply this diff:

-import { join } from 'path';

53-58: Tighten runtime directory permissions

Since the key is sensitive, prefer a restrictive directory mode when creating /var/run/unraid-api. Existing dirs won’t be changed, but this helps on first creation.

Apply this diff:

-            await mkdir(runDir, { recursive: true });
+            await mkdir(runDir, { recursive: true, mode: 0o700 });
             await writeFile(
                 AdminKeyService.RUNTIME_KEY_PATH,
                 this.ephemeralKey,
                 { mode: 0o600 } // Root only
             );
packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (1)

96-106: On-demand token generation is fine; consider guarding for interface drift

If updating @unraid/shared is delayed, the extended type handles compile-time. Optionally, at runtime, guard for missing method and fall back to a persistent key to avoid startup failure.

Proposed pattern (no diff required):

  • If 'registerModule' in this.apiKeyService is not a function, call this.apiKeyService.getOrCreateLocalKey('Module-Connect', 'Persistent key fallback', [Role.CONNECT]) and use that as the token.
api/src/unraid-api/auth/api-key.service.ts (3)

7-7: Remove unused imports (watch, environment)

File watching was removed and environment isn’t referenced.

Apply this diff:

-import { watch } from 'chokidar';
@@
-import { environment } from '@app/environment.js';

Also applies to: 14-14


101-109: Consider allowing hyphens/underscores in persistent key names

Ephemeral names like “Module-connect” contain hyphens; create() disallows them via sanitizeName. For consistency and to avoid surprising UX, broaden allowed chars.

Apply this diff:

-    if (/^[\p{L}\p{N} ]+$/u.test(name)) {
+    if (/^[\p{L}\p{N}_\- ]+$/u.test(name)) {
         return name;
     } else {
         throw new GraphQLError(
             'API key name must contain only letters, numbers, and spaces (Unicode letters are supported)'
         );
     }

And update the error message accordingly if you adopt this change.


277-295: Minor DRY opportunity in lookups

findByField and findByKey duplicate iteration logic. A small helper to query ephemeral first, then persistent, would reduce drift. Low priority.

Example (no diff required):

  • Implement a private iterateKeys(): Iterable that yields ephemeral then persistent; reuse in both methods.

Also applies to: 297-315

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6edd3a3 and 7f7e027.

📒 Files selected for processing (4)
  • api/src/unraid-api/auth/api-key.service.ts (7 hunks)
  • api/src/unraid-api/cli/admin-key.service.ts (2 hunks)
  • api/src/unraid-api/cli/internal-client.service.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/cli/internal-client.service.ts
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/cli/internal-client.service.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/cli/internal-client.service.ts
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
🧠 Learnings (4)
📚 Learning: 2024-11-04T20:44:46.432Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:44:46.432Z
Learning: When modifying `apiKey.roles` in `removeRoleFromApiKey` and `addRoleToApiKey` within `api/src/unraid-api/auth/auth.service.ts`, concurrency issues are not a concern because the keys are stored in the file system.

Applied to files:

  • api/src/unraid-api/cli/internal-client.service.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.

Applied to files:

  • api/src/unraid-api/cli/internal-client.service.ts
📚 Learning: 2024-11-04T20:41:22.303Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:41:22.303Z
Learning: In `api/src/unraid-api/auth/auth.service.ts`, the `addRoleToApiKey` function operates on API keys stored as JSON files in a directory, not a database, so concurrency is not a concern for modifying `apiKey.roles`.

Applied to files:

  • api/src/unraid-api/cli/internal-client.service.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
📚 Learning: 2024-11-15T16:22:03.485Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:176-188
Timestamp: 2024-11-15T16:22:03.485Z
Learning: Atomic writes are not required for the `saveApiKey` method in `ApiKeyService` (`api/src/unraid-api/auth/api-key.service.ts`) unless specifically needed.

Applied to files:

  • api/src/unraid-api/cli/internal-client.service.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
🧬 Code Graph Analysis (3)
packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (2)
api/src/unraid-api/auth/api-key.service.ts (1)
  • Injectable (25-555)
packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.ts (1)
  • Injectable (13-74)
api/src/unraid-api/auth/api-key.service.ts (2)
web/composables/gql/graphql.ts (1)
  • ApiKeyWithSecret (202-211)
api/src/utils.ts (1)
  • batchProcess (58-71)
api/src/unraid-api/cli/admin-key.service.ts (2)
api/src/unraid-api/cli/internal-client.service.ts (1)
  • Injectable (16-111)
api/src/unraid-api/auth/api-key.service.ts (1)
  • Injectable (25-555)
🪛 GitHub Actions: CI - Main (API)
packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts

[error] 21-21: Step 'npm run build' failed in package 'packages/unraid-api-plugin-connect' with TS2339: Property 'registerModule' does not exist on type 'ApiKeyService' (packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts:21:57).

🪛 GitHub Check: CodeQL
api/src/unraid-api/cli/admin-key.service.ts

[notice] 4-4: Unused variable, import, function or class
Unused import join.

🔇 Additional comments (5)
api/src/unraid-api/cli/internal-client.service.ts (1)

66-99: Client creation concurrency guard looks solid

The promise-sharing pattern with the equality check prevents double-creation and misassignment. finally resets creatingClient only if current. Good.

api/src/unraid-api/cli/admin-key.service.ts (1)

38-64: Ephemeral CLI key generation and in-memory registration aligns with the new model

Secure random generation, in-memory registration via ApiKeyService, and best-effort write to /var/run provide a robust flow with graceful fallback.

Also applies to: 66-80

packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (2)

1-16: Imports follow ESM .js specifiers; good

Imports use .js where applicable for ESM compatibility with our TS settings.


18-26: Use pnpm to rebuild only the Connect plugin after the type fix

The monorepo uses pnpm workspaces rather than npm workspaces, so the original npm run build -w … command will not work. To verify that the TS2339 error has been resolved, rebuild just the Connect package with:

pnpm --filter unraid-api-plugin-connect run build

This will compile packages/unraid-api-plugin-connect in isolation and confirm that the type extension fix removes the TS2339 error.

Likely an incorrect or invalid review comment.

api/src/unraid-api/auth/api-key.service.ts (1)

39-54: Separation of persistent vs. ephemeral keys is coherent and internally consistent

  • Startup loads disk-backed keys, cleans legacy, and rebuilds the in-memory view.
  • Lookups prioritize ephemeral keys; findAll returns only persistent.
  • delete/update correctly distinguish persistence boundaries and refresh caches.

Also applies to: 68-75, 321-369, 431-455, 490-498

@pujitm pujitm force-pushed the feat/memory-keys branch 3 times, most recently from b87d158 to 1c6fcfb Compare August 26, 2025 22:24
@pujitm pujitm changed the title feat(api): implement ephemeral and persistent API key management feat(api): implement local-session for internal client auth Aug 26, 2025
@pujitm pujitm changed the title feat(api): implement local-session for internal client auth refactor: implement local-session for internal client auth Aug 26, 2025
@pujitm
Copy link
Member

pujitm commented Aug 26, 2025

@coderabbitai summary

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

✅ Actions performed

Summary regeneration triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (4)
packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts (1)

26-26: Controller is registered as a provider; routes won’t be mounted

NestJS requires controllers to be listed under the controllers array, not providers. With the current registration, HTTP routes from MothershipController will not be exposed.

Apply this diff to move the controller to the correct slot:

 @Module({
   imports: [RemoteAccessModule],
   providers: [
     ConnectStatusWriterService,
     MothershipConnectionService,
     MothershipGraphqlClientService,
     MothershipHandler,
     MothershipSubscriptionHandler,
     TimeoutCheckerJob,
     CloudService,
     CloudResolver,
-    MothershipController,
   ],
+  controllers: [MothershipController],
   exports: [],
 })
 export class MothershipModule {}
packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (1)

137-144: Validate port server-side for static forwarding.

UI validation exists, but API should also enforce bounds when access is enabled with STATIC forwarding.

         if (settings.accessType) {
+            if (
+                settings.accessType === WAN_ACCESS_TYPE.ALWAYS &&
+                settings.forwardType === WAN_FORWARD_TYPE.STATIC
+            ) {
+                const p = Number(settings.port);
+                if (!Number.isInteger(p) || p < 1 || p > 65535) {
+                    throw new GraphQLError(
+                        'Port must be an integer between 1 and 65535 when using static port forwarding.'
+                    );
+                }
+            }
             await this.updateRemoteAccess({
                 accessType: settings.accessType,
                 forwardType: settings.forwardType,
                 port: settings.port,
             });
         }
api/src/unraid-api/auth/authentication.guard.ts (1)

98-105: Potential crash parsing cookies on GraphQL WS handshake when no Cookie header

If the Cookie header is missing, headerIndex becomes -1 and rawHeaders[headerIndex + 1] indexes the first header value, and parseCookies(undefined) will throw. Guard against missing/invalid header values.

Apply this diff:

-            if (fullContext.connectionParams) {
-                const rawHeaders = fullContext.req.extra.request.rawHeaders;
-                const headerIndex = rawHeaders.findIndex(
-                    (headerOrValue) => headerOrValue.toLowerCase() === 'cookie'
-                );
-                const cookieString = rawHeaders[headerIndex + 1];
-                request.cookies = parseCookies(cookieString);
-            }
+            if (fullContext.connectionParams) {
+                const rawHeaders = fullContext.req?.extra?.request?.rawHeaders ?? [];
+                const headerIndex = rawHeaders.findIndex(
+                    (h) => typeof h === 'string' && h.toLowerCase() === 'cookie'
+                );
+                if (headerIndex !== -1 && typeof rawHeaders[headerIndex + 1] === 'string') {
+                    const cookieString = rawHeaders[headerIndex + 1] as string;
+                    request.cookies = parseCookies(cookieString);
+                }
+            }
api/src/unraid-api/cli/api-report.service.ts (1)

146-154: Avoid unhandled rejections with Promise.race timeouts; factor a withTimeout helper

Using Promise.race with a rejecting timeout Promise can produce unhandledRejection warnings when the query resolves first and the timer later rejects. Wrap the awaited promise so the timer is always cleared and only a single promise settles.

Apply these diffs to replace each timeout block and add a helper:

-            systemResult = await Promise.race([
-                client.query({
-                    query: SYSTEM_REPORT_QUERY,
-                }),
-                new Promise<never>((_, reject) =>
-                    setTimeout(() => reject(new Error('Query timeout after 10 seconds')), 10000)
-                ),
-            ]);
+            systemResult = await this.withTimeout(
+                client.query({ query: SYSTEM_REPORT_QUERY }),
+                'System query',
+                10_000
+            );
@@
-            const connectResult = await Promise.race([
-                client.query({
-                    query: CONNECT_STATUS_QUERY,
-                }),
-                new Promise<never>((_, reject) =>
-                    setTimeout(() => reject(new Error('Connect query timeout after 10 seconds')), 10000)
-                ),
-            ]);
+            const connectResult = await this.withTimeout(
+                client.query({ query: CONNECT_STATUS_QUERY }),
+                'Connect query',
+                10_000
+            );
@@
-            const servicesResult = await Promise.race([
-                client.query({
-                    query: SERVICES_QUERY,
-                }),
-                new Promise<never>((_, reject) =>
-                    setTimeout(() => reject(new Error('Services query timeout after 10 seconds')), 10000)
-                ),
-            ]);
+            const servicesResult = await this.withTimeout(
+                client.query({ query: SERVICES_QUERY }),
+                'Services query',
+                10_000
+            );

Add this helper method (outside the selected ranges):

private withTimeout<T>(promise: Promise<T>, label: string, ms = 10_000): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms / 1000} seconds`)), ms);
    promise.then(
      (val) => { clearTimeout(timer); resolve(val); },
      (err) => { clearTimeout(timer); reject(err); },
    );
  });
}

Also applies to: 165-172, 181-187

🧹 Nitpick comments (29)
api/src/unraid-api/auth/cookie.service.ts (2)

70-75: Improve error logging to preserve stack and context

Logger.error expects a message and optional stack; passing the error object as the message drops stack/context. Log a clear message and the stack.

-        } catch (e) {
-            this.logger.error(e, 'Error reading session file');
-            return false;
-        }
+        } catch (e) {
+            const err = e as Error;
+            this.logger.error('Error reading session file', err?.stack ?? String(e), CookieService.name);
+            return false;
+        }

70-71: Encoding: prefer utf8 over ascii for session files

If session payloads include non-ASCII characters, 'ascii' may corrupt data. 'utf8' is safer for PHP session contents.

-            const sessionData = await readFile(sessionFile, 'ascii');
+            const sessionData = await readFile(sessionFile, 'utf8');
-            const sessionData = await readFile(join(this.opts.sessionDir, sessionFile), 'ascii');
+            const sessionData = await readFile(join(this.opts.sessionDir, sessionFile), 'utf8');

Also applies to: 106-106

packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)

9-14: Make “at least one auth source” a type-level guarantee

With all auth getters optional, callers can compile with none provided and fail at runtime. Encode “at least one of getApiKey | getCookieAuth | getLocalSession” in the type to prevent misuse.

-export interface InternalGraphQLClientFactory {
-    createClient(options: {
-        getApiKey?: () => Promise<string>;
-        getCookieAuth?: () => Promise<{ sessionId: string; csrfToken: string } | null>;
-        getLocalSession?: () => Promise<string | null>;
-        enableSubscriptions?: boolean;
-        origin?: string;
-    }): Promise<ApolloClient<NormalizedCacheObject>>;
-}
+type AtLeastOne<T, Keys extends keyof T = keyof T> =
+  Partial<T> & { [K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>> }[Keys];
+
+export interface InternalGraphQLClientFactory {
+    createClient(options: AtLeastOne<{
+        getApiKey: () => Promise<string>;
+        getCookieAuth: () => Promise<{ sessionId: string; csrfToken: string } | null>;
+        getLocalSession: () => Promise<string | null>;
+    }> & {
+        enableSubscriptions?: boolean;
+        origin?: string;
+    }): Promise<ApolloClient<NormalizedCacheObject>>;
+}
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)

8-13: Extract an options type and narrow the public surface.

Defining a named options type improves reuse and avoids inline structural typing in the interface.

Apply this change to the method signature:

-    getClient(options?: { enableSubscriptions?: boolean; origin?: string }): Promise<ApolloClient<NormalizedCacheObject>>;
+    getClient(options?: CanonicalInternalClientOptions): Promise<ApolloClient<NormalizedCacheObject>>;

Add this type alongside the interface (above it):

export type CanonicalInternalClientOptions = Readonly<{
  enableSubscriptions?: boolean;
  origin?: string;
}>;
packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (3)

184-189: Sanitize error surface on sign-in and log the original error.

Avoid leaking raw error objects to clients. Log internally; return a generic message.

-            } catch (error) {
-                throw new GraphQLError(`Failed to login user: ${error}`, {
-                    extensions: { code: 'INTERNAL_SERVER_ERROR' },
-                });
-            }
+            } catch (error) {
+                this.logger.error('Failed to login user', error as Error);
+                throw new GraphQLError('Failed to login user.', {
+                    extensions: { code: 'INTERNAL_SERVER_ERROR' },
+                });
+            }

14-14: Remove unused import decodeJwt.

Not referenced in this file.

-import { decodeJwt } from 'jose';

122-147: Return value restartRequired is never toggled.

The method advertises a “restart required” signal but always returns false. Either compute it (e.g., based on effective changes) or drop the return value.

Would you like me to propose a small change-tracking helper that compares prior vs. updated settings and sets this flag accordingly?

api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (1)

89-89: Tests: avoid asserting exact error messages in rejects.toThrow.

Per our testing guidelines, prefer .rejects.toThrow() without arguments to reduce brittleness.

-            await expect(resolver.create(input)).rejects.toThrow('Create failed');
+            await expect(resolver.create(input)).rejects.toThrow();

-            await expect(resolver.create(input)).rejects.toThrow('Sync failed');
+            await expect(resolver.create(input)).rejects.toThrow();

-            await expect(resolver.addRole(input)).rejects.toThrow('API key not found');
+            await expect(resolver.addRole(input)).rejects.toThrow();

Also applies to: 101-102, 142-143

api/src/unraid-api/auth/authentication.guard.ts (1)

51-52: Preserve stack when logging errors

Logger.error(message, stack?) expects a string stack/context. Passing the Error object may stringify poorly. Prefer logging stack (or message) explicitly.

-            this.logger.error('Error in handleRequest', err);
+            this.logger.error('Error in handleRequest', (err as Error)?.stack ?? String(err));
api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (1)

3-3: Type the override to the canonical internal client interface

Add the interface import and type the override to catch shape drift at compile time.

-import { CANONICAL_INTERNAL_CLIENT_TOKEN } from '@unraid/shared';
+import type { CanonicalInternalClientService } from '@unraid/shared';
+import { CANONICAL_INTERNAL_CLIENT_TOKEN } from '@unraid/shared';
@@
-            .overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN)
-            .useValue({ getClient: vi.fn() })
+            .overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN)
+            .useValue({ getClient: vi.fn() } as unknown as CanonicalInternalClientService)

Also applies to: 66-67

api/src/unraid-api/cli/__test__/api-report.service.test.ts (1)

22-25: Optional: enforce mock shape with satisfies

Use the satisfies operator so the mock must implement the interface without needing type assertions.

const mockInternalClientService = {
  getClient: vi.fn().mockResolvedValue(mockClient),
  clearClient: vi.fn(),
} satisfies CanonicalInternalClientService;
api/src/unraid-api/auth/auth.service.spec.ts (3)

101-104: Align error assertions with testing guidelines

Per our testing guidelines, prefer .rejects.toThrow() without arguments to avoid coupling tests to error classes/messages unless those are the subject under test.

Here’s an example; apply similarly to the other cases highlighted above:

- await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow(
-   UnauthorizedException
- );
+ await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow();

Another example:

- await expect(authService.syncApiKeyRoles('test-id', ['new-role'])).rejects.toThrow(
-   'Failed to delete role'
- );
+ await expect(authService.syncApiKeyRoles('test-id', ['new-role'])).rejects.toThrow();

Also applies to: 112-115, 137-140, 176-178, 186-189, 223-228, 259-262


79-79: Minor naming nit: describe block

The block name "validateCookiesCasbin" doesn’t match the method under test (validateCookiesWithCsrfToken). Consider renaming for clarity in test output.


63-73: Add coverage for the local-session path

Since LocalSessionService is a new auth path, consider a focused test that exercises validateLocalSession interactions (success and failure flows) in AuthService to lock in behavior.

If you want, I can draft a minimal test suite that stubs validateLocalSession to return a user and verifies role syncing and CSRF behavior gates.

api/src/unraid-api/auth/auth.module.ts (1)

21-25: Remove redundant module-level defaultStrategy configuration

The AuthenticationGuard explicitly extends AuthGuard with the exact same strategy list ([ServerHeaderStrategy.key, LocalSessionStrategy.key, UserCookieStrategy.key]), so the defaultStrategy defined in auth.module.ts is redundant and may cause confusion about which list is actually in effect. Consider removing or simplifying it to a single fallback.

  • In api/src/unraid-api/auth/auth.module.ts (lines 21–25), drop the multi-strategy array:
     PassportModule.register({
  • defaultStrategy: [
  •  ServerHeaderStrategy.key,
    
  •  LocalSessionStrategy.key,
    
  •  UserCookieStrategy.key,
    
  • ],
    }),
- Retain or define only a single `defaultStrategy` here if you need a module-wide fallback for any guards that don’t specify their own.
- Since **api/src/unraid-api/auth/authentication.guard.ts** line 41 already specifies the full chain, all route handlers using this guard will continue to work as intended.

</blockquote></details>
<details>
<summary>api/src/unraid-api/auth/local-session.strategy.ts (3)</summary><blockquote>

`23-31`: **Optional: restrict acceptance to loopback to reduce exposure**

Even with a strong token, accepting x-local-session from any source increases risk if a reverse proxy misroutes headers. Consider enforcing loopback-only requests for this strategy.  

Apply:

```diff
async validate(request: FastifyRequest): Promise<UserAccount | null> {
 try {
   const localSessionToken = request.headers['x-local-session'] as string;

   if (!localSessionToken) {
     this.logger.verbose('No local session token found in request headers');
     return null;
   }
+    // Reduce attack surface: only allow local loopback sources
+    const ip = (request as any).ip ?? (request as any).socket?.remoteAddress;
+    const isLoopback = ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
+    if (!isLoopback) {
+      this.logger.warn(`Rejected local-session auth from non-loopback address: ${ip}`);
+      return null;
+    }

41-44: Fix logger usage in catch; current call swaps message/context

Logger.verbose(message, context?) doesn’t accept an error/trace parameter; your call logs the error object as the message and the string as context. Use error() to include a stack or format explicitly.

-      this.logger.verbose(error, `Local session validation failed`);
+      this.logger.error(
+        'Local session validation failed',
+        error instanceof Error ? error.stack : String(error)
+      );

23-38: Add tests for happy-path, missing header, and invalid token

To prevent regressions:

  • No header → returns null
  • Invalid token → returns null
  • Valid token → returns UserAccount with ADMIN role (per AuthService.getLocalSessionUser)

I can generate Jest tests that mock AuthService.validateLocalSession to cover these cases. Want me to add them?

api/src/unraid-api/cli/sso/validate-token.command.ts (2)

52-52: Query error handling may miss GraphQL errors unless errorPolicy is 'all'

Depending on Apollo Client configuration, GraphQL errors often throw instead of populating result.errors. If you intend to read result.errors, set errorPolicy: 'all'.

-      const client = await this.internalClient.getClient({ enableSubscriptions: false });
+      const client = await this.internalClient.getClient({ enableSubscriptions: false });
       const { data, errors } = await client.query({
         query: VALIDATE_OIDC_SESSION_QUERY,
         variables: { token },
+        errorPolicy: 'all',
       });

52-52: Optionally tag origin for observability

If the internal client forwards an Origin header, consider setting a recognizable origin to aid server-side diagnostics.

-      const client = await this.internalClient.getClient({ enableSubscriptions: false });
+      const client = await this.internalClient.getClient({
+        enableSubscriptions: false,
+        origin: 'cli://validate-token',
+      });
packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (1)

32-36: Canonical internal client injection via token is correct; be explicit about WS needs

Constructor DI via CANONICAL_INTERNAL_CLIENT_TOKEN is aligned with the shared contract. Downstream, consider being explicit about (dis)enabling subscriptions to avoid unintended WS initialization.

-    const client = await this.internalClientService.getClient();
+    const client = await this.internalClientService.getClient({ enableSubscriptions: true });

And for query-only paths:

-    const internalClient = await this.internalClientService.getClient();
+    const internalClient = await this.internalClientService.getClient({ enableSubscriptions: false });
api/src/unraid-api/auth/auth.service.ts (2)

263-263: Consider using debug log level for consistency.

The log level for getSessionUser() was changed from debug to verbose (line 263), but getLocalSessionUser() also uses verbose (line 280). For consistency with other authentication methods in this service, consider using debug level for both methods.

-        this.logger.verbose('getSessionUser called!');
+        this.logger.debug('getSessionUser called!');
-        this.logger.verbose('getLocalSessionUser called!');
+        this.logger.debug('getLocalSessionUser called!');

Also applies to: 280-280


279-288: Document the user ID convention for system accounts.

The local session user uses ID -2 to distinguish it from the regular session user (ID -1). This convention should be documented to prevent ID conflicts.

Consider adding a comment or constants to clarify the ID scheme:

+    // System account IDs:
+    // -1: Web GUI session user (admin)
+    // -2: Local CLI/system session user (local-admin)
     async getLocalSessionUser(): Promise<UserAccount> {
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)

114-152: Consider extracting authentication header construction to reduce duplication.

The authentication logic is duplicated between HTTP headers (lines 114-152) and WebSocket connectionParams (lines 166-190). Consider extracting this to a shared helper method.

+    private async getAuthHeaders(
+        getApiKey?: () => Promise<string>,
+        getLocalSession?: () => Promise<string | null>,
+        getCookieAuth?: () => Promise<{ sessionId: string; csrfToken: string } | null>
+    ): Promise<Record<string, string>> {
+        if (getApiKey) {
+            const apiKey = await getApiKey();
+            return { 'x-api-key': apiKey };
+        } else if (getLocalSession) {
+            const localSession = await getLocalSession();
+            if (!localSession) {
+                throw new Error('No valid local session found');
+            }
+            return { 'x-local-session': localSession };
+        } else if (getCookieAuth) {
+            const cookieAuth = await getCookieAuth();
+            if (!cookieAuth) {
+                throw new Error('No valid session found for cookie authentication');
+            }
+            return {
+                'x-csrf-token': cookieAuth.csrfToken,
+                cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
+            };
+        }
+        return {};
+    }

     // Create auth link that dynamically fetches authentication info for each request
     const authLink = setContext(async (_, { headers }) => {
-        if (getApiKey) {
-            // Use API key authentication
-            ...
-        } else if (getLocalSession) {
-            // Use local session authentication
-            ...
-        } else if (getCookieAuth) {
-            // Use cookie-based authentication
-            ...
-        }
-
-        return { headers };
+        const authHeaders = await this.getAuthHeaders(getApiKey, getLocalSession, getCookieAuth);
+        return {
+            headers: {
+                ...headers,
+                ...authHeaders,
+            },
+        };
     });

Then reuse in the WebSocket connectionParams:

     connectionParams: async () => {
-        if (getApiKey) {
-            ...
-        } else if (getLocalSession) {
-            ...
-        } else if (getCookieAuth) {
-            ...
-        }
-        return {};
+        return await this.getAuthHeaders(getApiKey, getLocalSession, getCookieAuth);
     },
api/src/unraid-api/auth/local-session.service.ts (2)

14-14: Consider making the session file path configurable.

The hardcoded path /var/run/unraid-api/local-session might not be suitable for all deployment environments (e.g., development, testing).

-    private static readonly SESSION_FILE_PATH = '/var/run/unraid-api/local-session';
+    private static readonly SESSION_FILE_PATH = process.env.LOCAL_SESSION_PATH || '/var/run/unraid-api/local-session';

81-90: Consider using Node.js built-in crypto.timingSafeEqual for constant-time comparison.

While the current implementation is correct, Node.js provides a native constant-time comparison function that's more efficient.

+import { randomBytes, timingSafeEqual } from 'crypto';

     /**
      * Constant-time string comparison to prevent timing attacks
      */
     private constantTimeEqual(a: string, b: string): boolean {
         if (a.length !== b.length) return false;
-
-        let result = 0;
-        for (let i = 0; i < a.length; i++) {
-            result |= a.charCodeAt(i) ^ b.charCodeAt(i);
-        }
-
-        return result === 0;
+        
+        const bufferA = Buffer.from(a);
+        const bufferB = Buffer.from(b);
+        return timingSafeEqual(bufferA, bufferB);
     }
api/src/unraid-api/cli/cli.module.spec.ts (2)

27-31: Type module.get() for safer assertions.

Provide the token’s interface to module.get so failures are caught at compile time.

Apply this diff:

-it('should provide CanonicalInternalClient', () => {
-    const service = module.get(CANONICAL_INTERNAL_CLIENT_TOKEN);
+it('should provide CanonicalInternalClient', () => {
+    const service = module.get<import('@unraid/shared').CanonicalInternalClientService>(
+        CANONICAL_INTERNAL_CLIENT_TOKEN
+    );
     expect(service).toBeDefined();
     expect(service.getClient).toBeInstanceOf(Function);
 });

39-58: Minor: also type the dependency resolution check.

Keeps the test self-documenting and type-safe.

Apply this diff:

-const service = module.get(CANONICAL_INTERNAL_CLIENT_TOKEN);
+const service = module.get<import('@unraid/shared').CanonicalInternalClientService>(
+    CANONICAL_INTERNAL_CLIENT_TOKEN
+);
api/src/unraid-api/cli/developer/developer-tools.service.ts (1)

63-64: After restarting the API, clear the cached internal client to refresh auth/links.

When sandbox toggling triggers a restart, the server’s local session token may rotate. Clearing the cached client ensures the next call rebuilds transport and auth with fresh state (especially important if any consumer uses subscriptions).

Add after Line 80 (right after await this.restartCommand.run();):

// Recreate internal GraphQL client on next use after restart
this.internalClient.clearClient();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7e027 and 1c6fcfb.

📒 Files selected for processing (36)
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json (0 hunks)
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json (0 hunks)
  • api/src/unraid-api/auth/auth.module.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.spec.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.ts (5 hunks)
  • api/src/unraid-api/auth/authentication.guard.ts (2 hunks)
  • api/src/unraid-api/auth/cookie.service.ts (3 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.strategy.ts (1 hunks)
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts (2 hunks)
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (4 hunks)
  • api/src/unraid-api/cli/admin-key.service.ts (0 hunks)
  • api/src/unraid-api/cli/api-report.service.ts (4 hunks)
  • api/src/unraid-api/cli/cli-services.module.ts (1 hunks)
  • api/src/unraid-api/cli/cli.module.spec.ts (3 hunks)
  • api/src/unraid-api/cli/cli.module.ts (0 hunks)
  • api/src/unraid-api/cli/developer/developer-tools.service.ts (2 hunks)
  • api/src/unraid-api/cli/internal-client.service.spec.ts (0 hunks)
  • api/src/unraid-api/cli/internal-client.service.ts (0 hunks)
  • api/src/unraid-api/cli/sso/validate-token.command.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/plugin/global-deps.module.ts (4 hunks)
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (2 hunks)
  • api/src/unraid-api/shared/internal-client.service.ts (2 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts (3 hunks)
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts (1 hunks)
  • packages/unraid-shared/src/index.ts (1 hunks)
  • packages/unraid-shared/src/tokens.ts (1 hunks)
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1 hunks)
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1 hunks)
💤 Files with no reviewable changes (8)
  • api/src/unraid-api/cli/cli.module.ts
  • api/src/unraid-api/cli/internal-client.service.spec.ts
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts
  • api/src/unraid-api/cli/internal-client.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
  • packages/unraid-shared/src/index.ts
  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/auth/auth.module.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
  • packages/unraid-shared/src/index.ts
  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/auth/auth.module.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/cli/cli-services.module.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/auth/auth.module.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

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

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
🧠 Learnings (4)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/auth.module.ts
📚 Learning: 2024-11-15T16:22:03.485Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:176-188
Timestamp: 2024-11-15T16:22:03.485Z
Learning: Atomic writes are not required for the `saveApiKey` method in `ApiKeyService` (`api/src/unraid-api/auth/api-key.service.ts`) unless specifically needed.

Applied to files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
🧬 Code graph analysis (15)
packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (3)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/index.ts (1)
  • CanonicalInternalClientService (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (2)
packages/unraid-api-plugin-connect/src/mothership-proxy/connection.service.ts (1)
  • getApiKey (162-164)
api/src/unraid-api/auth/local-session.service.ts (1)
  • getLocalSession (55-63)
api/src/unraid-api/auth/auth.service.ts (1)
api/src/utils.ts (1)
  • handleAuthError (213-245)
api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
packages/unraid-shared/src/index.ts (1)
  • CanonicalInternalClientService (7-7)
api/src/unraid-api/auth/local-session.strategy.ts (3)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (15-289)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (10-98)
api/src/unraid-api/types/fastify.ts (1)
  • FastifyRequest (25-30)
api/src/unraid-api/cli/sso/validate-token.command.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (2)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/cli/api-report.service.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/auth/local-session.service.ts (4)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (15-289)
api/src/unraid-api/auth/local-session.strategy.ts (1)
  • Injectable (14-46)
api/src/unraid-api/shared/internal-client.service.ts (1)
  • Injectable (15-87)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (26-232)
api/src/unraid-api/cli/__test__/api-report.service.test.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/plugin/global-deps.module.ts (2)
api/src/unraid-api/auth/cookie.service.ts (1)
  • SESSION_COOKIE_CONFIG (10-10)
packages/unraid-shared/src/tokens.ts (4)
  • COOKIE_SERVICE_TOKEN (6-6)
  • NGINX_SERVICE_TOKEN (4-4)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
  • INTERNAL_CLIENT_SERVICE_TOKEN (5-5)
api/src/unraid-api/cli/developer/developer-tools.service.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/cli/cli.module.spec.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/shared/internal-client.service.ts (7)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (10-98)
api/src/unraid-api/cli/api-report.service.ts (1)
  • Injectable (62-201)
api/src/unraid-api/cli/developer/developer-tools.service.ts (1)
  • Injectable (13-235)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (26-232)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
packages/unraid-shared/src/tokens.ts (1)
  • INTERNAL_CLIENT_SERVICE_TOKEN (5-5)
packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)
  • InternalGraphQLClientFactory (7-15)
🔇 Additional comments (37)
packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts (1)

15-27: ConnectApiKeyService Removed; Verify InternalClientService References

  • ConnectApiKeyService

    • No occurrences found in the repo—removal confirmed.
  • InternalClientService

    • Defined in api/src/unraid-api/shared/internal-client.service.ts
    • Imported and provided in api/src/unraid-api/plugin/global-deps.module.ts (lines 24, 64–68)
    • Token constant in packages/unraid-shared/src/tokens.ts (line 5)
      These hits correspond to the new canonical internal‐client implementation.
  • Exports

    • No modules are still exporting either service (verified).

Action: Please confirm that each InternalClientService reference is indeed the new canonical implementation (and not a leftover import of the old service). Remove any legacy imports or providers if they remain.

packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts (2)

14-31: Removal of ConnectApiKeyService from module metadata looks correct

This aligns with the canonical internal client flow; providers/exports are consistent and imports already use .js specifiers.


14-31: Verified: No remaining dependencies on ConnectApiKeyService

I ran a full-text search for ConnectApiKeyService across the repository and found zero occurrences outside of node_modules. This confirms that no providers or resolvers are still injecting it, so you shouldn’t encounter any DI runtime errors related to it.

packages/unraid-shared/src/tokens.ts (1)

6-7: New DI tokens are reasonable and follow existing naming

COOKIE_SERVICE_TOKEN and CANONICAL_INTERNAL_CLIENT_TOKEN fit the existing token pattern. No conflicts spotted.

api/src/unraid-api/auth/cookie.service.ts (2)

78-80: Centralized validity check is good; keep the predicate tight

Reusing isSessionValid in both paths reduces drift. The predicate is minimal; if abuse is a concern, consider verifying expected key/value patterns rather than just substrings. No action required now.


30-37: Good: sameSite is set to 'lax' per prior guidance

Matches the documented preference for session cookies in this codebase.

packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)

9-14: Auth Precedence Is Explicit—Please Document and Confirm Against PR Goals

I verified that the factory’s authLink (and WebSocket connectionParams) enforces a strict if/else order:

  1. getApiKey → API key headers
  2. getLocalSessionx-local-session header
  3. getCookieAuthCookie: sessionId=…; csrfToken=…

To align with the PR’s intent of “prioritize ephemeral keys, then others,” please:

  • Update the JSDoc above createClient in internal-graphql-client.factory.ts to explicitly list this precedence.
  • Confirm that the order of local-session vs. cookie-based auth matches the PR objectives (i.e. whether cookie auth should come before or after local session).
  • (Optional) Add a unit test where both getLocalSession and getCookieAuth are provided to assert the intended branch is taken.
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)

1-1: LGTM on ESM import specifier.

Type-only import uses .js extension and aligns with our ESM guideline for TS. No concerns.

packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (1)

129-136: Review comment incorrect: WAN_ACCESS_TYPE.DYNAMIC is defined and the SSL check is reachable.

I’ve confirmed that in connect.model.ts, the WAN_ACCESS_TYPE enum includes DYNAMIC (alongside ALWAYS and DISABLED), so there’s no compile-time error and the branch in syncSettings can be hit by clients specifying dynamic access. The original concern about a missing enum member isn’t valid.

Likely an incorrect or invalid review comment.

packages/unraid-shared/src/index.ts (1)

7-7: LGTM: type-only export with .js path keeps bundle clean.

The new public type export follows our ESM path convention and won’t affect runtime.

api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (1)

51-53: LGTM: AuthService now accepts LocalSessionService (constructor updated).

Constructor wiring matches the new dependency order and keeps the test isolated by stubbing validateLocalSession.

api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts (1)

46-48: LGTM: constructor update for LocalSessionService in tests.

The added stub for LocalSessionService aligns with the new AuthService signature and keeps the resolver tests focused.

api/src/unraid-api/auth/authentication.guard.ts (2)

16-16: Importing LocalSessionStrategy looks correct and ESM-safe

Using the .js extension aligns with our ESM guideline. No issues here.


41-41: Confirm intended auth precedence with LocalSession before UserCookie

Strategy order dictates precedence. Placing LocalSessionStrategy between ServerHeader and UserCookie changes behavior for requests that could satisfy both local-session and cookie auth. Confirm this is intentional across GraphQL, REST, and WS upgrades.

api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (1)

3-3: Token import LGTM

Using the shared token keeps the test decoupled from concrete client implementations.

api/src/unraid-api/cli/__test__/api-report.service.test.ts (1)

3-4: Canonical client token/type import LGTM

The test now depends on the stable shared token and interface, aligning with production DI.

api/src/unraid-api/auth/auth.service.spec.ts (1)

11-11: AuthService constructor update wired correctly

LocalSessionService is mocked and injected in the new constructor position. Looks good.

Also applies to: 21-21, 69-73

api/src/unraid-api/cli/api-report.service.ts (3)

1-4: DI migration to canonical client LGTM

Injecting via the shared token/interface decouples the service from concrete client implementations and improves testability.

Also applies to: 65-67


141-141: Explicitly disabling subscriptions is appropriate

The CLI report workflow doesn’t need WS; passing { enableSubscriptions: false } is correct.


196-197: Safer access to system data LGTM

Switch to systemResult?.data avoids NPEs when the first query fails. Good defensive change.

api/src/unraid-api/auth/auth.module.ts (3)

74-78: Exporting LocalSession providers enables reuse—LGTM

Exporting LocalSessionStrategy and LocalSessionService from AuthModule enables clean DI in downstream modules. Looks good.


65-67: No changes needed: defaultOpts() already enforces SameSite: 'lax'

The CookieService.defaultOpts() implementation in api/src/unraid-api/auth/cookie.service.ts explicitly sets

sameSite: 'lax',

on line 36, ensuring that all session cookies use SameSite: 'lax' as per our prior decision.


60-64: AuthModule imports and provider singleton verified

AuthModule is only imported by:

  • app/src/unraid-api/app/app.module.ts (line 13)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (line 3)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.module.ts (line 4)

AuthModule does not import any of these modules in turn, so there are no circular dependencies. Moreover, NestJS modules are singleton-scoped by design—importing the same module into multiple parent modules does not re-create its providers. Therefore, LocalSessionService will only be instantiated once and its onModuleInit hook will only run a single time for token generation.

No further changes are needed here. If you’d like to simplify consumer imports even more, you can annotate AuthModule with @Global() and add LocalSessionService to its exports array—this will make the service available app-wide without explicit imports.

api/src/unraid-api/cli/cli-services.module.ts (1)

24-25: No lingering AdminKeyService/CliInternalClientService references found

Ran the provided ripgrep search for any imports or usages of AdminKeyService or CliInternalClientService. The only match is a comment in the test file packages/unraid-shared/src/services/internal-graphql-client-usage.spec.ts reference (“Service that uses the factory pattern (like CliInternalClientService)”), not an actual import or code usage. No further action is required to update consumers in cli-services.module.ts.

api/src/unraid-api/auth/local-session.strategy.ts (1)

15-17: Strategy key and DI are clean

Passport key 'local-session' with a static key property is consistent with usage elsewhere. Constructor DI into AuthService is straightforward.

api/src/unraid-api/cli/sso/validate-token.command.ts (1)

19-21: Token-based DI for the canonical internal client—good move

Switching to @Inject(CANONICAL_INTERNAL_CLIENT_TOKEN) decouples the command from a concrete class and aligns with the shared interface.

packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (1)

1-6: ESM import hygiene—LGTM

Using .js extensions for local ESM imports and type-only import for CanonicalInternalClientService is consistent with repo guidelines.

api/src/unraid-api/auth/auth.service.ts (1)

88-110: LGTM! Well-structured local session validation method.

The implementation follows the established pattern for authentication methods in this service, with proper error handling, role synchronization, and logging.

api/src/unraid-api/plugin/global-deps.module.ts (2)

51-53: Good implementation of session cookie configuration.

Using CookieService.defaultOpts() ensures consistent cookie configuration across the application, which aligns with the previously learned best practice of using sameSite: 'lax' for session cookies.


63-68: LGTM! Clean canonical client service aliasing.

The aliasing pattern using useExisting properly establishes InternalClientService as the canonical implementation without creating duplicate instances.

api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)

52-56: Good validation of authentication options.

The validation ensures at least one authentication method is provided, preventing misconfiguration.

api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (1)

4-6: LGTM! Clean migration to canonical internal client token.

The test correctly migrates from the legacy CliInternalClientService to the new CanonicalInternalClientService using the proper token pattern. This aligns well with the broader refactoring to standardize internal client usage across the codebase.

Also applies to: 19-19, 46-47, 57-57

api/src/unraid-api/shared/internal-client.service.ts (2)

21-25: Good DI: factory via token + LocalSessionService injection aligns with the canonical client design.

This simplifies consumers and centralizes auth via local session. Nice.


81-86: No action needed: ApolloClient.stop() is supported in v3.13.9

We’ve verified that all workspaces pin @apollo/client at version 3.13.9. According to the Apollo Client v3 API docs, ApolloClient exposes a stop(): void method to terminate any active client processes, making it safe to dispose of the instance. Therefore, the existing this.client?.stop() call is valid and no further changes are required. (apollographql.com)

api/src/unraid-api/cli/cli.module.spec.ts (1)

4-4: LGTM: tokens imported from shared; aligns with DI-by-token approach.

The switch to CANONICAL_INTERNAL_CLIENT_TOKEN and INTERNAL_CLIENT_SERVICE_TOKEN looks correct.

api/src/unraid-api/cli/developer/developer-tools.service.ts (2)

57-59: LGTM: Use canonical client via token.

Token-based DI decouples the implementation and matches the new architecture.


63-64: Note on getClient({ enableSubscriptions: false }): relies on options-aware caching.

If InternalClientService caches a single instance unconditionally, the first caller’s options win. Ensure the options-aware caching refactor in InternalClientService is adopted so this call won’t prevent other consumers from using subscriptions later.

If not adopting the refactor, consider always using subscriptions in the canonical client and letting HTTP-only consumers ignore the WS capability.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
api/src/unraid-api/cli/api-report.service.ts (1)

146-153: Fix unhandled promise rejections caused by Promise.race timeouts

The Promise.race([client.query(...), timeoutReject]) pattern schedules a rejecting promise that is never cleared if the query resolves first. When the timer fires later, it can trigger an unhandled rejection. Use a helper that clears the timeout in a finally block.

Apply these diffs to replace the three timeout usages:

-        let systemResult: { data: SystemReportQuery } | null = null;
-        try {
-            systemResult = await Promise.race([
-                client.query({
-                    query: SYSTEM_REPORT_QUERY,
-                }),
-                new Promise<never>((_, reject) =>
-                    setTimeout(() => reject(new Error('Query timeout after 10 seconds')), 10000)
-                ),
-            ]);
-        } catch (error) {
+        let systemResult: { data: SystemReportQuery } | null = null;
+        try {
+            systemResult = await withTimeout(
+                client.query({ query: SYSTEM_REPORT_QUERY }),
+                10_000,
+                'Query timeout after 10 seconds'
+            );
+        } catch (error) {
             this.logger.error('Error querying system data: ' + error);
             return this.createApiReportData({
                 apiRunning,
                 errorReason: 'System query failed',
             });
         }
-        try {
-            const connectResult = await Promise.race([
-                client.query({
-                    query: CONNECT_STATUS_QUERY,
-                }),
-                new Promise<never>((_, reject) =>
-                    setTimeout(() => reject(new Error('Connect query timeout after 10 seconds')), 10000)
-                ),
-            ]);
-            connectData = connectResult.data.connect;
-        } catch (error) {
+        try {
+            const connectResult = await withTimeout(
+                client.query({ query: CONNECT_STATUS_QUERY }),
+                10_000,
+                'Connect query timeout after 10 seconds'
+            );
+            connectData = connectResult.data.connect;
+        } catch (error) {
             this.logger.debug('Connect plugin not available: ' + error);
         }
-        try {
-            const servicesResult = await Promise.race([
-                client.query({
-                    query: SERVICES_QUERY,
-                }),
-                new Promise<never>((_, reject) =>
-                    setTimeout(() => reject(new Error('Services query timeout after 10 seconds')), 10000)
-                ),
-            ]);
-            servicesData = servicesResult.data.services || [];
-        } catch (error) {
+        try {
+            const servicesResult = await withTimeout(
+                client.query({ query: SERVICES_QUERY }),
+                10_000,
+                'Services query timeout after 10 seconds'
+            );
+            servicesData = servicesResult.data.services || [];
+        } catch (error) {
             this.logger.debug('Error querying services: ' + error);
         }

Add this helper inside the class (e.g., above generateReport):

private async withTimeout<T>(p: Promise<T>, ms: number, message: string): Promise<T> {
    let id: ReturnType<typeof setTimeout> | undefined;
    const timeout = new Promise<never>((_, reject) => {
        id = setTimeout(() => reject(new Error(message)), ms);
    });
    try {
        return await Promise.race([p, timeout]);
    } finally {
        if (id) clearTimeout(id);
    }
}

Or, if you prefer a static helper:

const withTimeout = async <T>(p: Promise<T>, ms: number, message: string) => { /* same body */ };

Then call await this.withTimeout(...) in the diffs above.

Also applies to: 165-172, 181-188

🧹 Nitpick comments (44)
packages/unraid-shared/src/types/internal-graphql-client.factory.ts (2)

9-11: Type-level guarantee: require at least one auth provider in createClient options.

Runtime guards exist, but the type allows calling without any of getApiKey/getCookieAuth/getLocalSession. Tighten the type to prevent accidental misuse.

Apply:

 export interface InternalGraphQLClientFactory {
-    createClient(options: {
-        getApiKey?: () => Promise<string>;
-        getCookieAuth?: () => Promise<{ sessionId: string; csrfToken: string } | null>;
-        getLocalSession?: () => Promise<string | null>;
-        enableSubscriptions?: boolean;
-        origin?: string;
-    }): Promise<ApolloClient<NormalizedCacheObject>>;
+    /**
+     * At least one of getApiKey, getCookieAuth, or getLocalSession must be provided.
+     */
+    createClient(options: RequireAtLeastOne<{
+        getApiKey?: () => Promise<string>;
+        getCookieAuth?: () => Promise<{ sessionId: string; csrfToken: string } | null>;
+        getLocalSession?: () => Promise<string | null>;
+        enableSubscriptions?: boolean;
+        origin?: string;
+    }, 'getApiKey' | 'getCookieAuth' | 'getLocalSession'>): Promise<ApolloClient<NormalizedCacheObject>>;
 }
+
+// Utility: require at least one of the specified keys
+type RequireAtLeastOne<T, K extends keyof T = keyof T> =
+  Pick<T, Exclude<keyof T, K>> &
+  { [P in K]-?: Required<Pick<T, P>> & Partial<Pick<T, Exclude<K, P>>> }[K];

7-15: Clarify precedence semantics in docs.

If multiple providers are supplied, document which one wins (e.g., API key > local session > cookie), matching the implementation, to prevent ambiguity for integrators.

api/src/unraid-api/auth/cookie.service.ts (2)

78-80: Session validity heuristic could allow expired sessions.

The content check ensures required keys but does not account for expiration/TTL. Consider validating freshness via file mtime against configured max lifetime, if available.

Example (outline):

  • Read file mtime with fs.stat and compare to a TTL (e.g., from config).
  • Keep content checks as-is to avoid parsing cost.

46-51: Minor: hasValidAuthCookie can short-circuit.

Optional micro-optimization: early return true on the first valid cookie instead of processing all cookies.

packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)

5-7: Docstring: broaden beyond cookie auth.

The canonical client can authenticate via local session (and potentially API key via factory). Update wording to avoid implying cookie-only.

Apply:

-     * Get GraphQL client with cookie authentication.
-     * This is the canonical internal client for the application.
+     * Get GraphQL client for internal use.
+     * Authentication strategy (e.g., local session, cookie) is chosen by the underlying factory.
packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (4)

171-176: Storing API key in config: re-evaluate persistence model.

With the broader shift to ephemeral/persistent key separation and local-session auth, confirm that persisting connect.config.apikey is still required and acceptable from a security standpoint (plaintext at rest).

Options:

  • Store an ephemeral token instead (rotated by AdminKeyService) and avoid persisting long-lived keys.
  • If persistence is required, consider encrypting at rest or scoping to minimal privileges.

14-14: Unused import: decodeJwt.

Appears unused; safe to remove.

-import { decodeJwt } from 'jose';

285-289: Port validation constraints are inconsistent across schemas.

In the conditional 'then' schema, port minimum is 1..65535; in the base definition it's 0..65535. Recommend removing min/max from the base schema or aligning both to 1..65535 to avoid conflicting validation paths.

-            port: {
-                // 'port' is null when remote access is disabled.
-                type: ['number', 'null'],
-                title: 'WAN Port',
-                minimum: 0,
-                maximum: 65535,
-            },
+            port: {
+                // 'port' is null when remote access is disabled.
+                type: ['number', 'null'],
+                title: 'WAN Port'
+            },

Also applies to: 426-428


96-99: isSignedIn relies solely on apikey presence.

Given the move toward local-session auth, consider augmenting this check (e.g., via CookieService.getActiveSession or a LocalSessionService) so UI/UX gates reflect actual auth state.

Would you like me to propose a small integration using CANONICAL_INTERNAL_CLIENT_TOKEN to determine signed-in state?

Also applies to: 309-315

api/src/unraid-api/auth/local-session.service.ts (4)

81-90: Prefer crypto.timingSafeEqual(Buffer, Buffer) for robust constant-time comparison

Your XOR aggregation is good, but Node’s built-in timingSafeEqual over Buffers is more robust and well-tested. Convert strings to Buffers and use timingSafeEqual.

Apply this diff:

@@
-import { randomBytes } from 'crypto';
+import { randomBytes, timingSafeEqual } from 'crypto';
@@
-    private constantTimeEqual(a: string, b: string): boolean {
-        if (a.length !== b.length) return false;
-
-        let result = 0;
-        for (let i = 0; i < a.length; i++) {
-            result |= a.charCodeAt(i) ^ b.charCodeAt(i);
-        }
-
-        return result === 0;
-    }
+    private constantTimeEqual(a: string, b: string): boolean {
+        const aBuf = Buffer.from(a);
+        const bBuf = Buffer.from(b);
+        if (aBuf.length !== bBuf.length) return false;
+        try {
+            return timingSafeEqual(aBuf, bBuf);
+        } catch {
+            return false;
+        }
+    }

Also applies to: 2-2


33-35: Harden directory permissions when creating the session directory

Set the directory mode to 0700 to minimize exposure (owner-only). While umask may still affect final permissions, this sets a strong baseline.

-            await mkdir(dirname(LocalSessionService.SESSION_FILE_PATH), { recursive: true });
+            await mkdir(dirname(LocalSessionService.SESSION_FILE_PATH), { recursive: true, mode: 0o700 });

20-22: Improve error logging: pass stack traces to Logger.error and include messages in debug logs

Nest’s Logger.error accepts a message and an optional stack trace. Passing the raw error as the second arg can miss stack info. Also include the error message when downgrading to debug.

-            this.logger.error('Failed to initialize local session:', error);
+            this.logger.error('Failed to initialize local session', (error as Error)?.stack);
@@
-            this.logger.error(`Failed to write local session: ${error}`);
+            this.logger.error('Failed to write local session', (error as Error)?.stack);
@@
-            this.logger.debug('Local session file not found or not readable');
+            this.logger.debug(`Local session file not found or not readable: ${(error as Error)?.message}`);

Also applies to: 46-49, 60-61


25-31: Confirm startup failure policy for token generation

If generateLocalSession() throws during startup, the service logs the error but continues. Given other components depend on a valid local session (e.g., canonical internal client), do we want to fail fast instead and prevent the app from starting in a half-broken state?

If you want to enforce fail-fast, change the onModuleInit catch to rethrow after logging. I can provide a small patch if that’s the desired behavior.

api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (2)

66-68: Stub the full CanonicalInternalClientService shape to prevent future breakage

The stub only provides getClient. The interface also exposes clearClient(). Adding it avoids surprises if tests or modules start calling it.

-            .overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN)
-            .useValue({ getClient: vi.fn() })
+            .overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN)
+            .useValue({ getClient: vi.fn(), clearClient: vi.fn() })

78-80: Avoid asserting on private fields; prefer behavior or container resolution

Accessing restService['apiReportService'] ties the test to implementation details. Since you already resolve ApiReportService from the container, you can assert module compilation and service resolution without peeking into private props.

Possible alternative:

  • Keep the existing module.get<ApiReportService>(ApiReportService) as the assertion (it already ensures DI is wired).
  • Or assert a public method that uses ApiReportService if appropriate.
api/src/unraid-api/auth/authentication.guard.ts (2)

58-61: Sanitize connectionParams in logs to avoid leaking secrets

Logging raw connectionParams may include tokens or API keys. Mask sensitive keys before logging.

-                apiLogger.error(
-                    'No user found in request - connection params: %o',
-                    fullContext.connectionParams ?? {}
-                );
+                const cps = { ...(fullContext.connectionParams ?? {}) };
+                for (const k of Object.keys(cps)) {
+                    if (/(token|auth|key|secret)/i.test(k)) cps[k] = '[REDACTED]';
+                }
+                apiLogger.error('No user found in request - connection params: %o', cps);

97-105: Guard cookie parsing on WS handshake to avoid mis-parsing/TypeErrors

If the Cookie header is absent, cookie.parse may receive an unexpected string or undefined. Add a headerIndex check and default to an empty cookie map.

-            if (fullContext.connectionParams) {
-                const rawHeaders = fullContext.req.extra.request.rawHeaders;
-                const headerIndex = rawHeaders.findIndex(
-                    (headerOrValue) => headerOrValue.toLowerCase() === 'cookie'
-                );
-                const cookieString = rawHeaders[headerIndex + 1];
-                request.cookies = parseCookies(cookieString);
-            }
+            if (fullContext.connectionParams) {
+                const rawHeaders = fullContext.req.extra.request.rawHeaders ?? [];
+                const headerIndex = rawHeaders.findIndex(
+                    (h) => typeof h === 'string' && h.toLowerCase() === 'cookie'
+                );
+                if (headerIndex >= 0 && typeof rawHeaders[headerIndex + 1] === 'string') {
+                    request.cookies = parseCookies(rawHeaders[headerIndex + 1] as string);
+                } else {
+                    request.cookies = {};
+                }
+            }
api/src/unraid-api/auth/auth.module.ts (1)

21-25: Validate PassportModule.register defaultStrategy usage with string[]

NestJS Passport commonly treats defaultStrategy as a string. Using a string[] may compile depending on local typings, but it’s non-standard. Since AuthenticationGuard explicitly enumerates strategies, consider keeping defaultStrategy to a single value (or omit it) to avoid confusion.

Option A: keep a single default

-            defaultStrategy: [
-                ServerHeaderStrategy.key,
-                LocalSessionStrategy.key,
-                UserCookieStrategy.key,
-            ],
+            defaultStrategy: ServerHeaderStrategy.key,

Option B: remove and rely on AuthenticationGuard’s multi-strategy

-        PassportModule.register({
-            defaultStrategy: [
-                ServerHeaderStrategy.key,
-                LocalSessionStrategy.key,
-                UserCookieStrategy.key,
-            ],
-        }),
+        PassportModule.register({}),

Would you like me to open a follow-up to align this across modules?

api/src/unraid-api/auth/auth.service.spec.ts (2)

69-73: Type the localSessionService mock precisely (avoid any)

Narrowing the mock type improves safety and editor help.

-        localSessionService = {
-            validateLocalSession: vi.fn(),
-        } as any;
-        authService = new AuthService(cookieService, apiKeyService, localSessionService, authzService);
+        const localSessionMock = {
+            validateLocalSession: vi.fn(),
+        } as unknown as LocalSessionService;
+        localSessionService = localSessionMock;
+        authService = new AuthService(cookieService, apiKeyService, localSessionMock, authzService);

101-104: Align error assertions with testing guidelines (.rejects.toThrow() without args)

Our testing guideline prefers avoiding exact messages and types for thrown errors unless they are the subject under test. Replace specific message/type assertions with a plain .rejects.toThrow().

-            await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow(
-                UnauthorizedException
-            );
+            await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow();
-            await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow(
-                UnauthorizedException
-            );
+            await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow();
-            await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow(
-                new UnauthorizedException('Invalid CSRF token')
-            );
+            await expect(authService.validateCookiesWithCsrfToken(mockRequest)).rejects.toThrow();
-            await expect(authService.syncApiKeyRoles('test-id', ['new-role'])).rejects.toThrow(
-                'Failed to delete role'
-            );
+            await expect(authService.syncApiKeyRoles('test-id', ['new-role'])).rejects.toThrow();
-            await expect(authService.syncApiKeyRoles('test-id', ['new-role'])).rejects.toThrow(
-                'Failed to add role'
-            );
+            await expect(authService.syncApiKeyRoles('test-id', ['new-role'])).rejects.toThrow();
-            await expect(authService.addRoleToApiKey('invalid-id', Role.GUEST)).rejects.toThrow(
-                UnauthorizedException
-            );
+            await expect(authService.addRoleToApiKey('invalid-id', Role.GUEST)).rejects.toThrow();
-            await expect(authService.removeRoleFromApiKey('invalid-id', Role.GUEST)).rejects.toThrow(
-                UnauthorizedException
-            );
+            await expect(authService.removeRoleFromApiKey('invalid-id', Role.GUEST)).rejects.toThrow();

Also applies to: 112-115, 137-140, 176-179, 186-189, 225-228, 259-262

api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (2)

88-90: Relax brittle error message assertions

Per guidelines, assert that an error was thrown without relying on exact messages.

-            await expect(resolver.create(input)).rejects.toThrow('Create failed');
+            await expect(resolver.create(input)).rejects.toThrow();
-            await expect(resolver.create(input)).rejects.toThrow('Sync failed');
+            await expect(resolver.create(input)).rejects.toThrow();
-            await expect(resolver.addRole(input)).rejects.toThrow('API key not found');
+            await expect(resolver.addRole(input)).rejects.toThrow();
-            await expect(resolver.removeRole(input)).rejects.toThrow('API key not found');
+            await expect(resolver.removeRole(input)).rejects.toThrow();

Also applies to: 101-102, 142-143, 163-164


78-79: Nit: assert with values from creation result to reduce coupling

You’re asserting sync with mockApiKey values, but the source of truth is the created key. Using the returned id/roles from create reduces fixture coupling.

-            expect(authService.syncApiKeyRoles).toHaveBeenCalledWith(mockApiKey.id, mockApiKey.roles);
+            expect(authService.syncApiKeyRoles).toHaveBeenCalledWith(
+                mockApiKeyWithSecret.id,
+                input.roles
+            );
api/src/unraid-api/cli/__test__/api-report.service.test.ts (1)

44-45: Pin the client-creation contract in the test

ApiReportService now calls getClient({ enableSubscriptions: false }). As a small safeguard, assert the call arguments so future regressions don’t silently re-enable subscriptions in this path.

Apply this addition near the other expectations:

             const result = await apiReportService.generateReport(true);

             // Verify GraphQL client was called for all queries
-            expect(mockInternalClientService.getClient).toHaveBeenCalled();
+            expect(mockInternalClientService.getClient).toHaveBeenCalledWith({ enableSubscriptions: false });
             expect(mockClient.query).toHaveBeenCalledWith({
                 query: SYSTEM_REPORT_QUERY,
             });

Also applies to: 131-138

api/src/unraid-api/cli/sso/validate-token.command.ts (1)

25-33: Mark createErrorAndExit as non-returning for stronger type safety

This function always exits the process; give it a never return type to help TS flow analysis in callers.

-    private createErrorAndExit = (errorMessage: string) => {
+    private createErrorAndExit = (errorMessage: string): never => {
         this.logger.always(
             JSON.stringify({
                 error: errorMessage,
                 valid: false,
             })
         );
         process.exit(1);
     };
api/src/unraid-api/cli/api-report.service.ts (1)

98-111: Defensive null-check for connect.dynamicRemoteAccess

If connect is present but dynamicRemoteAccess is null/undefined, this block will throw. Guard the nested fields.

Example refactor:

connect: connectData
    ? {
          installed: true,
          dynamicRemoteAccess: connectData.dynamicRemoteAccess
              ? {
                    enabledType: connectData.dynamicRemoteAccess.enabledType,
                    runningType: connectData.dynamicRemoteAccess.runningType,
                    error: connectData.dynamicRemoteAccess.error ?? null,
                }
              : undefined,
      }
    : {
          installed: false,
          reason: errorReason || 'Connect plugin not installed or not available',
      },
api/src/unraid-api/cli/cli.module.spec.ts (1)

27-31: Type the token-based retrieval for stronger safety (and assert full surface).

Use generics with the DI token so TS can catch mismatches at compile time, and assert clearClient is present just like in the dependency block.

@@
-import { CANONICAL_INTERNAL_CLIENT_TOKEN, INTERNAL_CLIENT_SERVICE_TOKEN } from '@unraid/shared';
+import { CANONICAL_INTERNAL_CLIENT_TOKEN, INTERNAL_CLIENT_SERVICE_TOKEN } from '@unraid/shared';
+import type { CanonicalInternalClientService } from '@unraid/shared';
@@
-    it('should provide CanonicalInternalClient', () => {
-        const service = module.get(CANONICAL_INTERNAL_CLIENT_TOKEN);
+    it('should provide CanonicalInternalClient', () => {
+        const service = module.get<CanonicalInternalClientService>(CANONICAL_INTERNAL_CLIENT_TOKEN);
         expect(service).toBeDefined();
         expect(service.getClient).toBeInstanceOf(Function);
+        expect(service.clearClient).toBeInstanceOf(Function);
     });
@@
-            const service = module.get(CANONICAL_INTERNAL_CLIENT_TOKEN);
+            const service = module.get<CanonicalInternalClientService>(CANONICAL_INTERNAL_CLIENT_TOKEN);
             expect(service).toBeDefined();

Also applies to: 39-45, 4-4

api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (4)

82-84: Avoid brittle log-message assertions; match intent, not exact text.

Use stringContaining so minor copy changes won’t break the test.

-            expect(logService.info).toHaveBeenCalledWith('Enabling sandbox mode - restarting API...');
+            expect(logService.info).toHaveBeenCalledWith(expect.stringContaining('Enabling sandbox mode'));

97-99: Same here: make the "disable" assertion resilient.

-            expect(logService.info).toHaveBeenCalledWith('Sandbox mode disabled successfully.');
+            expect(logService.info).toHaveBeenCalledWith(expect.stringContaining('Sandbox mode disabled'));

70-71: Verify the internal client is requested with the expected options.

This ensures the service honors the new getClient({ enableSubscriptions: false }) contract.

             await service.setSandboxMode(true);
 
+            expect(internalClient.getClient).toHaveBeenCalledWith({ enableSubscriptions: false });
             expect(mockClient.mutate).toHaveBeenCalledWith({
@@
             await service.setSandboxMode(false);
 
+            expect(internalClient.getClient).toHaveBeenCalledWith({ enableSubscriptions: false });
             expect(logService.info).toHaveBeenCalledWith('Sandbox mode disabled successfully.');

Also applies to: 95-96


121-127: Align error assertions with testing guidelines.

Per repo guidelines, avoid checking exact error messages; use .rejects.toThrow() without arguments.

-            await expect(service.enableModalTest()).rejects.toThrow(
-                'Directory does not exist: /usr/local/emhttp/plugins/dynamix.my.servers'
-            );
+            await expect(service.enableModalTest()).rejects.toThrow();
api/src/unraid-api/auth/local-session.strategy.ts (2)

23-31: Handle possible string[] header shape for robustness.

Fastify/Node headers can be string|string[]; normalize before use.

-    async validate(request: FastifyRequest): Promise<UserAccount | null> {
+    async validate(request: FastifyRequest): Promise<UserAccount | null> {
         try {
-            const localSessionToken = request.headers['x-local-session'] as string;
+            const raw = request.headers?.['x-local-session'] as unknown;
+            const localSessionToken = Array.isArray(raw) ? raw[0] : (raw as string | undefined);
 
             if (!localSessionToken) {
                 this.logger.verbose('No local session token found in request headers');
                 return null;
             }

41-44: Log errors at error level and include stack, not as verbose.

Keeps severity accurate and improves debuggability.

-        } catch (error) {
-            this.logger.verbose(error, `Local session validation failed`);
-            return null;
-        }
+        } catch (error) {
+            this.logger.error('Local session validation failed', (error as Error)?.stack);
+            return null;
+        }
api/src/unraid-api/auth/auth.service.ts (2)

88-110: Local-session validation path looks correct; consider minor hardening.

Logic mirrors cookie flow and correctly syncs roles. Consider extracting the shared “sync roles for user and log” into a small helper to DRY with validateCookiesWithCsrfToken.

Example helper (for reference only):

private async finalizeUser(user: UserAccount, contextLabel: string): Promise<UserAccount> {
  await this.syncUserRoles(user.id, user.roles);
  const roles = await this.authzService.getRolesForUser(user.id);
  this.logger.debug(`${contextLabel} user ${user.id} has roles: ${roles}`);
  return user;
}

And then:

const user = await this.getLocalSessionUser();
return this.finalizeUser(user, 'Local session');

262-288: Two factory methods share nearly identical structure.

getSessionUser and getLocalSessionUser differ only in IDs and labels. Consider a small factory function to reduce repetition.

api/src/unraid-api/cli/developer/developer-tools.service.ts (1)

17-18: Use consistent logging facilities for SSOFileModification.

You’re constructing a new Nest Logger just to pass to SSOFileModification while the service otherwise uses LogService. If feasible, accept LogService in SSOFileModification or wrap it to keep formatting/transport consistent.

api/src/unraid-api/shared/internal-graphql-client.factory.ts (5)

70-71: Logger formatting uses printf-style placeholders; Nest Logger doesn’t format %s

Use template literals (or pass a context string as the 2nd param) so the actual URI/path is logged.

-            this.logger.debug('WebSocket subscriptions enabled: %s', wsUri);
+            this.logger.debug(`WebSocket subscriptions enabled: ${wsUri}`);
...
-            this.logger.debug('Creating GraphQL client using Unix socket: %s', socketPath);
+            this.logger.debug(`Creating GraphQL client using Unix socket: ${socketPath}`);
...
-            this.logger.debug('Creating GraphQL client using HTTP: %s', httpUri);
+            this.logger.debug(`Creating GraphQL client using HTTP: ${httpUri}`);

Also applies to: 75-76, 101-102


52-56: Validation is good; minor copy edit and consider exposing which option was missing for quicker diagnosis

Error message is clear; optionally include which options were provided to aid debugging (kept as a nit).


113-152: Loosen hard-fail behavior and allow fallback across multiple provided auth providers

Right now, if getApiKey exists but returns an empty string or throws, the code won’t try getLocalSession/getCookieAuth. Same for local session before cookie. Suggest trying providers in priority order and only throwing if all fail. Also guard against empty strings.

-        const authLink = setContext(async (_, { headers }) => {
-            if (getApiKey) {
-                // Use API key authentication
-                const apiKey = await getApiKey();
-                return {
-                    headers: {
-                        ...headers,
-                        'x-api-key': apiKey,
-                    },
-                };
-            } else if (getLocalSession) {
-                // Use local session authentication
-                const localSession = await getLocalSession();
-                if (!localSession) {
-                    throw new Error('No valid local session found');
-                }
-                return {
-                    headers: {
-                        ...headers,
-                        'x-local-session': localSession,
-                    },
-                };
-            } else if (getCookieAuth) {
-                // Use cookie-based authentication
-                const cookieAuth = await getCookieAuth();
-                if (!cookieAuth) {
-                    throw new Error('No valid session found for cookie authentication');
-                }
-                return {
-                    headers: {
-                        ...headers,
-                        'x-csrf-token': cookieAuth.csrfToken,
-                        cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
-                    },
-                };
-            }
-
-            return { headers };
-        });
+        const authLink = setContext(async (_, { headers }) => {
+            let authHeaders: Record<string, string> | null = null;
+
+            // 1) API key
+            if (getApiKey) {
+                try {
+                    const apiKey = await getApiKey();
+                    if (apiKey) authHeaders = { 'x-api-key': apiKey };
+                } catch (e) {
+                    this.logger.warn('[GRAPHQL-CLIENT] getApiKey failed: %o', e);
+                }
+            }
+
+            // 2) Local session
+            if (!authHeaders && getLocalSession) {
+                try {
+                    const localSession = await getLocalSession();
+                    if (localSession) authHeaders = { 'x-local-session': localSession };
+                } catch (e) {
+                    this.logger.warn('[GRAPHQL-CLIENT] getLocalSession failed: %o', e);
+                }
+            }
+
+            // 3) Cookie auth
+            if (!authHeaders && getCookieAuth) {
+                try {
+                    const cookieAuth = await getCookieAuth();
+                    if (cookieAuth?.sessionId && cookieAuth?.csrfToken) {
+                        authHeaders = {
+                            'x-csrf-token': cookieAuth.csrfToken,
+                            // TODO: derive cookie name from config (see follow-up comment)
+                            cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
+                        };
+                    }
+                } catch (e) {
+                    this.logger.warn('[GRAPHQL-CLIENT] getCookieAuth failed: %o', e);
+                }
+            }
+
+            return { headers: { ...headers, ...(authHeaders ?? {}) } };
+        });

154-158: Also log GraphQL errors, not just network errors

Capturing graphQLErrors helps troubleshoot server-side exceptions quickly.

-        const errorLink = onError(({ networkError }) => {
-            if (networkError) {
-                this.logger.warn('[GRAPHQL-CLIENT] NETWORK ERROR ENCOUNTERED %o', networkError);
-            }
-        });
+        const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
+            if (graphQLErrors?.length) {
+                this.logger.warn(
+                    `[GRAPHQL-CLIENT] GraphQL errors in ${operation?.operationName ?? 'anonymous'}: ${graphQLErrors
+                        .map((e) => e.message)
+                        .join('; ')}`
+                );
+            }
+            if (networkError) {
+                this.logger.warn('[GRAPHQL-CLIENT] NETWORK ERROR ENCOUNTERED %o', networkError);
+            }
+        });

166-190: Mirror the “try-next-provider” fallback for WebSocket auth connectionParams

Same issue as HTTP path: if one provider is present but unusable (empty/throws), we never attempt the others. Consider the same progressive fallback.

-                        if (getApiKey) {
-                            const apiKey = await getApiKey();
-                            return { 'x-api-key': apiKey };
-                        } else if (getLocalSession) {
-                            const localSession = await getLocalSession();
-                            if (!localSession) {
-                                throw new Error(
-                                    'No valid local session found for WebSocket authentication'
-                                );
-                            }
-                            return { 'x-local-session': localSession };
-                        } else if (getCookieAuth) {
-                            const cookieAuth = await getCookieAuth();
-                            if (!cookieAuth) {
-                                throw new Error(
-                                    'No valid session found for WebSocket cookie authentication'
-                                );
-                            }
-                            return {
-                                'x-csrf-token': cookieAuth.csrfToken,
-                                cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
-                            };
-                        }
-                        return {};
+                        // Try API key
+                        if (getApiKey) {
+                            try {
+                                const apiKey = await getApiKey();
+                                if (apiKey) return { 'x-api-key': apiKey };
+                            } catch {}
+                        }
+                        // Try local session
+                        if (getLocalSession) {
+                            try {
+                                const localSession = await getLocalSession();
+                                if (localSession) return { 'x-local-session': localSession };
+                            } catch {}
+                        }
+                        // Try cookie auth
+                        if (getCookieAuth) {
+                            try {
+                                const cookieAuth = await getCookieAuth();
+                                if (cookieAuth?.sessionId && cookieAuth?.csrfToken) {
+                                    return {
+                                        'x-csrf-token': cookieAuth.csrfToken,
+                                        cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
+                                    };
+                                }
+                            } catch {}
+                        }
+                        return {};
api/src/unraid-api/plugin/global-deps.module.ts (1)

35-37: Token naming vs. implementation type can confuse future readers

INTERNAL_CLIENT_SERVICE_TOKEN points to InternalGraphQLClientFactory (a factory), while the name suggests a “service”. Consider renaming the token in a future PR to something like INTERNAL_GRAPHQL_CLIENT_FACTORY_TOKEN and updating consumers.

Also applies to: 76-85

api/src/unraid-api/shared/internal-client.service.ts (3)

30-33: Defaulting enableSubscriptions to true changes prior behavior; verify consumers expect WS by default

Factory’s default is false, but this service flips it to true. That can open WS connections even for clients that only need HTTP. Please confirm intended behavior or consider inheriting the factory’s default.

If you decide to align with the factory’s default:

-        const { enableSubscriptions = true, origin } = options || {};
+        const { enableSubscriptions = false, origin } = options || {};

Also applies to: 68-75


81-86: Potential WS resource leak when clearing the client

ApolloClient.stop() won’t necessarily dispose the underlying graphql-ws client created inside the factory. Consider extending the factory to return a disposer you can call from clearClient(), or store a handle to the WS client inside the Apollo link and expose a dispose() hook.

I can sketch a small abstraction on the factory to return { client, dispose } and thread it through here if you want.


30-33: Single cached client ignores differing option sets; document this or key the cache by options

If callers pass different { enableSubscriptions, origin } over time, the first one “wins” until clearClient(). Either document this behavior, or (future enhancement) maintain a small keyed cache per option set.

Also applies to: 64-79

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7e027 and 1c6fcfb.

📒 Files selected for processing (36)
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json (0 hunks)
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json (0 hunks)
  • api/src/unraid-api/auth/auth.module.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.spec.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.ts (5 hunks)
  • api/src/unraid-api/auth/authentication.guard.ts (2 hunks)
  • api/src/unraid-api/auth/cookie.service.ts (3 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.strategy.ts (1 hunks)
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts (2 hunks)
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (4 hunks)
  • api/src/unraid-api/cli/admin-key.service.ts (0 hunks)
  • api/src/unraid-api/cli/api-report.service.ts (4 hunks)
  • api/src/unraid-api/cli/cli-services.module.ts (1 hunks)
  • api/src/unraid-api/cli/cli.module.spec.ts (3 hunks)
  • api/src/unraid-api/cli/cli.module.ts (0 hunks)
  • api/src/unraid-api/cli/developer/developer-tools.service.ts (2 hunks)
  • api/src/unraid-api/cli/internal-client.service.spec.ts (0 hunks)
  • api/src/unraid-api/cli/internal-client.service.ts (0 hunks)
  • api/src/unraid-api/cli/sso/validate-token.command.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/plugin/global-deps.module.ts (4 hunks)
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (2 hunks)
  • api/src/unraid-api/shared/internal-client.service.ts (2 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts (3 hunks)
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts (1 hunks)
  • packages/unraid-shared/src/index.ts (1 hunks)
  • packages/unraid-shared/src/tokens.ts (1 hunks)
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1 hunks)
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1 hunks)
💤 Files with no reviewable changes (8)
  • api/src/unraid-api/cli/internal-client.service.spec.ts
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json
  • api/src/unraid-api/cli/cli.module.ts
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts
  • api/src/unraid-api/cli/internal-client.service.ts
  • api/src/unraid-api/cli/admin-key.service.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • packages/unraid-shared/src/index.ts
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts
  • api/src/unraid-api/auth/auth.module.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • api/src/unraid-api/auth/auth.service.ts
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/auth/auth.module.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • api/src/unraid-api/auth/auth.service.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • packages/unraid-shared/src/index.ts
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts
  • api/src/unraid-api/auth/auth.module.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • api/src/unraid-api/auth/auth.service.ts
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

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

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
🧠 Learnings (6)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
📚 Learning: 2024-11-04T20:44:46.432Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:44:46.432Z
Learning: When modifying `apiKey.roles` in `removeRoleFromApiKey` and `addRoleToApiKey` within `api/src/unraid-api/auth/auth.service.ts`, concurrency issues are not a concern because the keys are stored in the file system.

Applied to files:

  • api/src/unraid-api/auth/auth.module.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/auth/auth.module.ts
  • api/src/unraid-api/auth/cookie.service.ts
📚 Learning: 2025-03-10T18:43:13.016Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/types/fastify.ts:25-30
Timestamp: 2025-03-10T18:43:13.016Z
Learning: In the Unraid API, cookies should be marked as optional (with `?`) in the FastifyRequest type definition since the cookies may not always be present, which is consistent with the authentication flow throughout the codebase.

Applied to files:

  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
🧬 Code graph analysis (15)
packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/auth/local-session.strategy.ts (4)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (15-289)
api/src/unraid-api/auth/authentication.guard.ts (1)
  • Injectable (39-112)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (10-98)
api/src/unraid-api/types/fastify.ts (1)
  • FastifyRequest (25-30)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
packages/unraid-shared/src/index.ts (1)
  • CanonicalInternalClientService (7-7)
api/src/unraid-api/cli/developer/developer-tools.service.ts (3)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/index.ts (1)
  • CanonicalInternalClientService (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/cli/__test__/api-report.service.test.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/auth/local-session.service.ts (4)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (15-289)
api/src/unraid-api/auth/local-session.strategy.ts (1)
  • Injectable (14-46)
api/src/unraid-api/shared/internal-client.service.ts (1)
  • Injectable (15-87)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (26-232)
api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (2)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/plugin/global-deps.module.ts (2)
api/src/unraid-api/auth/cookie.service.ts (1)
  • SESSION_COOKIE_CONFIG (10-10)
packages/unraid-shared/src/tokens.ts (4)
  • COOKIE_SERVICE_TOKEN (6-6)
  • NGINX_SERVICE_TOKEN (4-4)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
  • INTERNAL_CLIENT_SERVICE_TOKEN (5-5)
api/src/unraid-api/cli/sso/validate-token.command.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/cli/api-report.service.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/auth/auth.service.ts (1)
api/src/utils.ts (1)
  • handleAuthError (213-245)
api/src/unraid-api/shared/internal-client.service.ts (5)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (10-98)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (26-232)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
packages/unraid-shared/src/tokens.ts (1)
  • INTERNAL_CLIENT_SERVICE_TOKEN (5-5)
packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)
  • InternalGraphQLClientFactory (7-15)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (2)
packages/unraid-api-plugin-connect/src/mothership-proxy/connection.service.ts (1)
  • getApiKey (162-164)
api/src/unraid-api/auth/local-session.service.ts (1)
  • getLocalSession (55-63)
api/src/unraid-api/cli/cli.module.spec.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (5)
api/src/unraid-api/auth/local-session.service.ts (3)

11-26: Add cleanup on service shutdown to prevent stale tokens.

The service generates a new token on startup but doesn't clean up the session file on shutdown. This could leave stale tokens on the filesystem during normal restarts.

Implement OnModuleDestroy to clean up:

-import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
+import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
 import { randomBytes, timingSafeEqual } from 'crypto';
-import { chmod, mkdir, readFile, writeFile } from 'fs/promises';
+import { chmod, mkdir, readFile, writeFile, unlink } from 'fs/promises';
 import { dirname } from 'path';

-export class LocalSessionService implements OnModuleInit {
+export class LocalSessionService implements OnModuleInit, OnModuleDestroy {
     private readonly logger = new Logger(LocalSessionService.name);
     private sessionToken: string | null = null;
     private static readonly SESSION_FILE_PATH = '/var/run/unraid-api/local-session';

     // NOTE: do NOT cleanup the session file upon eg. module/application shutdown.
     // That would invalidate the session after each cli invocation, which is incorrect.
     // Instead, rely on the startup logic to invalidate/overwrite any obsolete session.
     async onModuleInit() {
         try {
             await this.generateLocalSession();
             this.logger.verbose('Local session initialized');
         } catch (error) {
             this.logger.error('Failed to initialize local session:', error);
         }
     }

+    async onModuleDestroy() {
+        try {
+            await unlink(LocalSessionService.SESSION_FILE_PATH);
+            this.logger.debug('Local session file cleaned up');
+        } catch (error) {
+            this.logger.debug('Failed to clean up local session file:', error);
+        }
+    }

70-78: Optimize validateLocalSession to use cached token.

Similarly, validation can use the cached token to avoid disk reads on every authentication request.

 public async validateLocalSession(token: string): Promise<boolean> {
     if (!token) return false;
+    
+    // Compare against in-memory token first (fast path)
+    if (this.sessionToken) {
+        return timingSafeEqual(Buffer.from(token, 'utf-8'), Buffer.from(this.sessionToken, 'utf-8'));
+    }

     const currentToken = await this.getLocalSession();
     if (!currentToken) return false;

     // Use constant-time comparison to prevent timing attacks
     return timingSafeEqual(Buffer.from(token, 'utf-8'), Buffer.from(currentToken, 'utf-8'));
 }

58-65: Use in-memory token to avoid per-request disk I/O.

The current implementation reads from disk on every validation request, causing unnecessary I/O on hot paths. Since the token is already stored in memory, use it as the primary source.

 public async getLocalSession(): Promise<string | null> {
+    // Prefer in-memory token to avoid disk I/O on hot paths
+    if (this.sessionToken) return this.sessionToken;
     try {
-        return await readFile(LocalSessionService.SESSION_FILE_PATH, 'utf-8');
+        const token = await readFile(LocalSessionService.SESSION_FILE_PATH, 'utf-8');
+        this.sessionToken = token.trim();
+        return this.sessionToken;
     } catch (error) {
         this.logger.warn(error, 'Local session file not found or not readable');
         return null;
     }
 }
packages/unraid-shared/src/tokens.ts (1)

5-7: Export missing tokens in shared package index

The COOKIE_SERVICE_TOKEN and CANONICAL_INTERNAL_CLIENT_TOKEN constants are not currently re-exported from the shared package entrypoint, which will prevent consumers from importing them. They are correctly wired in the API’s global-deps.module.ts, so we just need to expose them in packages/unraid-shared/src/index.ts.

• File needing update:

  • packages/unraid-shared/src/index.ts

• Add the following exports at the bottom of that file:

 export * from './other-module';
 export * from './another-module';
+export { COOKIE_SERVICE_TOKEN } from './tokens';
+export { CANONICAL_INTERNAL_CLIENT_TOKEN } from './tokens';

Once added, downstream packages can import these tokens directly via the shared package index.

api/src/unraid-api/shared/internal-client.service.ts (1)

41-44: Fix “first call wins” bug: make cache options-aware and track config.

Current cache ignores per-call options; later callers can’t enable subscriptions or change origin.

Apply:

@@
-        // If client already exists, return it
-        if (this.client) {
-            return this.client;
-        }
+        // Normalize desired configuration
+        const desiredEnableSubscriptions = options?.enableSubscriptions ?? true;
+        const desiredOrigin = options?.origin;
+
+        // If we already have a client, only return it if it satisfies desired capabilities.
+        if (this.client && this.clientConfig) {
+            const { enableSubscriptions: hasSubs, origin: currentOrigin } = this.clientConfig;
+            const needsUpgrade =
+                (desiredEnableSubscriptions && !hasSubs) ||
+                (desiredOrigin && desiredOrigin !== currentOrigin);
+            if (!needsUpgrade) {
+                return this.client;
+            }
+            // Upgrade path: drop existing instance so we can recreate with richer capabilities.
+            this.clearClient();
+        }
@@
-        // Start client creation and store the promise
-        const creationPromise = this.createClient(options);
+        // Start client creation (normalized) and store the promise
+        const creationPromise = this.createClient({
+            enableSubscriptions: desiredEnableSubscriptions,
+            origin: desiredOrigin,
+        });
@@
-        this.logger.debug('Created canonical internal GraphQL client with local session authentication');
-        return client;
+        // Track active client configuration for future reuse/upgrade decisions
+        this.clientConfig = { enableSubscriptions, origin };
+        this.logger.debug(
+            `Created canonical internal GraphQL client with local session auth (subscriptions=${enableSubscriptions}, origin=${origin ?? 'http://localhost'})`
+        );
+        return client;
@@
         this.client?.stop();
         this.client = null;
         this.clientCreationPromise = null;
+        this.clientConfig = null;

Add this field near other members:

// Track the config used to create the cached client
private clientConfig: { enableSubscriptions: boolean; origin?: string } | null = null;

Also applies to: 51-54, 84-86, 91-93

🧹 Nitpick comments (4)
api/src/unraid-api/auth/cookie.service.ts (1)

103-126: Consider selecting the most recent valid session by mtime.

The current implementation returns the first valid session found, which may not be deterministic. Consider selecting the most recent session based on file modification time.

 async getActiveSession(): Promise<string | null> {
     let sessionFiles: string[] = [];
     try {
         sessionFiles = await readdir(this.opts.sessionDir);
     } catch (e) {
         this.logger.warn(e, 'Error reading session directory');
         return null;
     }
+    let mostRecentSession: { id: string; mtime: number } | null = null;
     for (const sessionFile of sessionFiles) {
         if (!sessionFile.startsWith('sess_')) {
             continue;
         }
         try {
             const sessionData = await readFile(join(this.opts.sessionDir, sessionFile), 'ascii');
             if (this.isSessionValid(sessionData)) {
-                return sessionFile.replace('sess_', '');
+                const stats = await stat(join(this.opts.sessionDir, sessionFile));
+                const sessionId = sessionFile.replace('sess_', '');
+                if (!mostRecentSession || stats.mtimeMs > mostRecentSession.mtime) {
+                    mostRecentSession = { id: sessionId, mtime: stats.mtimeMs };
+                }
             }
         } catch {
             // Ignore unreadable files and continue scanning
             continue;
         }
     }
-    return null;
+    return mostRecentSession?.id ?? null;
 }

Don't forget to import stat:

-import { readdir, readFile } from 'fs/promises';
+import { readdir, readFile, stat } from 'fs/promises';
api/src/unraid-api/cli/cli.module.spec.ts (2)

33-37: Avoid instance checks against concrete class; assert contract instead.

Binding may switch implementations without changing the token. Prefer capability assertion.

Apply:

-    expect(factory).toBeInstanceOf(InternalGraphQLClientFactory);
+    expect(typeof (factory as any).createClient).toBe('function');

39-50: Add a no-op idempotency check for clearClient.

Low-cost sanity guard that doesn’t hit the network.

Apply:

         expect(service.getClient).toBeDefined();
         expect(service.clearClient).toBeDefined();
+        // Idempotent dispose
+        expect(() => service.clearClient()).not.toThrow();
+        expect(() => service.clearClient()).not.toThrow();
api/src/unraid-api/plugin/global-deps.module.ts (1)

3-14: Minor: avoid token name ambiguity with class name.

INTERNAL_CLIENT_FACTORY_TOKEN resolves to 'InternalClientService' which is also a class in this module; different token identities but confusing.

Consider renaming the token string in packages/unraid-shared to 'InternalGraphQLClientFactory' to reduce ambiguity. If you want, I can draft that follow-up.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c6fcfb and 1c7f3c2.

📒 Files selected for processing (9)
  • api/src/unraid-api/auth/auth.service.ts (6 hunks)
  • api/src/unraid-api/auth/cookie.service.ts (4 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
  • api/src/unraid-api/cli/cli.module.spec.ts (3 hunks)
  • api/src/unraid-api/plugin/global-deps.module.ts (5 hunks)
  • api/src/unraid-api/shared/internal-client.service.ts (1 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts (2 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts (5 hunks)
  • packages/unraid-shared/src/tokens.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.service.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.service.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

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

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
🧠 Learnings (7)
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/auth/cookie.service.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should mock external dependencies and services

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.

Applied to files:

  • api/src/unraid-api/auth/auth.service.ts
📚 Learning: 2025-01-15T21:34:00.006Z
Learnt from: pujitm
PR: unraid/api#1047
File: api/src/unraid-api/graph/sandbox-plugin.ts:57-57
Timestamp: 2025-01-15T21:34:00.006Z
Learning: In the GraphQL sandbox (api/src/unraid-api/graph/sandbox-plugin.ts), CSRF token validation should fail silently with a fallback value to maintain sandbox accessibility, as it's a development tool where strict security measures aren't required.

Applied to files:

  • api/src/unraid-api/auth/auth.service.ts
📚 Learning: 2025-03-10T17:24:06.914Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/auth/cookie.strategy.ts:19-20
Timestamp: 2025-03-10T17:24:06.914Z
Learning: In the auth system, CSRF token validation and cookie validation have been unified in the `validateCookiesCasbin()` method in the AuthService class, which takes the entire FastifyRequest object and performs both validations sequentially.

Applied to files:

  • api/src/unraid-api/auth/auth.service.ts
🧬 Code graph analysis (6)
api/src/unraid-api/shared/internal-client.service.ts (5)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (10-86)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (28-235)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
packages/unraid-shared/src/tokens.ts (1)
  • INTERNAL_CLIENT_FACTORY_TOKEN (5-5)
packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)
  • InternalGraphQLClientFactory (7-15)
api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts (1)
api/src/unraid-api/auth/cookie.service.ts (1)
  • SESSION_COOKIE_CONFIG (10-10)
api/src/unraid-api/cli/cli.module.spec.ts (2)
packages/unraid-shared/src/tokens.ts (2)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
  • INTERNAL_CLIENT_FACTORY_TOKEN (5-5)
packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)
  • InternalGraphQLClientFactory (7-15)
api/src/unraid-api/plugin/global-deps.module.ts (2)
packages/unraid-shared/src/tokens.ts (4)
  • INTERNAL_CLIENT_FACTORY_TOKEN (5-5)
  • COOKIE_SERVICE_TOKEN (6-6)
  • NGINX_SERVICE_TOKEN (4-4)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/auth/cookie.service.ts (1)
  • SESSION_COOKIE_CONFIG (10-10)
api/src/unraid-api/auth/local-session.service.ts (4)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (28-235)
api/src/unraid-api/shared/internal-client.service.ts (1)
  • Injectable (15-94)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (16-293)
api/src/unraid-api/auth/local-session.strategy.ts (1)
  • Injectable (14-46)
api/src/unraid-api/auth/auth.service.ts (2)
api/src/utils.ts (1)
  • handleAuthError (213-245)
api/src/store/index.ts (1)
  • getters (18-23)
🔇 Additional comments (11)
api/src/unraid-api/auth/auth.service.ts (3)

253-258: LGTM! Proper timing-safe comparison implemented.

Good security improvement using timingSafeEqual for CSRF token comparison, preventing timing attacks.


89-111: LGTM! Local session validation properly implemented.

The implementation correctly validates the local session token, syncs roles, and uses proper error handling.


283-292: LGTM! Local session user properly configured.

The local session user is correctly configured with admin privileges and a descriptive identifier for CLI/system operations.

api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts (1)

53-61: LGTM! Test properly configured with SESSION_COOKIE_CONFIG.

The test correctly provides the SESSION_COOKIE_CONFIG token with appropriate test values.

api/src/unraid-api/auth/cookie.service.ts (1)

78-80: LGTM! Good refactoring to centralize session validation.

The extraction of session validation logic into a private helper method improves maintainability and reduces code duplication.

api/src/unraid-api/cli/cli.module.spec.ts (1)

27-31: Good smoke test for CanonicalInternalClient.

Asserting presence of getClient is sufficient here.

api/src/unraid-api/plugin/global-deps.module.ts (4)

35-37: Factory wiring via token is correct.

Token-based binding to InternalGraphQLClientFactory looks good.


50-57: CookieService DI and config export look correct.

SESSION_COOKIE_CONFIG and COOKIE_SERVICE_TOKEN are properly provided.


62-68: Canonical client mapped to existing service.

useExisting keeps the token stable while allowing internal refactors. Good.


81-85: Export set aligns with new auth path.

COOKIE_SERVICE_TOKEN, INTERNAL_CLIENT_FACTORY_TOKEN, and CANONICAL_INTERNAL_CLIENT_TOKEN exports are appropriate.

api/src/unraid-api/shared/internal-client.service.ts (1)

75-82: Confirm default subscription behavior is intentional.

You default enableSubscriptions to true (factory defaults to false). Ensure this is desired to avoid unnecessary WS links in CLI contexts.

If this should follow caller preference, keep the normalization above but don’t force true when options are undefined.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
api/src/unraid-api/auth/auth.module.ts (1)

57-69: Lifecycle service not provided → local session never generated/cleaned

LocalSessionLifecycleService isn’t in providers, so its OnModuleInit/Destroy won’t run. Internal clients depending on x-local-session will fail.

Apply:

     providers: [
         AuthService,
         ApiKeyService,
         ServerHeaderStrategy,
         LocalSessionStrategy,
         UserCookieStrategy,
         CookieService,
-        LocalSessionService,
+        LocalSessionService,
+        LocalSessionLifecycleService,
         {
             provide: SESSION_COOKIE_CONFIG,
             useValue: CookieService.defaultOpts(),
         },
     ],
♻️ Duplicate comments (1)
api/src/unraid-api/auth/local-session.service.ts (1)

46-53: Use in-memory cache, trim on read, and fix Logger arg order

Removes hot-path disk I/O, stabilizes behavior across transient FS issues, and fixes warn signature.

Apply:

-    public async getLocalSession(): Promise<string | null> {
-        try {
-            return await readFile(LocalSessionService.getSessionFilePath(), 'utf-8');
-        } catch (error) {
-            this.logger.warn(error, 'Local session file not found or not readable');
-            return null;
-        }
-    }
+    public async getLocalSession(): Promise<string | null> {
+        if (this.sessionToken) return this.sessionToken;
+        try {
+            const token = await readFile(LocalSessionService.getSessionFilePath(), 'utf-8');
+            this.sessionToken = token.trim();
+            return this.sessionToken;
+        } catch (error) {
+            this.logger.warn(
+                `Local session file not found or not readable: ${(error as Error)?.message ?? String(error)}`
+            );
+            return null;
+        }
+    }
@@
-    public async validateLocalSession(token: string): Promise<boolean> {
-        if (!token) return false;
-
-        const currentToken = await this.getLocalSession();
-        if (!currentToken) return false;
-
-        // Use constant-time comparison to prevent timing attacks
-        return timingSafeEqual(Buffer.from(token, 'utf-8'), Buffer.from(currentToken, 'utf-8'));
-    }
+    public async validateLocalSession(token: string): Promise<boolean> {
+        if (!token) return false;
+        const currentToken = this.sessionToken ?? (await this.getLocalSession());
+        if (!currentToken) return false;
+        const a = Buffer.from(token, 'utf-8');
+        const b = Buffer.from(currentToken, 'utf-8');
+        if (a.length !== b.length) return false;
+        return timingSafeEqual(a, b);
+    }

Also applies to: 58-66

🧹 Nitpick comments (3)
api/src/unraid-api/auth/local-session.service.ts (2)

68-74: Clear in-memory token on delete; downgrade ENOENT to debug/warn; fix Logger args

Prevents stale in-memory token use; avoids noisy errors when file is already gone.

Apply:

-    public async deleteLocalSession(): Promise<void> {
-        try {
-            await unlink(LocalSessionService.getSessionFilePath());
-        } catch (error) {
-            this.logger.error(error, 'Error deleting local session file');
-        }
-    }
+    public async deleteLocalSession(): Promise<void> {
+        try {
+            await unlink(LocalSessionService.getSessionFilePath());
+        } catch (error: any) {
+            if (error?.code === 'ENOENT') {
+                this.logger.debug('Local session file already removed');
+            } else {
+                this.logger.warn(`Error deleting local session file: ${error?.message ?? String(error)}`);
+            }
+        } finally {
+            this.sessionToken = null;
+        }
+    }

33-35: Redundant chmod after writeFile with mode

writeFile already sets 0600; keeping chmod is harmless but unnecessary.

Optionally remove the extra chmod to reduce syscalls.

api/src/unraid-api/auth/auth.module.ts (1)

70-81: Exporting lifecycle service likely unnecessary

It’s API-internal; exporting increases surface without need.

Apply:

     exports: [
         AuthService,
         ApiKeyService,
         PassportModule,
         ServerHeaderStrategy,
         LocalSessionStrategy,
         UserCookieStrategy,
         CookieService,
         LocalSessionService,
-        LocalSessionLifecycleService,
         AuthZModule,
     ],
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c7f3c2 and b62f1b1.

📒 Files selected for processing (3)
  • api/src/unraid-api/auth/auth.module.ts (3 hunks)
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.module.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.module.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.module.ts
🧠 Learnings (2)
📚 Learning: 2024-11-04T20:44:46.432Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:44:46.432Z
Learning: When modifying `apiKey.roles` in `removeRoleFromApiKey` and `addRoleToApiKey` within `api/src/unraid-api/auth/auth.service.ts`, concurrency issues are not a concern because the keys are stored in the file system.

Applied to files:

  • api/src/unraid-api/auth/auth.module.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/auth/auth.module.ts
🧬 Code graph analysis (3)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (10-82)
api/src/unraid-api/auth/local-session.service.ts (5)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
  • Injectable (14-25)
api/src/unraid-api/shared/internal-client.service.ts (1)
  • Injectable (15-94)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (28-235)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (16-293)
api/src/unraid-api/auth/local-session.strategy.ts (1)
  • Injectable (14-46)
api/src/unraid-api/auth/auth.module.ts (2)
api/src/unraid-api/app/app.module.ts (1)
  • Module (22-64)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (37-76)
🔇 Additional comments (3)
api/src/unraid-api/auth/auth.module.ts (1)

22-26: Strategy order looks good

Default strategies include local-session between server-header and user-cookie; aligns with the new flow.

api/src/unraid-api/auth/local-session-lifecycle.service.ts (2)

18-24: LGTM: Ties session lifecycle to API lifecycle

Init/Destroy delegation to LocalSessionService is clear and minimal.


15-16: Confirm AuthModule provider registration

The snippet from auth.module.ts shows:

55-        }),
56-    ],
57:    providers: [
58-        AuthService,
59-        ApiKeyService,
   … 

It isn’t clear whether LocalSessionLifecycleService appears later in this list. Please double-check that AuthModule includes LocalSessionLifecycleService in its providers array so that it will be instantiated during startup.

  • If it’s missing, add it alongside the other services.
  • If it’s already present further down, you can disregard this comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
api/src/unraid-api/auth/local-session.service.ts (2)

51-58: Use in-memory cache and trim; reduce hot-path I/O and normalize value

Also improve log message ordering.

-    public async getLocalSession(): Promise<string | null> {
-        try {
-            return await readFile(LocalSessionService.getSessionFilePath(), 'utf-8');
-        } catch (error) {
-            this.logger.warn(error, 'Local session file not found or not readable');
-            return null;
-        }
-    }
+    public async getLocalSession(): Promise<string | null> {
+        if (this.sessionToken) return this.sessionToken;
+        try {
+            const raw = await readFile(LocalSessionService.getSessionFilePath(), 'utf-8');
+            this.sessionToken = raw.trim();
+            return this.sessionToken;
+        } catch (error) {
+            this.logger.warn('Local session file not found or not readable: ' + String(error));
+            return null;
+        }
+    }

63-71: Guard timingSafeEqual against length mismatch; trim inputs and prefer cached token

Current code can throw and crash on malformed header.

-    public async validateLocalSession(token: string): Promise<boolean> {
-        if (!token) return false;
-
-        const currentToken = await this.getLocalSession();
-        if (!currentToken) return false;
-
-        // Use constant-time comparison to prevent timing attacks
-        return timingSafeEqual(Buffer.from(token, 'utf-8'), Buffer.from(currentToken, 'utf-8'));
-    }
+    public async validateLocalSession(token: string): Promise<boolean> {
+        if (!token) return false;
+        const expected = this.sessionToken ?? (await this.getLocalSession());
+        if (!expected) return false;
+        const a = Buffer.from(token.trim(), 'utf-8');
+        const b = Buffer.from(expected.trim(), 'utf-8');
+        if (a.length !== b.length) return false;
+        return timingSafeEqual(a, b);
+    }
🧹 Nitpick comments (5)
api/.env.test (1)

16-18: Reorder key to satisfy dotenv-linter and keep PATHS_ grouping consistent*

Place PATHS_LOCAL_SESSION_FILE before PATHS_LOGS_FILE.

-PATHS_LOGS_FILE=./dev/log/graphql-api.log
-PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOGS_FILE=./dev/log/graphql-api.log
api/.env.development (1)

18-22: Reorder key to satisfy dotenv-linter

Move PATHS_LOCAL_SESSION_FILE before PATHS_LOGS_FILE.

-PATHS_LOGS_FILE=./dev/log/graphql-api.log
-PATHS_CONNECT_STATUS_FILE_PATH=./dev/connectStatus.json # Connect plugin status file
-PATHS_OIDC_JSON=./dev/configs/oidc.local.json
-PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOGS_FILE=./dev/log/graphql-api.log
+PATHS_CONNECT_STATUS_FILE_PATH=./dev/connectStatus.json # Connect plugin status file
+PATHS_OIDC_JSON=./dev/configs/oidc.local.json
api/src/unraid-api/auth/local-session.service.ts (3)

1-7: Prefer node: scheme for built-ins for consistency with rest of repo

Minor consistency improvement.

-import { randomBytes, timingSafeEqual } from 'crypto';
-import { chmod, mkdir, readFile, unlink, writeFile } from 'fs/promises';
-import { dirname } from 'path';
+import { randomBytes, timingSafeEqual } from 'node:crypto';
+import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
+import { dirname } from 'node:path';

37-39: Fix Logger argument order

Pass message first; current usage logs the error as the message and sets context incorrectly.

-            await chmod(LocalSessionService.getSessionFilePath(), 0o600).catch((error) => {
-                this.logger.warn(error, 'Failed to set permissions on local session file');
-            });
+            await chmod(LocalSessionService.getSessionFilePath(), 0o600).catch((error) => {
+                this.logger.warn('Failed to set permissions on local session file: ' + String(error));
+            });

73-79: Don’t error on ENOENT during cleanup; downgrade severity and handle gracefully

Avoid noisy logs when file is already gone.

-        } catch (error) {
-            this.logger.error(error, 'Error deleting local session file');
-        }
+        } catch (error: any) {
+            if (error?.code === 'ENOENT') {
+                this.logger.debug('Local session file already removed');
+                return;
+            }
+            this.logger.warn('Failed to delete local session file: ' + String(error));
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b62f1b1 and 8fd75e1.

📒 Files selected for processing (6)
  • api/.env.development (1 hunks)
  • api/.env.test (1 hunks)
  • api/.gitignore (1 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/auth/auth.module.ts (3 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • api/.gitignore
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/environment.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.module.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/environment.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.module.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/auth.module.ts
🧠 Learnings (2)
📚 Learning: 2024-11-04T20:44:46.432Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:44:46.432Z
Learning: When modifying `apiKey.roles` in `removeRoleFromApiKey` and `addRoleToApiKey` within `api/src/unraid-api/auth/auth.service.ts`, concurrency issues are not a concern because the keys are stored in the file system.

Applied to files:

  • api/src/unraid-api/auth/auth.module.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/auth/auth.module.ts
🧬 Code graph analysis (1)
api/src/unraid-api/auth/local-session.service.ts (6)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
  • Injectable (14-25)
api/src/unraid-api/shared/internal-client.service.ts (1)
  • Injectable (15-94)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (28-235)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (16-293)
api/src/unraid-api/auth/local-session.strategy.ts (1)
  • Injectable (14-46)
api/src/environment.ts (1)
  • PATHS_LOCAL_SESSION_FILE (112-113)
🪛 dotenv-linter (3.3.0)
api/.env.test

[warning] 17-17: [UnorderedKey] The PATHS_LOCAL_SESSION_FILE key should go before the PATHS_LOGS_FILE key

(UnorderedKey)

api/.env.development

[warning] 21-21: [UnorderedKey] The PATHS_LOCAL_SESSION_FILE key should go before the PATHS_LOGS_FILE key

(UnorderedKey)

🔇 Additional comments (4)
api/src/environment.ts (1)

112-113: LGTM: adds PATHS_LOCAL_SESSION_FILE with a sensible default

Matches new auth flow and mirrors other PATHS_* constants.

api/src/unraid-api/auth/auth.module.ts (3)

14-16: LGTM: import local-session services/strategy

Imports align with the new auth mechanism.


61-66: LGTM: providers include LocalSessionStrategy/Service/Lifecycle

Correct DI wiring for the new flow.


76-81: LGTM: export local-session components

Makes strategy/service available to consumers.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (3)

18-20: Add explicit return type on lifecycle hook

Helps readability and satisfies stricter TS/Nest linters.

-    async onModuleInit() {
+    async onModuleInit(): Promise<void> {
         await this.localSessionService.generateLocalSession();
     }

1-1: Consider cleaning up the session on shutdown

If the token should not persist after API stop, wire OnModuleDestroy to delete the file. If persistence is desired, ignore.

-import { Injectable, OnModuleInit } from '@nestjs/common';
+import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

-@Injectable()
-export class LocalSessionLifecycleService implements OnModuleInit {
+@Injectable()
+export class LocalSessionLifecycleService implements OnModuleInit, OnModuleDestroy {
     constructor(private readonly localSessionService: LocalSessionService) {}

-    async onModuleInit() {
+    async onModuleInit(): Promise<void> {
         await this.localSessionService.generateLocalSession();
     }
+
+    async onModuleDestroy(): Promise<void> {
+        await this.localSessionService.deleteLocalSession();
+    }
 }

Also applies to: 15-21


18-20: Optionally avoid regenerating when a valid token already exists

Prevents unnecessary churn across restarts if stability is preferred.

-    async onModuleInit(): Promise<void> {
-        await this.localSessionService.generateLocalSession();
-    }
+    async onModuleInit(): Promise<void> {
+        const existing = await this.localSessionService.getLocalSession();
+        if (!existing) {
+            await this.localSessionService.generateLocalSession();
+        }
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd75e1 and 789fb3e.

📒 Files selected for processing (1)
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
🧬 Code graph analysis (1)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (12-87)
🔇 Additional comments (2)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (2)

3-3: ESM import extension is correct per guidelines

Import uses .js specifier and the file lives under api/src/unraid-api/, matching the repo rules. No change needed.


18-20: Confirm desired failure semantics and multi-instance behavior

Do we want API startup to fail if generation throws (current behavior), or degrade gracefully? Also, in multi-instance runs, concurrent inits can race/overwrite the token—confirm single-instance assumption or add coordination.

elibosley and others added 9 commits August 27, 2025 14:42
- Introduced persistent and ephemeral key storage in ApiKeyService for better key management.
- Updated onModuleInit to load persistent keys and clean up legacy internal keys.
- Enhanced findAll and findByField methods to prioritize ephemeral keys.
- Added methods for registering ephemeral keys and modules, improving CLI integration.
- Updated AdminKeyService to generate and manage ephemeral keys for CLI operations.
- Adjusted ConnectApiKeyService to utilize module tokens instead of static keys for better security.
@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1606/dynamix.unraid.net.plg

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
api/src/unraid-api/auth/cookie.service.ts (1)

99-126: getActiveSession: pick the newest valid sess_ and harden FS ops.*

Current scan returns the first valid file (non-deterministic across filesystems). Prefer newest by mtime and use withFileTypes+stat; keep per-file try/catch.

-    async getActiveSession(): Promise<string | null> {
-        let sessionFiles: string[] = [];
-        try {
-            sessionFiles = await readdir(this.opts.sessionDir);
-        } catch (e) {
-            this.logger.warn(e, 'Error reading session directory');
-            return null;
-        }
-        for (const sessionFile of sessionFiles) {
-            if (!sessionFile.startsWith('sess_')) {
-                continue;
-            }
-            try {
-                const sessionData = await readFile(join(this.opts.sessionDir, sessionFile), 'ascii');
-                if (this.isSessionValid(sessionData)) {
-                    return sessionFile.replace('sess_', '');
-                }
-            } catch {
-                // Ignore unreadable files and continue scanning
-                continue;
-            }
-        }
-        return null;
-    }
+    async getActiveSession(): Promise<string | null> {
+        try {
+            const entries = await readdir(this.opts.sessionDir, { withFileTypes: true });
+            let latest: { id: string; mtimeMs: number } | null = null;
+            for (const entry of entries) {
+                if (!entry.isFile() || !entry.name.startsWith('sess_')) continue;
+                const fullPath = join(this.opts.sessionDir, entry.name);
+                try {
+                    const [data, stats] = await Promise.all([
+                        readFile(fullPath, 'utf8'),
+                        (await import('fs/promises')).stat(fullPath),
+                    ]);
+                    if (this.isSessionValid(data)) {
+                        const id = entry.name.slice('sess_'.length);
+                        if (!latest || stats.mtimeMs > latest.mtimeMs) {
+                            latest = { id, mtimeMs: stats.mtimeMs };
+                        }
+                    }
+                } catch {
+                    continue; // skip unreadable/corrupt files
+                }
+            }
+            return latest?.id ?? null;
+        } catch (e) {
+            this.logger.warn(e, 'Error reading session directory');
+            return null;
+        }
+    }

And update imports:

-import { readdir, readFile } from 'fs/promises';
+import { readdir, readFile, stat } from 'fs/promises';
api/src/unraid-api/auth/local-session.service.ts (1)

83-89: Cleanup on shutdown to remove the session file.

Implement OnModuleDestroy to unlink the file; avoids stale tokens during downtime/redeploy.

-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
@@
-export class LocalSessionService {
+export class LocalSessionService implements OnModuleDestroy {
@@
     public async deleteLocalSession(): Promise<void> {
         try {
             await unlink(LocalSessionService.getSessionFilePath());
         } catch (error) {
             this.logger.error(error, 'Error deleting local session file');
         }
     }
+
+    async onModuleDestroy(): Promise<void> {
+        await this.deleteLocalSession();
+    }
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)

188-190: Fix: hardcoded cookie name in WS connectionParams

The WebSocket branch still hardcodes unraid_ and will break if namePrefix is overridden. Mirror the HTTP logic.

 return {
   'x-csrf-token': cookieAuth.csrfToken,
-  cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
+  cookie: `${this.sessionCookieConfig.namePrefix}${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
 };
🧹 Nitpick comments (9)
api/.env.test (1)

17-17: Reorder key to satisfy dotenv-linter (alphabetical).

Place PATHS_LOCAL_SESSION_FILE before PATHS_LOGS_FILE.

- PATHS_LOGS_FILE=./dev/log/graphql-api.log
- PATHS_LOCAL_SESSION_FILE=./dev/local-session
+ PATHS_LOCAL_SESSION_FILE=./dev/local-session
+ PATHS_LOGS_FILE=./dev/log/graphql-api.log
api/.env.development (1)

21-21: Reorder key to satisfy dotenv-linter (alphabetical).

Put PATHS_LOCAL_SESSION_FILE before PATHS_LOGS_FILE for consistent ordering.

 PATHS_LOG_BASE=./dev/log # Where we store logs
-PATHS_LOGS_FILE=./dev/log/graphql-api.log
+PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOGS_FILE=./dev/log/graphql-api.log
-PATHS_LOCAL_SESSION_FILE=./dev/local-session
api/src/unraid-api/auth/cookie.service.ts (1)

63-76: Use utf8 when reading session files.

Avoid 'ascii' to prevent accidental data loss; utf8 is standard.

-            const sessionData = await readFile(sessionFile, 'ascii');
+            const sessionData = await readFile(sessionFile, 'utf8');
api/src/unraid-api/auth/local-session.service.ts (1)

51-58: Trim on read and prefer in-memory token to avoid per-request disk I/O.

Avoid failures from trailing newlines and reduce hot-path I/O by caching.

-    public async getLocalSession(): Promise<string | null> {
-        try {
-            return await readFile(LocalSessionService.getSessionFilePath(), 'utf-8');
-        } catch (error) {
-            this.logger.warn(error, 'Local session file not found or not readable');
-            return null;
-        }
-    }
+    public async getLocalSession(): Promise<string | null> {
+        if (this.sessionToken) return this.sessionToken;
+        try {
+            const token = await readFile(LocalSessionService.getSessionFilePath(), 'utf-8');
+            this.sessionToken = token.trim();
+            return this.sessionToken;
+        } catch (error) {
+            this.logger.warn(error, 'Local session file not found or not readable');
+            return null;
+        }
+    }
@@
-    public async validateLocalSession(token: string): Promise<boolean> {
+    public async validateLocalSession(token: string): Promise<boolean> {
         // Coerce inputs to strings (or empty string if undefined)
-        const tokenStr = token || '';
-        const currentToken = await this.getLocalSession();
-        const currentTokenStr = currentToken || '';
+        const tokenStr = (token || '').trim();
+        const currentToken = await this.getLocalSession();
+        const currentTokenStr = (currentToken || '').trim();
@@
-        const tokenBuffer = Buffer.from(tokenStr, 'utf-8');
-        const currentTokenBuffer = Buffer.from(currentTokenStr, 'utf-8');
+        const tokenBuffer = Buffer.from(tokenStr, 'utf-8');
+        const currentTokenBuffer = Buffer.from(currentTokenStr, 'utf-8');

Also applies to: 63-81

api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)

18-21: Harden lifecycle: explicit return type + cleanup on shutdown (optional).

Consider making the lifecycle explicit and cleaning up the session file during shutdown. This clarifies startup failure semantics and avoids leaving stale tokens.

  • Is it intended for API startup to fail if session generation cannot write the file? If yes, keep rethrow; if no, add retry/backoff and degrade gracefully.

Apply:

-import { Injectable, OnModuleInit } from '@nestjs/common';
+import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

-@Injectable()
-export class LocalSessionLifecycleService implements OnModuleInit {
+@Injectable()
+export class LocalSessionLifecycleService implements OnModuleInit, OnModuleDestroy {
     constructor(private readonly localSessionService: LocalSessionService) {}

-    async onModuleInit() {
+    async onModuleInit(): Promise<void> {
         await this.localSessionService.generateLocalSession();
     }
+
+    async onModuleDestroy(): Promise<void> {
+        // Best-effort cleanup; ignore failures during shutdown
+        try {
+            await this.localSessionService.deleteLocalSession();
+        } catch {
+            /* noop */
+        }
+    }
 }

Also applies to: 1-1, 15-16

api/src/unraid-api/shared/internal-graphql-client.factory.ts (3)

55-58: Guard against ambiguous auth configuration

Right now, the factory only checks that at least one getter is provided. Consider warning or throwing when multiple getters are passed to avoid surprising precedence (API key > local-session > cookie). At minimum, document the precedence.

- if (!options.getApiKey && !options.getCookieAuth && !options.getLocalSession) {
+ const provided = [options.getApiKey, options.getCookieAuth, options.getLocalSession].filter(Boolean);
+ if (provided.length === 0) {
     throw new Error(
       'One of getApiKey, getCookieAuth, or getLocalSession function is required for creating a GraphQL client'
     );
+ }
+ if (provided.length > 1) {
+   // Consider: throw instead of warn if you want to enforce exclusivity
+   this.logger.warn('Multiple auth providers supplied; using precedence: apiKey > localSession > cookie');
+ }

116-152: DRY the auth selection to prevent HTTP/WS drift

The selection and header building are duplicated for HTTP and WS, which caused the cookie prefix inconsistency. Centralize into a small helper to keep them in sync.

// outside createClient()
private async buildAuthHeaders(getters: {
  getApiKey?: () => Promise<string>;
  getLocalSession?: () => Promise<string | null>;
  getCookieAuth?: () => Promise<{ sessionId: string; csrfToken: string } | null>;
}): Promise<Record<string, string>> {
  const { getApiKey, getLocalSession, getCookieAuth } = getters;
  if (getApiKey) {
    const apiKey = await getApiKey();
    if (!apiKey) throw new Error('No valid API key returned from getApiKey()');
    return { 'x-api-key': apiKey };
  } else if (getLocalSession) {
    const localSession = await getLocalSession();
    if (!localSession) throw new Error('No valid local session found');
    return { 'x-local-session': localSession };
  } else if (getCookieAuth) {
    const cookieAuth = await getCookieAuth();
    if (!cookieAuth) throw new Error('No valid session found for cookie authentication');
    const cookie = `${this.sessionCookieConfig.namePrefix}${cookieAuth.sessionId}=${cookieAuth.sessionId}`;
    return { 'x-csrf-token': cookieAuth.csrfToken, cookie };
  }
  return {};
}

Then:

// HTTP
- const authLink = setContext(async (_, { headers }) => {
-   if (getApiKey) { ... } else if (getLocalSession) { ... } else if (getCookieAuth) { ... }
-   return { headers };
- });
+ const authLink = setContext(async (_, { headers }) => ({
+   headers: { ...headers, ...(await this.buildAuthHeaders({ getApiKey, getLocalSession, getCookieAuth })) },
+ }));

// WS
- connectionParams: async () => {
-   if (getApiKey) { ... } else if (getLocalSession) { ... } else if (getCookieAuth) { ... }
-   return {};
- },
+ connectionParams: async () => await this.buildAuthHeaders({ getApiKey, getLocalSession, getCookieAuth }),

Also applies to: 169-192


157-161: Avoid logging sensitive details

onError logs the whole networkError object. Ensure it never includes headers/connection params (e.g., x-api-key, x-local-session, cookie).

- this.logger.warn('[GRAPHQL-CLIENT] NETWORK ERROR ENCOUNTERED %o', networkError);
+ const { name, message, statusCode } = (networkError as any) ?? {};
+ this.logger.warn('[GRAPHQL-CLIENT] network error: %s (%s) %s', name ?? 'Error', String(statusCode ?? ''), message ?? '');
api/src/unraid-api/plugin/global-deps.module.ts (1)

1-98: Optional: export SESSION_COOKIE_CONFIG if other modules must override/read it

If other modules need to override or read SESSION_COOKIE_CONFIG (e.g., environment-specific presets), consider exporting it. If not needed, ignore.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 789fb3e and b3e48bd.

📒 Files selected for processing (43)
  • api/.env.development (1 hunks)
  • api/.env.test (1 hunks)
  • api/.gitignore (1 hunks)
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json (0 hunks)
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json (0 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/auth/auth.module.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.spec.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.ts (7 hunks)
  • api/src/unraid-api/auth/authentication.guard.ts (2 hunks)
  • api/src/unraid-api/auth/cookie.service.ts (4 hunks)
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.strategy.ts (1 hunks)
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts (2 hunks)
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (4 hunks)
  • api/src/unraid-api/cli/admin-key.service.ts (0 hunks)
  • api/src/unraid-api/cli/api-report.service.ts (4 hunks)
  • api/src/unraid-api/cli/cli-services.module.ts (1 hunks)
  • api/src/unraid-api/cli/cli.module.spec.ts (3 hunks)
  • api/src/unraid-api/cli/cli.module.ts (0 hunks)
  • api/src/unraid-api/cli/developer/developer-tools.service.ts (2 hunks)
  • api/src/unraid-api/cli/internal-client.service.spec.ts (0 hunks)
  • api/src/unraid-api/cli/internal-client.service.ts (0 hunks)
  • api/src/unraid-api/cli/sso/validate-token.command.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/plugin/global-deps.module.ts (5 hunks)
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (2 hunks)
  • api/src/unraid-api/shared/internal-client.service.ts (1 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts (2 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts (5 hunks)
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts (1 hunks)
  • packages/unraid-shared/src/index.ts (1 hunks)
  • packages/unraid-shared/src/tokens.ts (1 hunks)
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1 hunks)
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1 hunks)
💤 Files with no reviewable changes (9)
  • api/src/unraid-api/cli/internal-client.service.ts
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json
  • api/src/unraid-api/cli/internal-client.service.spec.ts
  • api/src/unraid-api/cli/admin-key.service.ts
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.ts
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts
  • api/src/unraid-api/cli/cli.module.ts
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • api/.gitignore
  • api/src/environment.ts
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/auth.service.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
  • api/src/unraid-api/cli/test/developer-tools.service.test.ts
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • api/src/unraid-api/auth/auth.module.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts
  • api/src/unraid-api/cli/test/api-report.service.test.ts
  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • api/src/unraid-api/shared/internal-client.service.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • packages/unraid-shared/src/index.ts
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/local-session.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • packages/unraid-shared/src/tokens.ts
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • packages/unraid-shared/src/index.ts
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/local-session.service.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
  • api/src/unraid-api/auth/local-session.service.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

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

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
🧠 Learnings (4)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • api/src/unraid-api/auth/cookie.service.ts
📚 Learning: 2025-03-10T18:43:13.016Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/types/fastify.ts:25-30
Timestamp: 2025-03-10T18:43:13.016Z
Learning: In the Unraid API, cookies should be marked as optional (with `?`) in the FastifyRequest type definition since the cookies may not always be present, which is consistent with the authentication flow throughout the codebase.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
🧬 Code graph analysis (6)
api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (1)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (3)
api/src/unraid-api/auth/cookie.service.ts (2)
  • SESSION_COOKIE_CONFIG (10-10)
  • SessionCookieConfig (12-18)
packages/unraid-api-plugin-connect/src/mothership-proxy/connection.service.ts (1)
  • getApiKey (162-164)
api/src/unraid-api/auth/local-session.service.ts (1)
  • getLocalSession (51-58)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (12-97)
api/src/unraid-api/cli/developer/developer-tools.service.ts (2)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/plugin/global-deps.module.ts (2)
packages/unraid-shared/src/tokens.ts (4)
  • INTERNAL_CLIENT_FACTORY_TOKEN (5-5)
  • COOKIE_SERVICE_TOKEN (6-6)
  • NGINX_SERVICE_TOKEN (4-4)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
api/src/unraid-api/auth/cookie.service.ts (1)
  • SESSION_COOKIE_CONFIG (10-10)
api/src/unraid-api/auth/local-session.service.ts (6)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
  • Injectable (14-21)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (28-235)
api/src/unraid-api/shared/internal-client.service.ts (1)
  • Injectable (15-94)
api/src/unraid-api/auth/auth.service.ts (1)
  • Injectable (22-380)
api/src/unraid-api/auth/local-session.strategy.ts (1)
  • Injectable (14-46)
api/src/environment.ts (1)
  • PATHS_LOCAL_SESSION_FILE (112-113)
🪛 dotenv-linter (3.3.0)
api/.env.test

[warning] 17-17: [UnorderedKey] The PATHS_LOCAL_SESSION_FILE key should go before the PATHS_LOGS_FILE key

(UnorderedKey)

api/.env.development

[warning] 21-21: [UnorderedKey] The PATHS_LOCAL_SESSION_FILE key should go before the PATHS_LOGS_FILE key

(UnorderedKey)

🔇 Additional comments (17)
api/src/unraid-api/auth/authentication.guard.ts (2)

16-16: Adding LocalSessionStrategy import is correct.

Import path uses .js extension per ESM guideline. LGTM.


41-41: LocalSessionStrategy wiring and key confirmed

  • Verified LocalSessionStrategy defines static readonly key = 'local-session' in api/src/unraid-api/auth/local-session.strategy.ts:16.
  • Confirmed AuthModule registers and exports LocalSessionStrategy:
    • providers list includes LocalSessionStrategy (auth.module.ts:61)
    • exports list includes LocalSessionStrategy (auth.module.ts:76)
  • Confirmed PassportModule.register default strategies include LocalSessionStrategy.key alongside the other strategies (auth.module.ts:23–26).

No further action needed. Everything is correctly wired.

api/src/unraid-api/auth/cookie.service.ts (1)

12-18: Exporting SessionCookieConfig and sameSite: 'lax' aligns with prior guidance.

Matches the recorded preference to use SameSite=Lax for session cookies.

api/src/unraid-api/auth/local-session.service.ts (1)

21-45: Secure token generation + 0600 permissions look good.

mkdir with recursive and explicit chmod are appropriate.

api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)

3-3: ESM path correctness.

Local import uses .js extension per repo ESM rule. LGTM.

api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (2)

66-67: Good switch to DI token override.

Overriding CANONICAL_INTERNAL_CLIENT_TOKEN keeps the test decoupled from concrete implementations.


3-3: Verify CANONICAL_INTERNAL_CLIENT_TOKEN is actually available to RestModule

It looks like CANONICAL_INTERNAL_CLIENT_TOKEN is only provided in plugin/global-deps.module.ts (via provide: CANONICAL_INTERNAL_CLIENT_TOKEN, useExisting: InternalClientService) and your integration test for RestModule doesn’t import that module. Without it (or a @Global() decorator on GlobalDepsModule), calling overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN) in your test won’t bind the token.

Please check and adjust as needed:

  • api/src/unraid-api/plugin/global-deps.module.ts
    • Confirm it’s decorated with @Global() so consumers don’t need to import it manually.
    • If not global, export and import this module where needed.

  • api/src/unraid-api/rest/rest.module.ts
    • If you rely on a non-global module, add GlobalDepsModule to the imports array.

  • api/src/unraid-api/rest/test/rest-module.integration.test.ts
    • In Test.createTestingModule({ imports: […] }), include GlobalDepsModule (or the module that provides the token) alongside RestModule.
    • Example:

    const moduleRef = await Test.createTestingModule({
      imports: [RestModule, GlobalDepsModule],
      // …
    }).compile();

Ensuring the provider is actually loaded in RestModule’s injector graph will make your overrideProvider(CANONICAL_INTERNAL_CLIENT_TOKEN) calls effective.

packages/unraid-shared/src/index.ts (1)

8-8: Type re-export is ESM-compliant.

Type-only export with .js specifier matches our ESM rule. LGTM.

packages/unraid-shared/src/tokens.ts (1)

5-7: Token rename verified and safe

  • No occurrences of the old INTERNAL_CLIENT_SERVICE_TOKEN remain in the codebase.
  • All consumers now import and use INTERNAL_CLIENT_FACTORY_TOKEN consistently (e.g., in global-deps.module.ts, service injections, and tests).
  • The CANONICAL_INTERNAL_CLIENT_TOKEN binding and injections continue to function as intended.

No further changes are necessary.

api/src/unraid-api/cli/developer/developer-tools.service.ts (2)

57-59: Nice DI switch to canonical client + explicit options.

Injecting via CANONICAL_INTERNAL_CLIENT_TOKEN and disabling subscriptions for mutations is appropriate.

Also applies to: 63-63


1-1: All CliInternalClientService references have been removed from the CLI code
The only remaining occurrence is a commented note in
• packages/unraid-shared/src/services/internal-graphql-client-usage.spec.ts (line 58)
which does not import or execute the old service. All CLI classes now consistently inject the new client via
@Inject(CANONICAL_INTERNAL_CLIENT_TOKEN) in
– api/src/unraid-api/cli/api-report.service.ts
– api/src/unraid-api/cli/sso/validate-token.command.ts
– api/src/unraid-api/cli/developer/developer-tools.service.ts

No further changes are required.

api/src/unraid-api/shared/internal-graphql-client.factory.ts (2)

33-36: Good: DI for session cookie config is in place

Injecting SESSION_COOKIE_CONFIG and using the typed SessionCookieConfig aligns with the new cookie-based auth flow.


146-151: Cookie header construction respects configured namePrefix — nice

Using this.sessionCookieConfig.namePrefix avoids hardcoding and matches the cookie service configuration.

api/src/unraid-api/plugin/global-deps.module.ts (4)

35-37: Provider wiring for INTERNAL_CLIENT_FACTORY_TOKEN looks correct

Binding InternalGraphQLClientFactory to INTERNAL_CLIENT_FACTORY_TOKEN aligns with the refactor away from the old service token.


51-57: SESSION_COOKIE_CONFIG and CookieService registration are sound

Providing CookieService.defaultOpts() and wiring COOKIE_SERVICE_TOKEN via useClass matches the new cookie-based auth flow. Per prior learnings, defaultOpts should set sameSite: 'lax'.


62-68: Canonical internal client mapping via useExisting is appropriate

Exporting CANONICAL_INTERNAL_CLIENT_TOKEN as useExisting: InternalClientService keeps a single implementation.


76-85: Token migration complete – no residual references detected

  • No occurrences of INTERNAL_CLIENT_SERVICE_TOKEN remain in api/ or packages/, confirming the old token has been fully removed.
  • INTERNAL_CLIENT_FACTORY_TOKEN is declared once in
    packages/unraid-shared/src/tokens.ts and then injected or provided consistently across:
    api/src/unraid-api/plugin/global-deps.module.ts (providers & exports)
    api/src/unraid-api/shared/internal-client.service.ts (constructor injection)
    api/src/unraid-api/cli/cli.module.spec.ts (unit tests)
  • CANONICAL_INTERNAL_CLIENT_TOKEN is provided via useExisting: InternalClientService in
    api/src/unraid-api/plugin/global-deps.module.ts and consumed by CLI services and plugin handlers without duplication.

All token migrations have been verified; no further action required.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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)
api/src/unraid-api/cli/developer/developer-tools.service.ts (1)

65-76: Surface GraphQL errors from mutate() to avoid silent failures.

Apollo doesn’t throw on GraphQL errors by default; check result.errors.

             const result = await client.mutate({
                 mutation: UPDATE_SANDBOX_MUTATION,
                 variables: {
                     input: {
                         api: {
                             sandbox: enable,
                         },
                     },
                 },
             });
 
+            if (result.errors?.length) {
+                throw new Error(result.errors.map(e => e.message).join('; '));
+            }
+
             if (result.data?.updateSettings.restartRequired) {
♻️ Duplicate comments (3)
packages/unraid-shared/src/tokens.ts (1)

6-7: Ensure tokens are re-exported and wired in DI (follow-up to earlier review)

Re-export COOKIE_SERVICE_TOKEN and CANONICAL_INTERNAL_CLIENT_TOKEN from shared index and map them in the API DI module. This was flagged earlier and is still applicable.

Add to packages/unraid-shared/src/index.ts:

export { COOKIE_SERVICE_TOKEN, CANONICAL_INTERNAL_CLIENT_TOKEN, INTERNAL_CLIENT_FACTORY_TOKEN, INTERNAL_CLIENT_SERVICE_TOKEN } from './tokens.js';

Confirm provider wiring in the API (example):

import { COOKIE_SERVICE_TOKEN, CANONICAL_INTERNAL_CLIENT_TOKEN } from '@unraid/unraid-shared';
import { CookieService } from '@app/unraid-api/auth/cookie.service.js';
import { CanonicalInternalClientService } from '@app/unraid-api/internal/canonical-internal-client.service.js';

export const GLOBAL_PROVIDERS = [
  { provide: COOKIE_SERVICE_TOKEN, useClass: CookieService },
  { provide: CANONICAL_INTERNAL_CLIENT_TOKEN, useClass: CanonicalInternalClientService },
];
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)

187-191: WebSocket cookie name hardcoded to 'unraid_' — use configured prefix to avoid auth breakage.

HTTP path uses sessionCookieConfig.namePrefix; WS path still hardcodes unraid_. Align both.

-                            return {
-                                'x-csrf-token': cookieAuth.csrfToken,
-                                cookie: `unraid_${cookieAuth.sessionId}=${cookieAuth.sessionId}`,
-                            };
+                            const cookieName =
+                                `${this.sessionCookieConfig.namePrefix}${cookieAuth.sessionId}`;
+                            return {
+                                'x-csrf-token': cookieAuth.csrfToken,
+                                cookie: `${cookieName}=${cookieAuth.sessionId}`,
+                            };
api/src/unraid-api/shared/internal-client.service.ts (1)

41-69: Fix “first call wins” caching: make the client options-aware and upgradeable.

Current cache ignores enableSubscriptions and origin. Track active config, reuse when it satisfies the request, or recreate when a later call needs subscriptions or a different origin.

@@
 export class InternalClientService implements CanonicalInternalClientService {
     private readonly logger = new Logger(InternalClientService.name);
     private client: ApolloClient<NormalizedCacheObject> | null = null;
     private clientCreationPromise: Promise<ApolloClient<NormalizedCacheObject>> | null = null;
+    // Track the config used to create the cached client
+    private clientConfig: { enableSubscriptions: boolean; origin?: string } | null = null;
@@
-        // If client already exists, return it
-        if (this.client) {
-            return this.client;
-        }
+        // Normalize desired configuration
+        const desiredEnableSubscriptions = options?.enableSubscriptions ?? true;
+        const desiredOrigin = options?.origin;
+        // If an existing client satisfies the request, reuse it; otherwise upgrade
+        if (this.client && this.clientConfig) {
+            const { enableSubscriptions: hasSubs, origin: currentOrigin } = this.clientConfig;
+            const needsUpgrade =
+                (desiredEnableSubscriptions && !hasSubs) ||
+                (desiredOrigin && desiredOrigin !== currentOrigin);
+            if (!needsUpgrade) {
+                return this.client;
+            }
+            this.clearClient();
+        }
@@
-        // Start client creation and store the promise
-        const creationPromise = this.createClient(options);
+        // Start client creation and store the promise
+        const creationPromise = this.createClient({
+            enableSubscriptions: desiredEnableSubscriptions,
+            origin: desiredOrigin,
+        });
         this.clientCreationPromise = creationPromise;
@@
-        this.logger.debug('Created canonical internal GraphQL client with local session authentication');
+        // Track active client configuration for future reuse/upgrade decisions
+        this.clientConfig = { enableSubscriptions, origin };
+        this.logger.debug(
+            `Created canonical internal GraphQL client with local session authentication (subscriptions=${enableSubscriptions}, origin=${origin ?? 'http://localhost'})`
+        );
         return client;
@@
     public clearClient() {
         // Stop the Apollo client to terminate any active processes
         this.client?.stop();
         this.client = null;
         this.clientCreationPromise = null;
+        this.clientConfig = null;
     }

Also applies to: 71-86, 88-93, 17-20

🧹 Nitpick comments (7)
api/.env.test (1)

17-17: Reorder PATHS_LOCAL_SESSION_FILE to satisfy dotenv-linter and keep alphabetical grouping

Place PATHS_LOCAL_SESSION_FILE before PATHS_LOGS_FILE.

-PATHS_LOGS_FILE=./dev/log/graphql-api.log
-PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOGS_FILE=./dev/log/graphql-api.log
api/.env.development (1)

21-21: Keep env keys ordered; move PATHS_LOCAL_SESSION_FILE above PATHS_LOGS_FILE

Aligns with dotenv-linter’s UnorderedKey rule.

-PATHS_LOGS_FILE=./dev/log/graphql-api.log
-PATHS_CONNECT_STATUS_FILE_PATH=./dev/connectStatus.json # Connect plugin status file
-PATHS_OIDC_JSON=./dev/configs/oidc.local.json
-PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOCAL_SESSION_FILE=./dev/local-session
+PATHS_LOGS_FILE=./dev/log/graphql-api.log
+PATHS_CONNECT_STATUS_FILE_PATH=./dev/connectStatus.json # Connect plugin status file
+PATHS_OIDC_JSON=./dev/configs/oidc.local.json
api/src/unraid-api/auth/auth.service.spec.ts (1)

60-64: Tighten LocalSessionService mock typing across all AuthService tests

To avoid using as any, cast your façades to Partial<LocalSessionService> before asserting the full service type. Apply the same change in each test suite:

api/src/unraid-api/auth/auth.service.spec.ts (around line 60)
api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts (around line 37)
api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (around line 41)

- const localSessionService = {
-     validateLocalSession: vi.fn(),
- } as any;
+ const localSessionService = ({
+     validateLocalSession: vi.fn(),
+ } as Partial<LocalSessionService>) as LocalSessionService;
packages/unraid-shared/src/tokens.ts (1)

5-5: Token rename is breaking; provide a temporary alias to ease migration

Consider exporting a deprecation alias for one release cycle.

 export const INTERNAL_CLIENT_FACTORY_TOKEN = 'InternalClientService';
+// DEPRECATED: remove after <next major>; maintained for backward compat
+export const INTERNAL_CLIENT_SERVICE_TOKEN = INTERNAL_CLIENT_FACTORY_TOKEN;
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)

18-20: Consider cleaning up the local session on shutdown (confirm lifecycle intent).

If the local session should be scoped to the API’s lifetime, delete it on shutdown. If persistence across restarts is desired, ignore.

-import { Injectable, OnModuleInit } from '@nestjs/common';
+import { Injectable, OnModuleInit, OnApplicationShutdown } from '@nestjs/common';
@@
-@Injectable()
-export class LocalSessionLifecycleService implements OnModuleInit {
+@Injectable()
+export class LocalSessionLifecycleService implements OnModuleInit, OnApplicationShutdown {
@@
     async onModuleInit() {
         await this.localSessionService.generateLocalSession();
     }
+
+    async onApplicationShutdown() {
+        await this.localSessionService.deleteLocalSession();
+    }
 }
api/src/unraid-api/shared/internal-graphql-client.factory.ts (2)

157-161: Also log GraphQL errors, not just network errors.

Improves debuggability without changing behavior.

-        const errorLink = onError(({ networkError }) => {
-            if (networkError) {
-                this.logger.warn('[GRAPHQL-CLIENT] NETWORK ERROR ENCOUNTERED %o', networkError);
-            }
-        });
+        const errorLink = onError(({ graphQLErrors, networkError }) => {
+            if (graphQLErrors?.length) {
+                for (const err of graphQLErrors) {
+                    this.logger.warn('[GRAPHQL-CLIENT] GRAPHQL ERROR %o', {
+                        message: err.message,
+                        path: err.path,
+                        extensions: err.extensions,
+                    });
+                }
+            }
+            if (networkError) {
+                this.logger.warn('[GRAPHQL-CLIENT] NETWORK ERROR %o', networkError);
+            }
+        });

55-58: Tighten error message wording.

Minor clarity/grammar tweak.

-            throw new Error(
-                'One of getApiKey, getCookieAuth, or getLocalSession function is required for creating a GraphQL client'
-            );
+            throw new Error(
+                'At least one of getApiKey, getCookieAuth, or getLocalSession is required to create a GraphQL client'
+            );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 789fb3e and b3e48bd.

📒 Files selected for processing (43)
  • api/.env.development (1 hunks)
  • api/.env.test (1 hunks)
  • api/.gitignore (1 hunks)
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json (0 hunks)
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json (0 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/auth/auth.module.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.spec.ts (3 hunks)
  • api/src/unraid-api/auth/auth.service.ts (7 hunks)
  • api/src/unraid-api/auth/authentication.guard.ts (2 hunks)
  • api/src/unraid-api/auth/cookie.service.ts (4 hunks)
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.service.ts (1 hunks)
  • api/src/unraid-api/auth/local-session.strategy.ts (1 hunks)
  • api/src/unraid-api/cli/__test__/api-report.service.test.ts (2 hunks)
  • api/src/unraid-api/cli/__test__/developer-tools.service.test.ts (4 hunks)
  • api/src/unraid-api/cli/admin-key.service.ts (0 hunks)
  • api/src/unraid-api/cli/api-report.service.ts (4 hunks)
  • api/src/unraid-api/cli/cli-services.module.ts (1 hunks)
  • api/src/unraid-api/cli/cli.module.spec.ts (3 hunks)
  • api/src/unraid-api/cli/cli.module.ts (0 hunks)
  • api/src/unraid-api/cli/developer/developer-tools.service.ts (2 hunks)
  • api/src/unraid-api/cli/internal-client.service.spec.ts (0 hunks)
  • api/src/unraid-api/cli/internal-client.service.ts (0 hunks)
  • api/src/unraid-api/cli/sso/validate-token.command.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/plugin/global-deps.module.ts (5 hunks)
  • api/src/unraid-api/rest/__test__/rest-module.integration.test.ts (2 hunks)
  • api/src/unraid-api/shared/internal-client.service.ts (1 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts (2 hunks)
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts (5 hunks)
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts (1 hunks)
  • packages/unraid-shared/src/index.ts (1 hunks)
  • packages/unraid-shared/src/tokens.ts (1 hunks)
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1 hunks)
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1 hunks)
💤 Files with no reviewable changes (9)
  • packages/unraid-api-plugin-connect/src/authn/connect-api-key.service.ts
  • api/src/unraid-api/cli/internal-client.service.ts
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.ts
  • api/src/unraid-api/cli/internal-client.service.spec.ts
  • api/src/unraid-api/cli/cli.module.ts
  • packages/unraid-api-plugin-connect/src/internal-rpc/internal.client.spec.ts
  • api/src/unraid-api/cli/admin-key.service.ts
  • api/dev/keys/fc91da7b-0284-46f4-9018-55aa9759fba9.json
  • api/dev/keys/b5b4aa3d-8e40-4c92-bc40-d50182071886.json
🚧 Files skipped from review as they are similar to previous changes (25)
  • api/src/unraid-api/cli/test/api-report.service.test.ts
  • packages/unraid-shared/src/index.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.spec.ts
  • api/src/environment.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership-subscription.handler.ts
  • api/src/unraid-api/cli/cli-services.module.ts
  • api/src/unraid-api/auth/auth.service.ts
  • packages/unraid-shared/src/types/internal-graphql-client.factory.ts
  • api/src/unraid-api/rest/test/rest-module.integration.test.ts
  • api/.gitignore
  • api/src/unraid-api/graph/resolvers/api-key/api-key.mutation.spec.ts
  • api/src/unraid-api/cli/test/developer-tools.service.test.ts
  • api/src/unraid-api/auth/local-session.service.ts
  • api/src/unraid-api/auth/local-session.strategy.ts
  • packages/unraid-api-plugin-connect/src/mothership-proxy/mothership.module.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key.resolver.spec.ts
  • api/src/unraid-api/auth/cookie.service.ts
  • api/src/unraid-api/auth/auth.module.ts
  • api/src/unraid-api/cli/cli.module.spec.ts
  • api/src/unraid-api/cli/api-report.service.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect-settings.service.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • packages/unraid-api-plugin-connect/src/unraid-connect/connect.module.ts
  • packages/unraid-shared/src/types/canonical-internal-client.interface.ts
  • api/src/unraid-api/plugin/global-deps.module.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • packages/unraid-shared/src/tokens.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/auth/auth.service.spec.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/auth/auth.service.spec.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/auth/auth.service.spec.ts
  • api/src/unraid-api/cli/developer/developer-tools.service.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • api/src/unraid-api/shared/internal-client.service.ts
  • api/src/unraid-api/auth/local-session-lifecycle.service.ts
  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
  • packages/unraid-shared/src/tokens.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

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

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/auth/auth.service.spec.ts
🧠 Learnings (3)
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
📚 Learning: 2025-03-10T18:43:13.016Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/types/fastify.ts:25-30
Timestamp: 2025-03-10T18:43:13.016Z
Learning: In the Unraid API, cookies should be marked as optional (with `?`) in the FastifyRequest type definition since the cookies may not always be present, which is consistent with the authentication flow throughout the codebase.

Applied to files:

  • api/src/unraid-api/shared/internal-graphql-client.factory.ts
🧬 Code graph analysis (4)
api/src/unraid-api/cli/developer/developer-tools.service.ts (3)
packages/unraid-shared/src/tokens.ts (1)
  • CANONICAL_INTERNAL_CLIENT_TOKEN (7-7)
packages/unraid-shared/src/index.ts (1)
  • CanonicalInternalClientService (8-8)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
api/src/unraid-api/shared/internal-client.service.ts (5)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (12-97)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (1)
  • Injectable (28-235)
packages/unraid-shared/src/types/canonical-internal-client.interface.ts (1)
  • CanonicalInternalClientService (3-14)
packages/unraid-shared/src/tokens.ts (1)
  • INTERNAL_CLIENT_FACTORY_TOKEN (5-5)
packages/unraid-shared/src/types/internal-graphql-client.factory.ts (1)
  • InternalGraphQLClientFactory (7-15)
api/src/unraid-api/auth/local-session-lifecycle.service.ts (1)
api/src/unraid-api/auth/local-session.service.ts (1)
  • Injectable (12-97)
api/src/unraid-api/shared/internal-graphql-client.factory.ts (3)
api/src/unraid-api/auth/cookie.service.ts (2)
  • SESSION_COOKIE_CONFIG (10-10)
  • SessionCookieConfig (12-18)
packages/unraid-api-plugin-connect/src/mothership-proxy/connection.service.ts (1)
  • getApiKey (162-164)
api/src/unraid-api/auth/local-session.service.ts (1)
  • getLocalSession (51-58)
🪛 dotenv-linter (3.3.0)
api/.env.development

[warning] 21-21: [UnorderedKey] The PATHS_LOCAL_SESSION_FILE key should go before the PATHS_LOGS_FILE key

(UnorderedKey)

api/.env.test

[warning] 17-17: [UnorderedKey] The PATHS_LOCAL_SESSION_FILE key should go before the PATHS_LOGS_FILE key

(UnorderedKey)

🔇 Additional comments (7)
api/.env.test (1)

17-17: Confirm dev token file is ignored from VCS

Ensure dev/local-session is gitignored to avoid leaking local tokens (AI summary says it is, but please double-check).

api/src/unraid-api/auth/auth.service.spec.ts (2)

11-11: Import looks correct and ESM-friendly

Uses .js extension per repo ESM guideline. No issues.


21-21: LocalSessionService test variable added—OK

Declaration aligns with the updated AuthService constructor.

api/src/unraid-api/auth/authentication.guard.ts (2)

16-16: Importing LocalSessionStrategy—good

Import path uses .js extension and matches the new strategy.


41-41: Strategy order: verify intended precedence

Order is [ServerHeader, LocalSession, UserCookie]. Confirm this is the desired priority (internal header first, then local-session for CLI, finally user cookie). If local-session should supersede server header in some contexts, swap accordingly.

api/src/unraid-api/cli/developer/developer-tools.service.ts (1)

57-59: Switch to canonical internal client: LGTM.

DI via CANONICAL_INTERNAL_CLIENT_TOKEN and type usage are correct.

api/src/unraid-api/shared/internal-client.service.ts (1)

46-69: Concurrency guard and in-flight promise handling: solid.

The promise pinning and equality checks avoid races and stale writes.

@pujitm pujitm merged commit c1ab3a4 into main Aug 27, 2025
18 checks passed
@pujitm pujitm deleted the feat/memory-keys branch August 27, 2025 19:28
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.

3 participants