Skip to content

feat: add account_id param to GET /api/sandboxes for Org API keys#207

Merged
sweetmantech merged 3 commits intotestfrom
sweetmantech/myc-4146-docs-get-apisandboxes-accept-account_id-param-only-for-org
Feb 5, 2026
Merged

feat: add account_id param to GET /api/sandboxes for Org API keys#207
sweetmantech merged 3 commits intotestfrom
sweetmantech/myc-4146-docs-get-apisandboxes-accept-account_id-param-only-for-org

Conversation

@sweetmantech
Copy link
Contributor

@sweetmantech sweetmantech commented Feb 4, 2026

Summary

  • Add account_id query parameter to GET /api/sandboxes endpoint (org API keys only)
  • Create buildGetSandboxesParams.ts for consistent auth/access handling
  • Add 403 Forbidden response for unauthorized account_id usage
  • Update validateGetSandboxesRequest.ts to use the new builder pattern

Changes

  • New file: lib/sandbox/buildGetSandboxesParams.ts - Handles authorization logic for account_id filtering
  • Updated: lib/sandbox/validateGetSandboxesRequest.ts - Added account_id query param support
  • New tests: lib/sandbox/__tests__/buildGetSandboxesParams.test.ts - 11 test cases
  • Updated tests: lib/sandbox/__tests__/validateGetSandboxesRequest.test.ts - 9 test cases

Test plan

  • All 80 sandbox tests pass
  • Personal keys cannot use account_id (returns 403)
  • Org keys can filter by member account_id
  • Org keys get error for non-member account_id (returns 403)
  • Recoup admin has universal access

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced sandbox query filtering to support account-specific queries, enabling more granular sandbox management based on authentication context.
  • Improvements

    • Improved parameter validation and error handling for sandbox queries with better access control checks.

- Add buildGetSandboxesParams.ts for consistent auth/access handling
- Update validateGetSandboxesRequest to support account_id query param
- Add 403 response for unauthorized account_id usage
- Add comprehensive tests for all scenarios

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@vercel
Copy link
Contributor

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Feb 5, 2026 3:39pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

A new utility module extracts sandbox query parameter building logic from validation code into a reusable function that handles authentication context, account access validation, and org membership checks to construct appropriate SelectAccountSandboxes parameters.

Changes

Cohort / File(s) Summary
Sandbox Parameter Builder
lib/sandbox/buildGetSandboxesParams.ts
New utility module exporting BuildGetSandboxesParamsInput interface, BuildGetSandboxesParamsResult union type, and async buildGetSandboxesParams() function. Implements account access validation via canAccessAccount, org membership fetching, and conditional parameter construction based on authentication context (Recoup admin, org key, or personal key).
Validation Integration
lib/sandbox/validateGetSandboxesRequest.ts
Refactored to depend on new buildGetSandboxesParams utility instead of inline parameter construction. Added account_id to query schema validation and extended error handling to catch parameter-building failures with 403 responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

Sandboxes organized with care,
Utility functions floating in air,
Params built cleanly, no mess in sight,
Access checked once, always right ✨
Refactored code, a solid delight.

🚥 Pre-merge checks | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Code violates SOLID principles with fail-open security vulnerability: empty member arrays bypass authorization filters and expose all sandboxes. Implement fail-closed guard checking memberAccountIds.length === 0, pass orgId to params as secondary safeguard, and separate authorization validation into dedicated pre-check function.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4146-docs-get-apisandboxes-accept-account_id-param-only-for-org

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Move access control check from buildGetSandboxesParams to validateGetSandboxesRequest
- buildGetSandboxesParams now assumes access has been validated by caller
- Update tests to reflect the new responsibility separation

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The refactor in 32523bf moved the access check out of the builder and
into validateGetSandboxesRequest, breaking the established pattern used
by buildGetChatsParams and buildGetPulsesParams. This left the builder
as a security-unaware function that blindly accepted any target_account_id.

Align with the correct pattern: canAccessAccount lives inside the builder
so any caller gets defense-in-depth access checks.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@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

🤖 Fix all issues with AI agents
In `@lib/sandbox/buildGetSandboxesParams.ts`:
- Around line 58-63: When org_id is provided in buildGetSandboxesParams, the
code currently calls getAccountOrganizations and returns accountIds even if that
call failed (empty array), which lets selectAccountSandboxes drop all filters;
fix by detecting when getAccountOrganizations returns an empty member list:
after const memberAccountIds = orgMembers.map(...), if memberAccountIds.length
=== 0 return { params: null, error: new Error('no organization members found') }
(or appropriate error shape), and also include orgId: org_id in the returned
params object (return { params: { accountIds: memberAccountIds, sandboxId:
sandbox_id, orgId: org_id }, error: null }) so selectAccountSandboxes can still
apply an org-level filter as a defensive safeguard.
🧹 Nitpick comments (2)
lib/sandbox/validateGetSandboxesRequest.ts (2)

14-24: Consider renaming to validateGetSandboxesQuery.ts for consistency.
This matches the validate*.ts naming convention and improves discoverability.


9-12: Export the inferred query type and align filename with query parameter validation.

The schema validates query parameters (sandbox_id, account_id from searchParams), so it should follow the naming and type export patterns used throughout the codebase:

♻️ Suggested changes
 const getSandboxesQuerySchema = z.object({
   sandbox_id: z.string().optional(),
   account_id: z.string().uuid("account_id must be a valid UUID").optional(),
 });
