Skip to content

Feat/db config onboarding - #63

Merged
thedevyashsaini merged 8 commits into
mainfrom
feat/db-config-onboarding
May 30, 2026
Merged

Feat/db config onboarding#63
thedevyashsaini merged 8 commits into
mainfrom
feat/db-config-onboarding

Conversation

@thedevyashsaini

@thedevyashsaini thedevyashsaini commented May 29, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added a config status API endpoint to report persisted setup and masked secrets.
    • Persist and return payment provider settings, currency, and redirect URL; credentials are stored encrypted.
    • Added an encryption utility for metadata.
  • Refactor

    • Payment provider now loads config and credentials from stored metadata (with client caching and explicit cache clearing).
    • Updated templates to document ClickHouse config placement.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thedevyashsaini, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 57 minutes and 10 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f55f845a-8130-4434-aa90-fd814612fc65

📥 Commits

Reviewing files that changed from the base of the PR and between 7793e74 and b547714.

📒 Files selected for processing (5)
  • src/routes/gRPC/payment/paymentProvider.ts
  • src/storage/db/postgres/helpers/metadata.ts
  • src/storage/db/postgres/schema.ts
  • src/utils/encryptMetadata.ts
  • src/zod/internals.ts
📝 Walkthrough

Walkthrough

Encrypted Dodo credentials are added to metadata, with AES-256-GCM helpers, schema columns, selective upsert/decrypt, async metadata-backed Dodo client initialization with caching/clear, onboarding persistence updates, a GET /api/v1/internals/config endpoint, and small wiring/await fixes in checkout/webhook flows.

Changes

Dodo Configuration Management

Layer / File(s) Summary
Encryption infrastructure and database schema
src/utils/encryptMetadata.ts, src/storage/db/postgres/schema.ts
AES-256-GCM encrypt/decrypt and isEncrypted helpers; metadataTable extended with dodo_live_api_key, dodo_test_api_key, dodo_product_id, dodo_webhook_secret, currency, and redirect_url.
Metadata storage with encryption support
src/storage/db/postgres/helpers/metadata.ts
UpsertMetadataInput made optional for fields; upsertMetadata selectively encrypts and persists provided Dodo fields; getMetadata returns decrypted record or undefined, tolerant of plaintext.
Configuration validation schema
src/zod/internals.ts
onboardingCronSchema adds optional Dodo fields (dodoLiveApiKey, dodoTestApiKey, dodoProductId, dodoWebhookSecret) plus optional currency and redirectUrl (validated as URL).
Async payment provider initialization
src/routes/gRPC/payment/paymentProvider.ts
getDodoClient and getPaymentProviderConfig become async and read from metadata; clients are cached and clearClients() exported; checkout creation awaits the async client.
Onboarding and config endpoint
src/routes/http/api/onboarding.ts, src/routes/http/api/registerApiRoutes.ts
Onboarding POST persists Dodo config fields (normalized) and calls clearClients(); new exported handleGetConfig authenticates, returns masked config or { configured: false }, handles errors/Sentry, and is registered at GET /api/v1/internals/config.
Async integration in checkout/webhook flows
src/routes/gRPC/payment/createCheckoutLink.ts, src/routes/http/createdCheckout.ts
createCheckoutLink and webhook handler now await the async provider config/client calls.
Environment template reorganization
.env.example
CLICKHOUSE_URL moved to a dedicated ClickHouse section with note that omitting it keeps Postgres-only mode.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ScrawnDotDev/Scrawn#61: Related to webhook forwarding and webhook plumbing touching createdCheckout webhook flows.
  • ScrawnDotDev/Scrawn#50: Overlaps changes to the gRPC createCheckoutLink handler and related checkout flow.
  • ScrawnDotDev/Scrawn#55: Modifies metadata persistence and schema areas overlapping with these metadata/upsert changes.

Suggested reviewers

  • SteakFisher

Poem

🐰 In burrows of code I quietly creep,
I hide the keys where secrets sleep,
Async waits patrolling the trail,
Metadata whispers without fail,
Dodo payments sleep sound and deep.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/db config onboarding' is directly related to the main changes: database schema extensions for metadata storage, onboarding API enhancements for configuration persistence, and payment provider configuration management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/db-config-onboarding

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
src/routes/gRPC/payment/paymentProvider.ts (1)

72-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid sending an empty return_url to Dodo when redirect_url is unset.

getPaymentProviderConfig() sets returnUrl to "" when metadata?.redirect_url is missing, and createProviderCheckout() always passes return_url: config.returnUrl. Dodo’s docs define return_url as a redirect destination (nullable string | null) and do not document "" as a supported value—empty strings can trigger validation/unexpected checkout behavior. Prefer omitting return_url from the payload (or sending null) when no redirect URL is configured.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/gRPC/payment/paymentProvider.ts` around lines 72 - 83,
getPaymentProviderConfig currently sets returnUrl to an empty string which then
gets sent to Dodo; change getPaymentProviderConfig to return returnUrl as null
(or undefined) instead of "" when metadata.redirect_url is unset, update the
PaymentProviderConfig type if needed to allow returnUrl?: string | null, and
modify createProviderCheckout to omit return_url from the outgoing Dodo payload
(or explicitly send null) when config.returnUrl is null/undefined so an empty
string is never sent.
🧹 Nitpick comments (3)
src/storage/db/postgres/helpers/metadata.ts (1)

26-30: ⚡ Quick win

Don't silently swallow decryption failures.

The empty catch treats every failure the same way. Migration-era plaintext is the intended case, but a genuinely encrypted value that fails to decrypt (secret rotation, corruption, tampering) is also silently passed through — the raw ciphertext then flows downstream as if it were the credential, with no signal. At minimum, log the failure so these cases are observable.

Suggested change
         try {
           (result as Record<string, unknown>)[field] = decrypt(value);
-        } catch {
-          // leave as-is (e.g. plaintext from migration)
+        } catch (e) {
+          // Tolerate plaintext (e.g. from migration) but surface genuine failures.
+          logger.lifecycleWarning(/* decryption failed for field */);
         }

As per coding guidelines: "Use the WideEventLogger from errors/logger".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/db/postgres/helpers/metadata.ts` around lines 26 - 30, The catch
block swallowing decryption errors should instead log the failure using the
WideEventLogger from errors/logger so genuine decryption problems are
observable; in the catch for the decrypt(value) call (referencing decrypt,
result, field, value) import and use WideEventLogger to emit an error/wide-event
that includes the field name and the caught error details (and any safe
contextual identifiers), then continue to leave the value as-is for
migration/plaintext cases as currently done.
src/utils/encryptMetadata.ts (1)

11-17: ⚡ Quick win

