Shortened gift token from UUID to 8-char base64url for cleaner URLs#27230
Conversation
ref https://linear.app/ghost/issue/BER-3484 Uses `crypto.randomBytes(6).toString('base64url')` which gives 48 bits of entropy encoded into 8 URL-safe characters
WalkthroughThe gift payment link generation in 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@ghost/core/core/server/services/members/members-api/services/payments-service.js`:
- Line 177: In recordPurchase(), wrap the DB create that uses the token
(currently assigned via crypto.randomBytes(6).toString('base64url')) in a retry
loop with try/catch to handle UNIQUE constraint violations: on a collision catch
the error, regenerate a larger token (e.g.,
crypto.randomBytes(8).toString('base64url')) or increase length progressively,
and retry the create up to a small max attempts (e.g., 3-5); if all attempts
fail rethrow or return a controlled error. Ensure the logic references the token
variable and the recordPurchase() create call, and keep the retry limit and
generation strategy documented in comments.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 76056984-804a-46e1-8870-4fd935685110
📒 Files selected for processing (2)
ghost/core/core/server/services/members/members-api/services/payments-service.jsghost/core/test/unit/server/services/members/members-api/services/payments-service.test.js
| const currency = tier.currency.toLowerCase(); | ||
|
|
||
| const token = crypto.randomUUID(); | ||
| const token = crypto.randomBytes(6).toString('base64url'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find gift token write/read paths =="
rg -nP --type=js --type=ts -C3 '\bgift_token\b|\btoken\b' ghost/core/core/server/services/gifts ghost/core/core/server/services/stripe ghost/core/core/server/services/members || true
echo
echo "== Find schema/migrations touching gift token uniqueness =="
fd -i "gift" ghost/core/core/server/data || true
rg -nP --type=js --type=ts -C3 'gift|gifts|token|unique|index' ghost/core/core/server/data || trueRepository: TryGhost/Ghost
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Gift table schema/migrations =="
fd -type f \( -name "*migration*" -o -name "*schema*" \) ghost/core/core/server/data | head -20
echo
echo "== Look for gift table definition =="
rg -nP --type=js --type=ts "create.*gift|alter.*gift|token.*unique|unique.*token" ghost/core/core/server/data --max-count=30
echo
echo "== Check gift table model definition =="
fd -type f -name "*.js" -o -name "*.ts" | xargs rg -l "gift" ghost/core/core/server/models || trueRepository: TryGhost/Ghost
Length of output: 280
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for gift recordPurchase error handling =="
rg -nP --type=ts -A15 "recordPurchase" ghost/core/core/server/services/gifts/gift-service.ts
echo
echo "== Check for duplicate/collision handling in create =="
rg -nP --type=ts -B5 -A10 "async create" ghost/core/core/server/services/gifts/gift-bookshelf-repository.tsRepository: TryGhost/Ghost
Length of output: 1356
🏁 Script executed:
#!/bin/bash
set -euo npipefail
echo "== Find gifts migrations =="
find ghost/core -name "*migration*" -type f | xargs grep -l "gift" 2>/dev/null || true
echo
echo "== Look for knex schema builder patterns for gifts =="
rg -nP "createTable.*gift|table\.string.*token|table\.unique" ghost/core --max-count=50 || trueRepository: TryGhost/Ghost
Length of output: 112
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find gifts migrations in data/migrations =="
find ghost/core/core/server/data -name "*.js" -type f 2>/dev/null | xargs grep -l "gift" 2>/dev/null | head -10
echo
echo "== Look for gifts table in schema =="
rg -nP "gift" ghost/core/core/server/data/schema --max-count=50 || trueRepository: TryGhost/Ghost
Length of output: 718
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check if recordPurchase wraps create in try-catch =="
rg -nP --type=ts -A30 "async recordPurchase" ghost/core/core/server/services/gifts/gift-service.ts | head -50
echo
echo "== Look for token uniqueness in bookshelf repository or model =="
rg -nP "unique|token.*index|UNIQUE" ghost/core/core/server/services/gifts/ --max-count=20 || trueRepository: TryGhost/Ghost
Length of output: 1386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for gifts Bookshelf model file =="
find ghost/core -path "*/models/*" -name "*gift*" 2>/dev/null
echo
echo "== Check core/server/models for gift definition =="
fd -type f "gift" ghost/core/core/server/models 2>/dev/null || trueRepository: TryGhost/Ghost
Length of output: 191
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Schema definition for gifts table =="
rg -nP -A25 "^\s+gifts:\s+{" ghost/core/core/server/data/schema/schema.js
echo
echo "== Gift model definition =="
head -50 ghost/core/core/server/models/gift.jsRepository: TryGhost/Ghost
Length of output: 2198
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Gift table migration =="
cat ghost/core/core/server/data/migrations/versions/6.27/2026-04-06-07-48-06-add-gifts-table.jsRepository: TryGhost/Ghost
Length of output: 1645
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check recordPurchase completion - error handling after create =="
rg -nP --type=ts -A50 "await this.giftRepository.create" ghost/core/core/server/services/gifts/gift-service.tsRepository: TryGhost/Ghost
Length of output: 2107
Add error handling for token collision during gift recording.
randomBytes(6) produces 2^48 possible tokens. While the schema enforces uniqueness (token column is UNIQUE), the recordPurchase() method has no try-catch around the create operation. If a token collision occurs (extremely unlikely initially but possible at scale), the database constraint violation propagates uncaught, causing the webhook to fail. Add collision error handling with retry logic using a larger token or implement token expiration to bound the collision space.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@ghost/core/core/server/services/members/members-api/services/payments-service.js`
at line 177, In recordPurchase(), wrap the DB create that uses the token
(currently assigned via crypto.randomBytes(6).toString('base64url')) in a retry
loop with try/catch to handle UNIQUE constraint violations: on a collision catch
the error, regenerate a larger token (e.g.,
crypto.randomBytes(8).toString('base64url')) or increase length progressively,
and retry the create up to a small max attempts (e.g., 3-5); if all attempts
fail rethrow or return a controlled error. Ensure the logic references the token
variable and the recordPurchase() create call, and keep the retry limit and
generation strategy documented in comments.
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 24140723166 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
1 similar comment
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 24140723166 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 24140723166 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
…ryGhost#27230) ref https://linear.app/ghost/issue/BER-3484 Uses `crypto.randomBytes(6).toString('base64url')` which gives 48 bits of entropy encoded into 8 URL-safe characters



ref https://linear.app/ghost/issue/BER-3484
Uses
crypto.randomBytes(6).toString('base64url')which gives 48 bits of entropy encoded into 8 URL-safe characters