Skip to content

✨ server: add allower#839

Open
aguxez wants to merge 5 commits intomainfrom
gcp-allower
Open

✨ server: add allower#839
aguxez wants to merge 5 commits intomainfrom
gcp-allower

Conversation

@aguxez
Copy link
Copy Markdown
Contributor

@aguxez aguxez commented Feb 25, 2026

closes #643

Summary by CodeRabbit

  • New Features

    • GCP KMS integration for account keys, an account-allower (firewall) flow, and automated asset poking for ETH/WETH/ERC‑20
  • Bug Fixes

    • Simplified per-account activity orchestration, error handling, and poke/account activation flow
  • Tests

    • Added GCP credentials validation tests and expanded tests for account, firewall, and poke behaviors
  • Chores

    • Added runtime env vars and cloud/KMS-related dependencies

Open with Devin

Greptile Summary

This PR integrates Google Cloud KMS (via @google-cloud/kms and @valora/viem-account-hsm-gcp) to sign firewall allow transactions with an HSM-backed key, and adds a new keeper.poke utility that proactively pokes account assets (ETH, WETH, ERC-20) after KYC approval and on activity webhooks.

Key changes:

  • server/utils/gcp.ts — new module handles credential bootstrapping (triple base64 decode → /tmp/gcp-service-account.json), lazy initialization with promise caching, and retryable KMS error classification.
  • server/utils/accounts.ts — adds getAccount() (GCP KMS → viem LocalAccount), allower() (wallet client that wraps allow on the Firewall contract), and poke() (scans balances and pokes every non-zero asset via exaSend).
  • server/hooks/persona.ts — after KYC approval calls allow then fire-and-forgets poke; allowerPromise caches the expensive KMS init across requests.
  • server/hooks/activity.ts — calls poke after account deployment, with NotAllowed in the ignore list.

Verified findings:

  • The delay function in poke's withRetry uses bitwise left-shift which overflows at count >= 31 (currently safe with retryCount: 10, but a footgun if the parameter is raised).
  • The DECODING_ITERATIONS = 3 constant lacks documentation explaining the triple base64 encoding strategy.
  • The keeper client's onFetchRequest hook lacks error handling, unlike the allower version, making it susceptible to unhandled exceptions on malformed RPC payloads.

Confidence Score: 3/5

  • Mergeable with awareness of three concrete code-quality issues requiring follow-up.
  • The PR's core allow+poke flow is functional and well-tested. However, three verified findings need attention: (1) the bitwise-shift overflow in retry delay is a preventable footgun if retryCount is raised, (2) the triple base64 decoding strategy lacks documentation creating fragility, and (3) the missing try/catch in keeper's onFetchRequest creates an inconsistency with allower that could surface unhandled exceptions. None of these are critical under normal conditions, but they represent code-quality debt worth fixing before production ramp-up.
  • server/utils/accounts.ts (bitwise-shift delay, error handling inconsistency) and server/utils/gcp.ts (undocumented constant).

Last reviewed commit: 0275499

Context used:

  • Rule from dashboard - AGENTS.md (source)
  • Rule from dashboard - CLAUDE.md (source)

This is part 1 of 2 in a stack made with GitButler:

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 25, 2026

🦋 Changeset detected

Latest commit: 9f8058b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds GCP KMS integration and runtime envs, replaces default keeper with a named keeper from server/utils/accounts (new getAccount/allower/withExaSend APIs), integrates firewall allower and keeper.poke into persona/activity hooks, restructures account orchestration, and updates tests/mocks.

Changes

Cohort / File(s) Summary
Changesets & small config
.changeset/lucky-jokes-change.md, .changeset/silly-yaks-divide.md, cspell.json
New changesets and minor cspell addition.
Deployment & runtime envs
.do/app.yaml, server/script/openapi.ts, server/vitest.config.mts
Add GCP env vars: GCP_BASE64_JSON, GCP_KMS_KEY_RING, GCP_KMS_KEY_VERSION, GCP_PROJECT_ID for runtime and tests.
Dependencies
server/package.json
Add @google-cloud/kms and @valora/viem-account-hsm-gcp.
GCP utilities & credential init
server/utils/gcp.ts, server/test/utils/gcp.test.ts
New GCP credential initializer, idempotent init/reset, hasCredentials, retryable-KMS-error helper, and tests for file write/access and permissions.
Accounts API & keeper export
server/utils/accounts.ts, server/test/mocks/accounts.ts, server/test/utils/accounts.test.ts
New keeper named export and APIs: getAccount(), allower(), withExaSend(); extender signatures adjusted; tests/mocks updated to import from .../accounts.
Removed legacy keeper
server/utils/keeper.ts
Removed previous default-exported keeper and legacy extender/exaSend implementation.
Hook imports & behavior
server/hooks/activity.ts, server/hooks/persona.ts, server/hooks/block.ts, server/hooks/panda.ts
Switch to named keeper from utils/accounts; activity refactored to account-lookup per-account spans and simplified flows; persona integrates firewall allower + keeper.poke and adjusts addCapita/account derivation.
Tests & mocks updates
server/test/hooks/*.test.ts, server/test/api/card.test.ts, server/test/e2e.ts, server/test/mocks/accounts.ts, server/test/hooks/persona.test.ts
Replace keeper imports/usages, adapt tests to keeper.poke semantics and balance setups, add accounts mocks and firewall/allower mocks, add GCP-related tests.
Vitest / test env
server/vitest.config.mts
Expose new GCP env vars to Vitest environment.

Sequence Diagram

sequenceDiagram
    participant Client
    participant PersonaHook as Persona Hook
    participant RiskSvc as Risk Assessment
    participant Allower as Firewall/Allower
    participant GCP as GCP KMS
    participant Chain as Chain / keeper

    Client->>PersonaHook: POST inquiry
    PersonaHook->>RiskSvc: perform risk assessment
    RiskSvc-->>PersonaHook: pass/fail

    alt pass
        PersonaHook->>PersonaHook: derive account
        PersonaHook->>Allower: getAllower()
        Allower->>GCP: init credentials / request KMS-backed account
        GCP-->>Allower: LocalAccount / credentials
        Allower-->>PersonaHook: allower client ready
        PersonaHook->>Allower: allow(account)
        Allower-->>PersonaHook: allow result
        PersonaHook->>Chain: keeper.poke(account, { ignore: [...] })
        Chain-->>PersonaHook: tx hash / result
        PersonaHook-->>Client: 200 success
    else fail
        PersonaHook-->>Client: rejection
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
  • nfmelendez
  • dieguezguille
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '✨ server: add allower' accurately reflects the main changes: adding an allower function to the server's accounts module for firewall integration, which is a central addition across multiple files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gcp-allower

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.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @aguxez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the server's account management and security by integrating Google Cloud KMS for a new allower functionality. It centralizes wallet client operations into a dedicated utility, ensuring that account creation and asset 'poking' are handled securely and efficiently, especially after user verification. The changes also include necessary environment configurations and dependency updates to support this new cryptographic infrastructure.

Highlights

  • GCP KMS Integration for Allower: Introduced a new allower functionality that leverages Google Cloud Key Management Service (KMS) for secure cryptographic operations, specifically for allowing accounts within the system. This enhances the security posture of account management.
  • Refactored Account Management Utilities: Consolidated and refactored the keeper and new allower wallet client logic into a dedicated server/utils/accounts.ts module. This centralizes account-related transaction sending and interaction with smart contracts.
  • Automated Account Poking Post-KYC: Implemented logic within the persona hook to automatically 'poke' an account after a successful Know Your Customer (KYC) process. This ensures that newly verified accounts are properly initialized and their assets are recognized by the system, including interaction with the new allower firewall.
  • Environment Variable Configuration: Added new environment variables (GCP_KMS_KEY_RING, GCP_KMS_KEY_VERSION, GCP_PROJECT_ID, GCP_BASE64_JSON) to .do/app.yaml and server/vitest.config.mts to support the configuration and testing of the GCP KMS integration.
  • Dependency Updates: Updated server/package.json to include @google-cloud/kms and @valora/viem-account-hsm-gcp, providing the necessary libraries for interacting with GCP KMS and integrating HSM-backed accounts with Viem.
Changelog
  • .changeset/lucky-jokes-change.md
    • Added a new changeset file to document the integration of GCP KMS for the allower functionality.
  • .changeset/silly-yaks-divide.md
    • Added a new changeset file to document the feature of poking accounts after KYC completion.
  • .do/app.yaml
    • Added new environment variables for GCP KMS configuration, including key ring, key version, project ID, and base64 encoded JSON credentials.
  • cspell.json
    • Added 'valora' to the custom spelling dictionary.
  • server/hooks/activity.ts
    • Removed unused ABI imports and the withRetry utility from viem.
    • Updated the import path for keeper to the new ../utils/accounts module.
    • Renamed accounts variable to accountLookup for clarity in database queries.
    • Modified the logic for processing transfers, replacing pokes map with a Set of accounts.
    • Refactored the account creation and poking logic to use the new keeper.poke function, simplifying asset handling and error management.
  • server/hooks/block.ts
    • Updated the import path for keeper to the new ../utils/accounts module.
  • server/hooks/panda.ts
    • Updated the import path for keeper to the new ../utils/accounts module.
  • server/hooks/persona.ts
    • Imported firewallAddress from @exactly/common/generated/chain.
    • Imported allower and keeper from the new ../utils/accounts module.
    • Added a getAllower function to lazily initialize the allower client.
    • Integrated allower().then((client) => client.allow(account)) call after risk assessment to allow the account through the firewall.
    • Added a keeper.poke call after KYC to update account assets with a notification.
    • Refactored the addCapita call to directly use the parsed account address, removing the safeParse check.
  • server/package.json
    • Added @google-cloud/kms as a dependency.
    • Added @valora/viem-account-hsm-gcp as a dependency.
  • server/script/openapi.ts
    • Added stub environment variables for GCP KMS configuration.
  • server/test/api/card.test.ts
    • Updated the mock import from ../mocks/keeper to ../mocks/accounts.
  • server/test/e2e.ts
    • Updated the mock import from ../mocks/keeper to ../mocks/accounts.
  • server/test/hooks/activity.test.ts
    • Updated the mock import from ../mocks/keeper to ../mocks/accounts.
    • Removed the test case for capturing 'no balance' errors after retries.
    • Adjusted captureException expectation to be more general.
    • Added anvilClient.setBalance calls to various tests to ensure sufficient balance for transactions.
    • Replaced exaSend spy with pokeSpy for keeper interactions.
    • Added a new test case to verify poke is called with the correct ignore option.
  • server/test/hooks/block.test.ts
    • Updated the mock import from ../mocks/keeper to ../mocks/accounts.
  • server/test/hooks/panda.test.ts
    • Updated the mock import from ../mocks/keeper to ../mocks/accounts.
  • server/test/hooks/persona.test.ts
    • Added mocks for accounts and firewallAddress.
    • Updated afterEach hook to restore all mocks.
    • Updated persona signature header in test payloads.
    • Added tests for poking assets when balances are positive, poking only ETH, skipping WETH when ETH balance is positive, and not poking when balances are zero.
    • Added a test case to verify error handling when the firewall call fails.
    • Added a mock for panda.createUser in the manteca template test.
  • server/test/mocks/accounts.ts
    • Renamed server/test/mocks/keeper.ts to server/test/mocks/accounts.ts.
    • Updated the mock to include allower and keeper from the new accounts module.
    • Configured allower mock to resolve with an allow function.
  • server/test/utils/accounts.test.ts
    • Renamed server/test/utils/keeper.test.ts to server/test/utils/accounts.test.ts.
    • Updated imports to reference the new accounts mock and module.
    • Updated type imports to use @exactly/common/validation for Hash.
  • server/test/utils/gcp.test.ts
    • Added a new test file to verify GCP credentials security, including file creation with secure permissions and early return for existing credentials.
  • server/utils/accounts.ts
    • Added a new file to define keeper and allower wallet clients.
    • Implemented extender function to add poke functionality to the wallet client, handling ETH and ERC20 asset poking based on balances.
    • Implemented withExaSend function for robust transaction sending with Sentry tracing, error handling, and nonce management.
    • Integrated GCP KMS for the allower client, including credential initialization and retry logic for KMS errors.
    • Defined getAccount to retrieve a local account from GCP HSM.
  • server/utils/gcp.ts
    • Added a new file to manage GCP credentials, including base64 decoding and secure file writing.
    • Implemented initializeGcpCredentials to ensure GCP service account credentials are set up securely.
    • Added hasCredentials to check for the existence of the credentials file.
    • Provided isRetryableKmsError to identify transient KMS errors for retry mechanisms.
  • server/utils/keeper.ts
    • Removed the file, as its functionality has been refactored into server/utils/accounts.ts.
  • server/vitest.config.mts
    • Added GCP KMS related environment variables for Vitest testing.
Activity
  • The pull request introduces new functionality and refactors existing code, indicating active development.
  • New changeset files were added, suggesting that these changes are intended for release notes.
  • Extensive test updates and additions were made, reflecting thorough testing of the new allower and refactored accounts modules.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@sentry
Copy link
Copy Markdown

sentry bot commented Feb 25, 2026

Codecov Report

❌ Patch coverage is 70.81545% with 68 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.41%. Comparing base (8f7fa82) to head (9f8058b).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/utils/accounts.ts 66.43% 32 Missing and 16 partials ⚠️
server/hooks/activity.ts 68.29% 9 Missing and 4 partials ⚠️
server/utils/gcp.ts 85.29% 3 Missing and 2 partials ⚠️
server/hooks/persona.ts 86.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #839      +/-   ##
==========================================
- Coverage   71.59%   71.41%   -0.19%     
==========================================
  Files         227      228       +1     
  Lines        8217     8304      +87     
  Branches     2626     2644      +18     
==========================================
+ Hits         5883     5930      +47     
- Misses       2106     2135      +29     
- Partials      228      239      +11     
Flag Coverage Δ
e2e 51.98% <43.77%> (-0.37%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@aguxez aguxez marked this pull request as ready for review February 25, 2026 23:38
greptile-apps[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@aguxez aguxez force-pushed the gcp-allower branch 2 times, most recently from c642d46 to 88aa6aa Compare March 2, 2026 12:51
sentry[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

greptile-apps[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 3, 2026

Additional Comments (2)

server/utils/gcp.ts, line 37
Stale initializationPromise after credentials file deletion

initializationPromise is cached after the first successful initialization. If the credentials file is later removed at runtime (e.g., OS /tmp cleanup, systemd-tmpfiles, long-running container with volatile /tmp), every subsequent call to initializeGcpCredentials() returns the cached resolved promise without attempting to recreate the file.

Downstream in getAccount():

await initializeGcpCredentials();       // returns cached no-op promise
if (!(await hasCredentials())) throw   // file is gone → always throws

The server cannot recover without a full restart. The resetGcpInitialization() export exists but is never called automatically, leaving allower() unable to recover from runtime credential file loss.

Suggested fix: Call resetGcpInitialization() inside getAccount() if hasCredentials() returns false after initialization resolves, allowing the next attempt to re-decode and re-write the credentials file:

export async function getAccount(): Promise<LocalAccount> {
  await initializeGcpCredentials();
  if (!(await hasCredentials())) {
    resetGcpInitialization(); // allow next caller to retry
    throw new Error();
  }
  
}

server/utils/accounts.ts, line 63
Module-level GCP validation breaks keeper when GCP is unavailable

These GCP env-var checks run at module import time, before any GCP-dependent code is invoked. keeper (defined at line 65) is a plain private-key wallet client with no GCP dependency. Any hook that imports keeper (activity.ts, block.ts, panda.ts, etc.) will now fail to start if GCP_PROJECT_ID, GCP_KMS_KEY_RING, or GCP_KMS_KEY_VERSION are absent — even in environments or unit tests where the allower is not used.

This breaks the deployment of unrelated features if GCP credentials are not configured, and makes testing hooks that use keeper harder in isolated environments.

Suggested fix: Move the GCP env-var validation inside getAccount(), which is the only function that actually requires them:

export async function getAccount(): Promise<LocalAccount> {
  if (!process.env.GCP_PROJECT_ID) throw new Error("GCP_PROJECT_ID is required");
  const projectId = process.env.GCP_PROJECT_ID;
  if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(projectId)) {
    throw new Error("GCP_PROJECT_ID must be a valid GCP project ID format");
  }
  // … validate GCP_KMS_KEY_RING and GCP_KMS_KEY_VERSION here too
  await initializeGcpCredentials();
  
}

This keeps keeper importable regardless of GCP configuration and surfaces the error at the point of use.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 3, 2026

Additional Comments (3)

server/utils/accounts.ts, line 161
Bitwise left-shift overflow in retry delay

1 << count uses a 32-bit signed integer operation. For count >= 31, the result wraps to a negative number (1 << 31 = -2147483648), which would produce a large negative delay and cause immediate (potentially infinite) retries without any back-off. With retryCount: 10 the current cap of count = 9 avoids the overflow, but this is a silent footgun if retryCount is ever raised.

Prefer the explicit exponentiation operator instead:

            delay: ({ count }) => Math.trunc(2 ** count) * 60,

server/utils/gcp.ts, line 4
DECODING_ITERATIONS magic number is undocumented

The triple base64 decode (DECODING_ITERATIONS = 3) is an unusual encoding strategy that will silently produce a corrupted credentials file if GCP_BASE64_JSON is encoded with a different iteration count (e.g., a single base64 encode as would be typical). There is no comment explaining why three iterations are required, and the only clue is hidden in the config-level test value.

Consider adding a comment to the constant (and possibly the app.yaml instructions) documenting the expected encoding:

// Credentials are stored triple-base64-encoded (base64(base64(base64(json)))) to survive
// shell/environment variable escaping layers. Ensure GCP_BASE64_JSON is encoded accordingly.
const DECODING_ITERATIONS = 3;

server/utils/accounts.ts, line 72
keeper onFetchRequest handler lacks error handling unlike allower

The allower client's onFetchRequest wraps captureRequests in a try/catch and logs failures with captureMessage (lines 385-394). The keeper client's handler does not — if clone.json() or parse(Requests, …) throws (e.g., malformed RPC batch payload), the error propagates out of the transport hook and can unexpectedly fail the entire RPC call.

      async onFetchRequest(request) {
        try {
          const clone = request.clone();
          captureRequests(parse(Requests, await clone.json()));
        } catch (error: unknown) {
          captureMessage("failed to parse or capture rpc requests", {
            level: "error",
            extra: { error },
          });
        }
      },

devin-ai-integration[bot]

This comment was marked as resolved.

@aguxez aguxez force-pushed the gcp-allower branch 2 times, most recently from a4d043e to e5dca00 Compare March 10, 2026 11:17
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5dca00480

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +297 to +304
keeper
.poke(account, {
notification: {
headings: { en: "Account assets updated" },
contents: { en: "Your funds are ready to use" },
},
})
.catch((error: unknown) => captureException(error, { level: "error" }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move poke after user creation succeeds

keeper.poke is triggered before createUser and before persisting pandaId, so transient onboarding failures can cause Persona webhook retries to run poke repeatedly for the same inquiry. Because this call may submit on-chain pokes and sends the "Account assets updated" notification, retries can produce duplicate transactions/notifications even though onboarding did not complete; this side effect should run only after successful user creation is persisted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f7769a467

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +121 to +123
if (result.status === "rejected") {
captureException(result.reason, { level: "error" });
return [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not drop rejected balance reads in poke

When a getBalance/balanceOf call fails, this branch captures the exception and returns [], which silently removes that asset from assetsToPoke. In the presence of transient RPC failures, a funded market can be skipped while poke still resolves as success, so callers have no signal to retry and the account can miss activation/yield on that asset until a later unrelated trigger.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

View 17 additional findings in Devin Review.

Open in Devin Review

Comment on lines +26 to +30
let json = gcpBase64Json;
for (let index = 0; index < DECODING_ITERATIONS; index++) {
json = Buffer.from(json, "base64").toString("utf8");
}
await writeFile(GOOGLE_APPLICATION_CREDENTIALS, json, { mode: 0o600 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 GCP credentials written to filesystem violates "secrets never in files" rule

AGENTS.md/CLAUDE.md explicitly states: "production secrets are environment variables at runtime only - never in files." The new gcp.ts decodes GCP_BASE64_JSON and writes the GCP service account JSON to /tmp/gcp-service-account.json, which is then used by KeyManagementServiceClient via keyFilename in server/utils/accounts.ts:361-363. The @google-cloud/kms KeyManagementServiceClient supports a credentials option that accepts a parsed JSON object directly in memory, eliminating the need to write secrets to the filesystem. This would avoid both the rule violation and the security risk of persisting credentials on disk.

Prompt for agents
In server/utils/gcp.ts, instead of writing the decoded GCP credentials JSON to a file at /tmp/gcp-service-account.json, export the parsed credentials object directly. Replace the file-write logic (lines 21-33) with a function that returns the parsed JSON object. Then in server/utils/accounts.ts lines 361-363, replace `keyFilename: GOOGLE_APPLICATION_CREDENTIALS` with `credentials: getGcpCredentials()` (passing the parsed JSON object directly to the KeyManagementServiceClient constructor). This avoids writing production secrets to the filesystem, which is explicitly prohibited by the project rules. The @google-cloud/kms KeyManagementServiceClient constructor accepts a `credentials` option that takes { client_email, private_key } directly.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const account = await withRetry(
() =>
gcpHsmToAccount({
hsmKeyVersion: `projects/${projectId}/locations/us-west2/keyRings/${keyRing}/cryptoKeys/allower/cryptoKeyVersions/${version}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 GCP KMS location us-west2 hardcoded while other KMS path components are parameterized

In the KMS key version path at server/utils/accounts.ts:360, the GCP location is hardcoded as us-west2 while project ID, key ring, and version are all configurable via environment variables (GCP_PROJECT_ID, GCP_KMS_KEY_RING, GCP_KMS_KEY_VERSION). If the KMS key ring is provisioned in a different region (e.g., for a staging environment or disaster recovery), the code will fail to find the key and must be changed instead of just updating an environment variable. This inconsistency makes the configuration fragile.

Prompt for agents
In server/utils/accounts.ts, add a new environment variable (e.g., GCP_KMS_LOCATION) alongside the existing GCP_PROJECT_ID, GCP_KMS_KEY_RING, and GCP_KMS_KEY_VERSION checks at the top of the file (~line 53-63). Then use it in the hsmKeyVersion template string at line 360 to replace the hardcoded 'us-west2'. Also add the new env var to .do/app.yaml, server/vitest.config.mts, and server/script/openapi.ts for consistency with the other GCP config values.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1 to 5
import "../mocks/accounts";
import "../mocks/auth";
import "../mocks/deployments";
import "../mocks/keeper";
import "../mocks/onesignal";
import "../mocks/pax";
import "../mocks/persona";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 onesignal mock removed from card.test.ts

card.test.ts previously imported ../mocks/onesignal which mocked sendPushNotification to a no-op. This import was removed in the PR. The card API at server/api/card.ts:33,383 imports and calls sendPushNotification. If any test path in card.test.ts triggers a notification (e.g., card mode changes), the real onesignal module would be used. Since the test env has no ONESIGNAL_API_KEY, the call might fail silently (caught by .catch()) or cause unexpected behavior. The tests presumably still pass in CI, suggesting notification paths aren't triggered in the current test cases, but this removal reduces test isolation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddbd0ed187

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +324 to +325
keeper
.poke(account, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Run post-kyc poke outside firewall gating

The keeper.poke(...) side effect is now executed only inside the if (firewallAddress) block, so in environments where firewallAddress is unset (a case this handler already accounts for), approved Persona inquiries will skip poking entirely. That leaves existing funded balances unactivated for yield until some later unrelated trigger runs poke, which is a functional gap in the onboarding flow.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73ad6d7111

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

columns: { id: true, mode: true },
where: inArray(cards.status, ["ACTIVE", "FROZEN"]),
await keeper
.poke(account, { ignore: [`NotAllowed(${account})`] })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not claim yield when NotAllowed skipped the poke

In activity, this new ignore: [\NotAllowed(${account})`]path means firewall-gated deposits can now skipkeeper.poke` entirely, but the push notification emitted a few lines earlier still says the funds "instantly started earning yield" whenever the asset has a market. For pre-KYC users on deployments with the firewall enabled, that message becomes false: the transfer was received, but no market entry happened until a later Persona approval.

Useful? React with 👍 / 👎.

contents: { en: "Your funds are ready to use" },
},
})
.catch((error: unknown) => captureException(error, { level: "error" }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface post-kyc poke failures so Persona can retry

This fire-and-forget keeper.poke rejection handler turns post-KYC activation failures into a logged 200 response. If poke rejects before its internal per-asset catches run (for example while loading exaPreviewer.assets or reading balances), Persona will not retry the webhook, so accounts that were waiting on KYC can remain unpoked and not earning until some unrelated later activity happens.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 18 additional findings in Devin Review.

Open in Devin Review

Comment on lines +166 to +170
return r.flatMap((result) => {
if (result.status === "rejected") {
captureException(result.reason, { level: "error" });
return [];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 poke function double-reports errors: once in exaSend and again in Promise.allSettled handler

Inside the poke function, when an exaSend call fails with a non-ignored error and withRetry exhausts all 10 retries, the error is reported to Sentry twice per final failure: once inside exaSend's catch block (server/utils/accounts.ts:336) with proper fingerprinting via revertFingerprint, and once more in poke's Promise.allSettled handler (server/utils/accounts.ts:168) without fingerprinting. Additionally, each of the 10 retry attempts also triggers exaSend's internal error reporting. This leads to 11 Sentry error events per failed poke asset (10 from retries + 1 from the handler), with the final one being a duplicate lacking fingerprint context.

Prompt for agents
In server/utils/accounts.ts, inside the poke function's Promise.allSettled handler (lines 165-174), the rejected result handler calls captureException again after exaSend has already reported the error internally with fingerprinting. To avoid duplicate reports, either:

1. Pass { level: false } to exaSend's options to suppress its internal reporting, and keep the handler's captureException (but add fingerprinting). OR
2. Remove the captureException from the Promise.allSettled handler (lines 167-169) since exaSend already reports the error, changing the rejected branch to just return [].

Option 2 is simpler: in the flatMap callback for rejected results (line 167-169), remove the captureException call so the block just returns []. The error was already reported by exaSend's internal catch handler with proper fingerprinting.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@aguxez aguxez force-pushed the gcp-allower branch 2 times, most recently from 4f4d674 to 1216f5f Compare March 27, 2026 14:10
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 18 additional findings in Devin Review.

Open in Devin Review

Comment on lines +4 to +37
const DECODING_ITERATIONS = 3;
export const GOOGLE_APPLICATION_CREDENTIALS = "/tmp/gcp-service-account.json";

if (!process.env.GCP_BASE64_JSON) throw new Error("GCP_BASE64_JSON is required when using GCP KMS");
const gcpBase64Json = process.env.GCP_BASE64_JSON;

let initializationPromise: null | Promise<void> = null;

export function resetGcpInitialization() {
initializationPromise = null;
}

export async function initializeGcpCredentials() {
if (initializationPromise) {
return initializationPromise;
}

initializationPromise = (async () => {
if (await hasCredentials()) {
return;
}

let json = gcpBase64Json;
for (let index = 0; index < DECODING_ITERATIONS; index++) {
json = Buffer.from(json, "base64").toString("utf8");
}
await writeFile(GOOGLE_APPLICATION_CREDENTIALS, json, { mode: 0o600 });
})().catch((error: unknown) => {
initializationPromise = null;
throw error;
});

return initializationPromise;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 GCP credentials triple base64 decoding is intentional but fragile

The DECODING_ITERATIONS = 3 constant at server/utils/gcp.ts:4 means GCP_BASE64_JSON must be triple base64-encoded. This is presumably an obfuscation layer for the CI/CD pipeline. If the encoding depth changes without updating this constant, the decoded JSON would be garbage and KeyManagementServiceClient initialization would fail at runtime. There's no validation that the decoded result is valid JSON before writing to disk at line 30. The initializationPromise caching at line 10-36 means a corrupted write would be cached as success, requiring a process restart to recover.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 18 additional findings in Devin Review.

Open in Devin Review

Comment on lines +164 to +194
await keeper
.poke(account, { ignore: [`NotAllowed(${account})`] })
.catch((error: unknown) => captureException(error, { level: "error" }));
autoCredit(account)
.then(async (auto) => {
span.setAttribute("exa.autoCredit", auto);
if (!auto) return;
const credential = await database.query.credentials.findFirst({
where: eq(credentials.account, account),
columns: {},
with: {
cards: {
columns: { id: true, mode: true },
where: inArray(cards.status, ["ACTIVE", "FROZEN"]),
},
},
},
});
if (!credential || credential.cards.length === 0) return;
const card = credential.cards[0];
span.setAttribute("exa.card", card?.id);
if (card?.mode !== 0) return;
await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id));
span.setAttribute("exa.mode", 1);
sendPushNotification({
userId: account,
headings: { en: "Card mode changed" },
contents: { en: "Credit mode activated" },
}).catch((error: unknown) => captureException(error));
})
.catch((error: unknown) => captureException(error));
span.setStatus({ code: SPAN_STATUS_OK });
},
});
if (!credential || credential.cards.length === 0) return;
const card = credential.cards[0];
span.setAttribute("exa.card", card?.id);
if (card?.mode !== 0) return;
await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id));
span.setAttribute("exa.mode", 1);
sendPushNotification({
userId: account,
headings: { en: "Card mode changed" },
contents: { en: "Credit mode activated" },
}).catch((error: unknown) => captureException(error, { level: "error" }));
})
.catch((error: unknown) => captureException(error, { level: "error" }));
span.setStatus({ code: SPAN_STATUS_OK });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Activity span always reports OK, masking poke failures

In the old code, when individual pokes failed with non-NoBalance() errors, the span status was set to SPAN_STATUS_ERROR and the error was re-thrown — which caused the outer Promise.allSettled at line 206 to record a rejected result, ultimately setting the webhook's overall status to activity_failed. In the new code, keeper.poke() errors are unconditionally swallowed by the .catch() at line 166. Execution then falls through to span.setStatus({ code: SPAN_STATUS_OK }) at line 194, meaning the per-account span always reports success. Because every account span now resolves successfully, the outer Promise.allSettled at server/hooks/activity.ts:207-211 always sees all results as fulfilled, so the webhook-level span is also set to SPAN_STATUS_OK. This eliminates trace-level visibility into poke failures.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

server: poke account after allowing it in the firewall

2 participants