Derive the AES key via HKDF with domain separation from HMAC_SECRET
src/utils/encryptMetadata.ts derives the AES-256-GCM key as SHA-256(HMAC_SECRET), but HMAC_SECRET is also used for HMAC hashing in src/utils/hashAPIKey.ts. Use HKDF with a distinct info label (e.g., for metadata encryption) to derive independent keys for each cryptographic purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/encryptMetadata.ts` around lines 11 - 17, The current assertSecret
function derives the AES key by hashing HMAC_SECRET; instead derive a distinct
32-byte AES-256-GCM key from process.env.HMAC_SECRET using HKDF with a clear
domain-separating info label (e.g., "metadata-encryption") so it is independent
from the HMAC key in hashAPIKey.ts; update the assertSecret function to validate
HMAC_SECRET, then call Node's crypto.hkdfSync (or hkdf) with a zero or
appropriate salt, the secret as IKM, the info string for metadata encryption,
and output length 32, and return that derived key Buffer (replace the createHash
usage and keep the same function name assertSecret).
src/zod/internals.ts (1)

59-59: ⚡ Quick win

Zod 4: use top-level z.url() for redirectUrl
In Zod 4, the chained validator z.string().url() is deprecated in favor of the standalone z.url() schema.

♻️ Proposed change
-  redirectUrl: z.string().url("Redirect URL must be a valid URL").optional(),
+  redirectUrl: z.url("Redirect URL must be a valid URL").optional(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/zod/internals.ts` at line 59, The redirectUrl schema uses the deprecated
chained form z.string().url(...); replace that with the Zod 4 top-level z.url()
keeping the .optional() and the same validation message (i.e., change the
expression from redirectUrl: z.string().url("Redirect URL must be a valid
URL").optional() to using z.url(...).optional()), so update the redirectUrl
definition to use z.url() while preserving the existing error text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routes/http/api/onboarding.ts`:
- Around line 86-90: The masking function maskApiKey currently reveals 8
characters for any key longer than 8, which exposes too much for short secrets;
update maskApiKey so that it returns a fully masked value (e.g., "****") for
keys of length <= 16 to avoid revealing most of short secrets, and keep the
existing slice behavior only for keys longer than 16 (i.e., if key.length > 16
return key.slice(0,4) + "****" + key.slice(-4) else return "****").
- Around line 92-137: handleGetConfig is exposing internals without auth;
protect it by invoking the existing HTTP API key guard and wiring the route the
same way other internals endpoints do: call authenticateHttpApiKey at the start
of handleGetConfig (or add it as a preHandler hook when registering the route)
so requests are validated before returning config, and ensure errors from failed
authentication short-circuit with the same response pattern used by other
routes; reference authenticateHttpApiKey and handleGetConfig to locate where to
add the check and mirror the auth pattern used by other internals routes in your
route registration logic.

In `@src/storage/db/postgres/helpers/metadata.ts`:
- Around line 105-119: upsertMetadata is vulnerable to race conditions because
it does a select-then-insert on metadataTable (existingMetadata / insertValues)
without a uniqueness constraint or locking; make the singleton write atomic by
using a deterministic singleton key and a database upsert or an advisory lock:
either (A) enforce a single-row invariant (add a fixed/deterministic id for the
singleton row and a unique constraint) and replace the select-then-insert with a
single INSERT ... ON CONFLICT(id) DO UPDATE using metadataTable and insertValues
inside txn, or (B) acquire a Postgres advisory lock (pg_advisory_xact_lock) at
the start of upsertMetadata before reading existingMetadata and inserting to
ensure only one concurrent creator proceeds; update the upsertMetadata
implementation (and any schema migration to add the unique/partial index if you
choose option A) and keep getMetadata calls unchanged.

In `@src/storage/db/postgres/schema.ts`:
- Line 258: The redirect_url column currently defaults to
"http://localhost:3000", which allows production redirects to land on localhost;
remove that unsafe hardcoded default and instead supply a safe default from
configuration or require the caller to provide it: update the schema's
redirect_url definition to not default to localhost (either make it nullable or
default to a value read from an environment/config var), and modify
upsertMetadata (the function that checks input.redirect_url !== undefined) to
set input.redirect_url to a configured DEFAULT_REDIRECT_URL when undefined; also
ensure the onboarding flow that passes validated.redirectUrl either always
validates/provides a value or falls back to the same configured default before
calling upsertMetadata.

In `@src/utils/encryptMetadata.ts`:
- Line 14: Replace plain Error throws in assertSecret and decrypt with a
project-style custom domain error (e.g., MetadataEncryptionError) that includes
an error type, message, and optional originalError; implement static factory
methods like MetadataEncryptionError.missingSecret(message) and
.decryptionFailed(message, originalError) and use those in assertSecret and
decrypt so the thrown error carries type, descriptive message and the original
underlying exception where applicable.

---

Outside diff comments:
In `@src/routes/gRPC/payment/paymentProvider.ts`:
- Around line 72-83: getPaymentProviderConfig currently sets returnUrl to an
empty string which then gets sent to Dodo; change getPaymentProviderConfig to
return returnUrl as null (or undefined) instead of "" when metadata.redirect_url
is unset, update the PaymentProviderConfig type if needed to allow returnUrl?:
string | null, and modify createProviderCheckout to omit return_url from the
outgoing Dodo payload (or explicitly send null) when config.returnUrl is
null/undefined so an empty string is never sent.

---

Nitpick comments:
In `@src/storage/db/postgres/helpers/metadata.ts`:
- Around line 26-30: The catch block swallowing decryption errors should instead
log the failure using the WideEventLogger from errors/logger so genuine
decryption problems are observable; in the catch for the decrypt(value) call
(referencing decrypt, result, field, value) import and use WideEventLogger to
emit an error/wide-event that includes the field name and the caught error
details (and any safe contextual identifiers), then continue to leave the value
as-is for migration/plaintext cases as currently done.

In `@src/utils/encryptMetadata.ts`:
- Around line 11-17: The current assertSecret function derives the AES key by
hashing HMAC_SECRET; instead derive a distinct 32-byte AES-256-GCM key from
process.env.HMAC_SECRET using HKDF with a clear domain-separating info label
(e.g., "metadata-encryption") so it is independent from the HMAC key in
hashAPIKey.ts; update the assertSecret function to validate HMAC_SECRET, then
call Node's crypto.hkdfSync (or hkdf) with a zero or appropriate salt, the
secret as IKM, the info string for metadata encryption, and output length 32,
and return that derived key Buffer (replace the createHash usage and keep the
same function name assertSecret).

In `@src/zod/internals.ts`:
- Line 59: The redirectUrl schema uses the deprecated chained form
z.string().url(...); replace that with the Zod 4 top-level z.url() keeping the
.optional() and the same validation message (i.e., change the expression from
redirectUrl: z.string().url("Redirect URL must be a valid URL").optional() to
using z.url(...).optional()), so update the redirectUrl definition to use
z.url() while preserving the existing error text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ded03d0-c6b5-49da-8572-a3e4e8a7159d

📥 Commits

Reviewing files that changed from the base of the PR and between b510af2 and 42da5a6.

📒 Files selected for processing (10)
  • .env.example
  • src/routes/gRPC/payment/createCheckoutLink.ts
  • src/routes/gRPC/payment/paymentProvider.ts
  • src/routes/http/api/onboarding.ts
  • src/routes/http/api/registerApiRoutes.ts
  • src/routes/http/createdCheckout.ts
  • src/storage/db/postgres/helpers/metadata.ts
  • src/storage/db/postgres/schema.ts
  • src/utils/encryptMetadata.ts
  • src/zod/internals.ts

Comment thread src/routes/http/api/onboarding.ts
Comment thread src/routes/http/api/onboarding.ts
Comment thread src/storage/db/postgres/helpers/metadata.ts
Comment thread src/storage/db/postgres/schema.ts Outdated
Comment thread src/utils/encryptMetadata.ts Outdated
@thedevyashsaini
thedevyashsaini merged commit dee4622 into main May 30, 2026
2 of 3 checks passed
@SteakFisher
SteakFisher deleted the feat/db-config-onboarding branch June 6, 2026 18:36
@SteakFisher
SteakFisher restored the feat/db-config-onboarding branch June 6, 2026 18:36
@SteakFisher
SteakFisher deleted the feat/db-config-onboarding branch June 8, 2026 16:31
@SteakFisher
SteakFisher restored the feat/db-config-onboarding branch June 8, 2026 16:31
@SteakFisher
SteakFisher deleted the feat/db-config-onboarding branch June 8, 2026 16:31
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