From c2eb8bebbd90823ca6d56be4c52e124bda1d8ed5 Mon Sep 17 00:00:00 2001 From: Gogo-Eng <“progressgogochinda@gmail.com”> Date: Sat, 28 Mar 2026 03:48:06 +0100 Subject: [PATCH] docs: migrate documentation to MDX with syntax highlighting --- frontend/content/docs/api-guide.mdx | 170 ++++++++++++++++++ frontend/content/docs/hmac-signatures.mdx | 118 ++++++++++++ frontend/package.json | 4 +- .../src/app/(public)/docs/[slug]/page.tsx | 8 +- frontend/src/app/globals.css | 85 +++++++++ frontend/src/lib/docs.ts | 41 +++-- frontend/src/lib/mdx-config.ts | 12 ++ 7 files changed, 420 insertions(+), 18 deletions(-) create mode 100644 frontend/content/docs/api-guide.mdx create mode 100644 frontend/content/docs/hmac-signatures.mdx create mode 100644 frontend/src/lib/mdx-config.ts diff --git a/frontend/content/docs/api-guide.mdx b/frontend/content/docs/api-guide.mdx new file mode 100644 index 00000000..98942f8b --- /dev/null +++ b/frontend/content/docs/api-guide.mdx @@ -0,0 +1,170 @@ +# How to use the API + +This guide mirrors the routes currently implemented in the backend of this repository. + +## Base URL + +During local development, the frontend defaults to: + +```text +http://localhost:4000 +``` + +## 1. Register a merchant + +Create a merchant and receive both an API key and a webhook secret. + +**Endpoint** + +```http +POST /api/register-merchant +``` + +**Example** + +```bash +curl -X POST http://localhost:4000/api/register-merchant \ + -H "Content-Type: application/json" \ + -d '{ + "email": "merchant@example.com", + "business_name": "Example Store", + "notification_email": "ops@example.com" + }' +``` + +**Response fields to save** + +- `merchant.api_key` +- `merchant.webhook_secret` +- `merchant.id` + +## 2. Create a payment link + +All merchant-protected endpoints use the `x-api-key` header. + +**Endpoint** + +```http +POST /api/create-payment +``` + +**Headers** + +```text +x-api-key: sk_... +Content-Type: application/json +Idempotency-Key: 3f0d65e1-27b8-4b28-8f2f-8a6f9fd9d7d9 +``` + +`Idempotency-Key` is optional, but recommended. The backend caches successful duplicate requests for 24 hours. + +**Example** + +```bash +curl -X POST http://localhost:4000/api/create-payment \ + -H "Content-Type: application/json" \ + -H "x-api-key: sk_your_api_key" \ + -H "Idempotency-Key: 3f0d65e1-27b8-4b28-8f2f-8a6f9fd9d7d9" \ + -d '{ + "amount": 25, + "asset": "XLM", + "recipient": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN", + "description": "Order #2048", + "webhook_url": "https://merchant.example/webhooks/stellar" + }' +``` + +**Typical success response** + +```json +{ + "payment_id": "6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f", + "payment_link": "http://localhost:3000/pay/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f", + "status": "pending", + "branding_config": { + "primary_color": "#5ef2c0", + "secondary_color": "#b8ffe2", + "background_color": "#050608" + } +} +``` + +## 3. Check payment status + +Use the public status endpoint to read the latest payment state. + +**Endpoint** + +```http +GET /api/payment-status/:id +``` + +**Example** + +```bash +curl http://localhost:4000/api/payment-status/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f +``` + +## 4. Verify the payment on Stellar + +After the customer submits payment, verify it against the Stellar network. + +**Endpoint** + +```http +POST /api/verify-payment/:id +``` + +If the payment is found, the API marks it as `confirmed`, stores the `tx_id`, emits the merchant socket event, and sends the webhook. + +**Example** + +```bash +curl -X POST http://localhost:4000/api/verify-payment/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f +``` + +## 5. List merchant payments + +Read recent payments for the authenticated merchant. + +**Endpoint** + +```http +GET /api/payments?page=1&limit=10 +``` + +**Headers** + +```text +x-api-key: sk_... +``` + +## 6. Test webhook delivery + +If you already stored a webhook URL for the merchant, you can send a signed test event using: + +```http +POST /api/webhooks/test +``` + +If you want to ping an arbitrary URL directly, the repo also exposes: + +```http +POST /api/test-webhook +``` + +with: + +```json +{ + "webhook_url": "https://merchant.example/webhooks/stellar" +} +``` + +## Notes + +- Merchant auth in this codebase uses `x-api-key`, not `Authorization: Bearer ...`. +- The create-payment flow supports optional `webhook_url`, `memo`, `memo_type`, and `branding_overrides`. +- Webhook events are signed with the merchant webhook secret using `HMAC-SHA256`. + +Continue with the HMAC guide in `/docs/hmac-signatures` to verify those webhook requests correctly. diff --git a/frontend/content/docs/hmac-signatures.mdx b/frontend/content/docs/hmac-signatures.mdx new file mode 100644 index 00000000..4c4ad135 --- /dev/null +++ b/frontend/content/docs/hmac-signatures.mdx @@ -0,0 +1,118 @@ +# How to verify HMAC signatures + +Webhook signing in this repository lives in `backend/src/lib/webhooks.js`. + +## Header format + +When a webhook secret is available, the backend sends: + +```text +Stellar-Signature: sha256= +``` + +The digest is generated from the exact JSON string body using HMAC-SHA256. + +## Signing logic used by the backend + +The backend currently signs payloads with logic equivalent to: + +```js +import {createHmac} from "crypto"; + +const rawBody = JSON.stringify(payload); +const digest = createHmac("sha256", webhookSecret) + .update(rawBody) + .digest("hex"); + +const header = `sha256=${digest}`; +``` + +## Important verification rule + +Verify the signature against the **raw request body**, not a re-serialized object created later in your handler. + +If your framework parses JSON first and you rebuild it with a different key order or whitespace, the signature check can fail. + +## Node.js example + +```js +import crypto from "node:crypto"; +import express from "express"; + +const app = express(); + +app.post( + "/webhooks/stellar", + express.raw({type: "application/json"}), + (req, res) => { + const secret = process.env.STELLAR_WEBHOOK_SECRET; + const rawBody = req.body.toString("utf8"); + const incoming = req.get("Stellar-Signature") || ""; + + const expected = `sha256=${crypto + .createHmac("sha256", secret) + .update(rawBody) + .digest("hex")}`; + + const valid = + incoming.length === expected.length && + crypto.timingSafeEqual(Buffer.from(incoming), Buffer.from(expected)); + + if (!valid) { + return res.status(401).json({error: "Invalid webhook signature"}); + } + + const payload = JSON.parse(rawBody); + + if (payload.event === "payment.confirmed") { + console.log("Confirmed payment:", payload.payment_id, payload.tx_id); + } + + res.status(204).end(); + } +); +``` + +## Example payload fields + +The backend sends a `payment.confirmed` payload with fields like: + +```json +{ + "event": "payment.confirmed", + "payment_id": "6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f", + "amount": 25, + "asset": "XLM", + "asset_issuer": null, + "recipient": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN", + "tx_id": "stellar_tx_hash" +} +``` + +The test webhook route sends a similar payload with: + +- `event: "payment.confirmed"` +- `test: true` + +## Retry behavior + +If delivery fails, the backend schedules retries after: + +- 10 seconds +- 30 seconds +- 60 seconds + +Your webhook handler should therefore be: + +- idempotent +- fast to acknowledge +- tolerant of duplicate deliveries + +## Checklist + +- Save the `webhook_secret` returned during merchant registration. +- Read the raw request body before JSON parsing changes it. +- Compute `HMAC-SHA256` over that exact raw body. +- Compare against the `Stellar-Signature` header. +- Reject invalid signatures with `401`. +- Treat webhook events as retryable and idempotent. diff --git a/frontend/package.json b/frontend/package.json index 449fb8b2..8ca32969 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,9 +17,9 @@ "@walletconnect/types": "^2.23.8", "boring-avatars": "^2.0.4", "framer-motion": "^12.38.0", - "marked": "^17.0.5", "next": "^14.2.5", "next-intl": "^4.8.3", + "next-mdx-remote": "^5.0.0", "next-themes": "^0.4.6", "qrcode.react": "^4.2.0", "react": "^18.3.1", @@ -28,6 +28,8 @@ "react-loading-skeleton": "^3.5.0", "react-simple-pull-to-refresh": "^1.3.4", "recharts": "^2.15.4", + "rehype-prism-plus": "^2.0.0", + "remark-gfm": "^4.0.0", "socket.io-client": "^4.8.1", "stellar-sdk": "^12.2.0", "zustand": "^5.0.12" diff --git a/frontend/src/app/(public)/docs/[slug]/page.tsx b/frontend/src/app/(public)/docs/[slug]/page.tsx index 2d6e6397..2a05a878 100644 --- a/frontend/src/app/(public)/docs/[slug]/page.tsx +++ b/frontend/src/app/(public)/docs/[slug]/page.tsx @@ -1,4 +1,5 @@ import { notFound } from "next/navigation"; +import { MDXRemote } from "next-mdx-remote/rsc"; import { docsManifest } from "@/lib/docs-manifest"; import { getDocBySlug } from "@/lib/docs"; @@ -50,10 +51,9 @@ export default async function DocPage({

-
+
+ +
); } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5916c3d8..3ab15510 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -105,6 +105,91 @@ body { padding: 0.15rem 0.45rem; } +/* Syntax highlighting for code blocks with rehype-prism-plus */ +.docs-prose pre { + overflow-x: auto; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(2, 6, 23, 0.82); + border-radius: 1rem; + padding: 1rem 1.1rem; + margin: 1.25rem 0; +} + +.docs-prose pre code { + border: 0; + background: transparent; + color: #e2e8f0; + padding: 0; + font-size: 0.9em; + line-height: 1.5; +} + +/* prism-plus syntax highlighting tokens */ +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #7d8590; +} + +.token.punctuation { + color: #e2e8f0; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #ffa657; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #7ee787; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #79c0ff; +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #ff7b72; +} + +.token.function, +.token.class-name { + color: #d2a8ff; +} + +.token.regex, +.token.important, +.token.variable { + color: #ffa657; +} + +/* Line highlighting for code blocks */ +.docs-prose pre > code { + display: block; +} + +.token.line { + display: block; + min-height: 1.5em; +} + .docs-prose pre { overflow-x: auto; border: 1px solid rgba(255, 255, 255, 0.08); diff --git a/frontend/src/lib/docs.ts b/frontend/src/lib/docs.ts index 6c620adf..386bb14d 100644 --- a/frontend/src/lib/docs.ts +++ b/frontend/src/lib/docs.ts @@ -1,12 +1,10 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import { marked } from "marked"; +import { serialize } from "next-mdx-remote/serialize"; +import remarkGfm from "remark-gfm"; +import rehypePrismPlus from "rehype-prism-plus"; import { docsManifest } from "@/lib/docs-manifest"; -marked.setOptions({ - gfm: true, -}); - export async function getDocBySlug(slug: string) { const entry = docsManifest.find((doc) => doc.slug === slug); @@ -14,13 +12,30 @@ export async function getDocBySlug(slug: string) { return null; } - const filePath = path.join(process.cwd(), "content", "docs", entry.filename); - const markdown = await fs.readFile(filePath, "utf8"); - const html = await marked.parse(markdown); + // Updated to use .mdx extension + const mdxFilename = entry.filename.replace(".md", ".mdx"); + const filePath = path.join(process.cwd(), "content", "docs", mdxFilename); + + try { + const mdxContent = await fs.readFile(filePath, "utf8"); + + // Serialize MDX content with plugins for syntax highlighting and GFM support + const serialized = await serialize(mdxContent, { + mdxOptions: { + remarkPlugins: [remarkGfm], + rehypePlugins: [ + [rehypePrismPlus, { defaultLanguage: "bash", showLineNumbers: false }], + ], + }, + }); - return { - ...entry, - markdown, - html, - }; + return { + ...entry, + serialized, + filename: mdxFilename, + }; + } catch (error) { + console.error(`Error loading doc ${slug}:`, error); + return null; + } } diff --git a/frontend/src/lib/mdx-config.ts b/frontend/src/lib/mdx-config.ts new file mode 100644 index 00000000..3ca1c54b --- /dev/null +++ b/frontend/src/lib/mdx-config.ts @@ -0,0 +1,12 @@ +import remarkGfm from 'remark-gfm'; +import { MDXRemoteSerializeOptions } from 'next-mdx-remote/rsc'; +import rehypePrismPlus from 'rehype-prism-plus'; + +export const mdxOptions: MDXRemoteSerializeOptions = { + mdxOptions: { + remarkPlugins: [remarkGfm], + rehypePlugins: [ + [rehypePrismPlus, { defaultLanguage: 'bash' }], + ], + }, +};