+
+export type GetSandboxesQuery = z.infer<typeof getSandboxesQuerySchema>;

Also rename the file from validateGetSandboxesRequest.ts to validateGetSandboxesQuery.ts to match the guideline pattern for query validation functions.

Comment on lines +58 to +63
if (org_id) {
// Org key: fetch all member account IDs for this organization
const orgMembers = await getAccountOrganizations({ organizationId: org_id });
const memberAccountIds = orgMembers.map(member => member.account_id);
return { params: { accountIds: memberAccountIds, sandboxId: sandbox_id }, error: null };
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd 'buildGetSandboxesParams\.ts$' lib -t f

Repository: Recoupable-com/Recoup-API

Length of output: 107


🏁 Script executed:

fd 'getAccountOrganizations' lib -t f | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 133


🏁 Script executed:

fd 'selectAccountSandboxes' lib -t f | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 197


🏁 Script executed:

wc -l lib/sandbox/buildGetSandboxesParams.ts

Repository: Recoupable-com/Recoup-API

Length of output: 110


🏁 Script executed:

cat -n lib/sandbox/buildGetSandboxesParams.ts

Repository: Recoupable-com/Recoup-API

Length of output: 3280


🏁 Script executed:

wc -l lib/supabase/account_organization_ids/getAccountOrganizations.ts

Repository: Recoupable-com/Recoup-API

Length of output: 136


🏁 Script executed:

cat -n lib/supabase/account_organization_ids/getAccountOrganizations.ts

Repository: Recoupable-com/Recoup-API

Length of output: 2175


🏁 Script executed:

wc -l lib/supabase/account_sandboxes/selectAccountSandboxes.ts

Repository: Recoupable-com/Recoup-API

Length of output: 128


🏁 Script executed:

cat -n lib/supabase/account_sandboxes/selectAccountSandboxes.ts

Repository: Recoupable-com/Recoup-API

Length of output: 2089


🏁 Script executed:

cat -n lib/supabase/account_sandboxes/__tests__/selectAccountSandboxes.test.ts

Repository: Recoupable-com/Recoup-API

Length of output: 5393


🏁 Script executed:

# Verify that selectAccountSandboxes does NOT apply any filter when both accountIds and orgId are absent/empty
cat -n lib/supabase/account_sandboxes/selectAccountSandboxes.ts | sed -n '24,48p'

Repository: Recoupable-com/Recoup-API

Length of output: 995


Fail-open vulnerability: empty org member lookup exposes all sandboxes.

getAccountOrganizations returns [] on error (line 58 of getAccountOrganizations.ts). When this happens, buildGetSandboxesParams passes accountIds: [] to selectAccountSandboxes without orgId. In selectAccountSandboxes, the condition accountIds && accountIds.length > 0 fails, and since orgId is not passed, the fallback else if (orgId) also fails. This leaves the query completely unfiltered—all sandboxes are returned to the org key.

Guard against this by checking memberAccountIds.length === 0 and returning an error. Additionally, pass orgId to params so that selectAccountSandboxes can apply an organization filter as a defensive safeguard:

Suggested fail-closed guard
   if (org_id) {
     // Org key: fetch all member account IDs for this organization
     const orgMembers = await getAccountOrganizations({ organizationId: org_id });
     const memberAccountIds = orgMembers.map(member => member.account_id);
+    if (memberAccountIds.length === 0) {
+      return { params: null, error: "No organization members found for this org" };
+    }
-    return { params: { accountIds: memberAccountIds, sandboxId: sandbox_id }, error: null };
+    return {
+      params: { accountIds: memberAccountIds, orgId: org_id, sandboxId: sandbox_id },
+      error: null,
+    };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (org_id) {
// Org key: fetch all member account IDs for this organization
const orgMembers = await getAccountOrganizations({ organizationId: org_id });
const memberAccountIds = orgMembers.map(member => member.account_id);
return { params: { accountIds: memberAccountIds, sandboxId: sandbox_id }, error: null };
}
if (org_id) {
// Org key: fetch all member account IDs for this organization
const orgMembers = await getAccountOrganizations({ organizationId: org_id });
const memberAccountIds = orgMembers.map(member => member.account_id);
if (memberAccountIds.length === 0) {
return { params: null, error: "No organization members found for this org" };
}
return {
params: { accountIds: memberAccountIds, orgId: org_id, sandboxId: sandbox_id },
error: null,
};
}
🤖 Prompt for AI Agents
In `@lib/sandbox/buildGetSandboxesParams.ts` around lines 58 - 63, When org_id is
provided in buildGetSandboxesParams, the code currently calls
getAccountOrganizations and returns accountIds even if that call failed (empty
array), which lets selectAccountSandboxes drop all filters; fix by detecting
when getAccountOrganizations returns an empty member list: after const
memberAccountIds = orgMembers.map(...), if memberAccountIds.length === 0 return
{ params: null, error: new Error('no organization members found') } (or
appropriate error shape), and also include orgId: org_id in the returned params
object (return { params: { accountIds: memberAccountIds, sandboxId: sandbox_id,
orgId: org_id }, error: null }) so selectAccountSandboxes can still apply an
org-level filter as a defensive safeguard.

@sweetmantech sweetmantech merged commit 451a238 into test Feb 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant