Skip to main content

integrate-webhook

Implement and register a ramp WebhookProcessor in sdp-api with raw-request verification, replay protection, typed @sdp/payments parsing, and idempotent settlement.

跳到安装

来源信息

仓库
solana-foundation/solana-developer-platform
最近来源活动
2026年9月3日 10:11
检测到的 SKILL.md 语言
英语
星标
54
分支
25

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
integrate-webhook
description
Implement and register a ramp WebhookProcessor in sdp-api with raw-request verification, replay protection, typed @sdp/payments parsing, and idempotent settlement.
disable-model-invocation
true
# Integrate webhook Webhooks drive a transfer's lifecycle after the quote: `awaiting_payment → settling → completed | failed | expired`. Implement one `WebhookProcessor` in `apps/sdp-api/src/routes/webhooks/ramps/<id>.ts` using the interface in `processor.ts`: - `verify(context)` — verify the signature, return the parsed payload. - `parse(payload)` — pure wire-format → `RampSettlementEvent` mapping. - `process(c, environment, event)` — DB orchestration; calls `applyRampSettlementEvent`. Canonical examples: the registered processors in `apps/sdp-api/src/routes/webhooks/ramps/` — read the closest one before writing yours. ## Mount + flow Webhooks are **not** under `/v1`. They land at `POST /webhooks/payments/ramps/{sandbox|production}/:provider`. `parseRampWebhookProvider` accepts an id only after registration in `apps/sdp-api/src/routes/webhooks/handlers.ts`. The handler reads the raw body → verifies → parses → returns 2xx → runs `process` through `c.executionCtx.waitUntil`. ## verify `verify(context: RampWebhookValidationContext): Promise<Payload>`. `context` = `{ env, environment, headers, rawBody, requestUrl? }`. Read the mode-keyed verification secret/key from `env`, verify the signature over the **raw** body, then `JSON.parse` and return the payload. Throw `UNAUTHORIZED` on a missing/invalid signature; throw `badRequest` on non-JSON. Use `apps/sdp-api/src/lib/webhook-signature.ts` and supply the upstream timestamp as `timestampSeconds`; the helper enforces replay tolerance as well as signature verification. The shared verifier currently supports HMAC-SHA256 and ECDSA P-256/SHA-256, and it requires a provider-signed timestamp. If the upstream uses another algorithm or does not sign a timestamp, stop and extend/review the shared verification contract; do not fabricate a local timestamp or bypass replay protection. This is one of the few places `unknown` is allowed — it's a genuine trust boundary, narrowed immediately by `parse`. Signature variety across the six registered webhook providers: | Provider | Header | Algorithm | Env keys | |---|---|---|---| | Lightspark | `x-grid-signature` | ECDSA P-256 / SHA-256 (public key) | `LIGHTSPARK_GRID_WEBHOOK_PUBLIC_KEY` / `LIGHTSPARK_GRID_SANDBOX_WEBHOOK_PUBLIC_KEY` | | MoonPay | `moonpay-signature-v2` | HMAC-SHA256 (hex, `t=…,s=…`) | `MOONPAY_WEBHOOK_KEY` / `MOONPAY_SANDBOX_WEBHOOK_KEY` | | BVNK | `x-signature` | HMAC-SHA256 (base64) | `BVNK_WEBHOOK_SECRET` / `BVNK_SANDBOX_WEBHOOK_SECRET` | | Coinbase | `x-hook0-signature` | HMAC-SHA256 (hex, `t=…,v0=…`) | `COINBASE_CDP_RAMPS_WEBHOOK_SECRET` | | Mural | `x-mural-webhook-signature` + timestamp header | ECDSA P-256 / SHA-256 (public key, base64) | `MURAL_PAY_WEBHOOK_PUBLIC_KEY` / `MURAL_PAY_SANDBOX_WEBHOOK_PUBLIC_KEY` | | Stripe | `stripe-signature` | HMAC-SHA256 (hex, `t=…,v1=…`) | `STRIPE_WEBHOOK_SECRET` | ## parse `(payload: Payload) → Event`, typically `RampSettlementEvent` from `@sdp/payments/ramps/types`. Narrow the payload with `readString` / `readRecord` / `readNumber` from `@sdp/payments/json`. Map upstream event types to `kind`, and set `reference` to the quote id persisted by SDP; that is how settlement finds the transfer. Anything legitimately signed but irrelevant maps to `{ provider, kind: "ignore", reason }`. `parse` runs synchronously **before** the 2xx ack, so it must be total over every payload the provider can legitimately sign: unknown event types and transactions the platform didn't create (sandbox tests, manual payments on the same account) must map to an `ignore` event or an absent reference — never a throw, which turns into a non-2xx and a provider retry loop. Reserve throws for payloads that violate the provider's own guaranteed envelope (e.g. a missing event type), where a loud deterministic failure is the point. Example: references not minted by SDP return `undefined` from the processor's reference reader and get logged-and-skipped in `process`. `RampSettlementEvent` (`packages/sdp-payments/src/ramps/types.ts`): ``` `receivedAmount` is a major/display-unit string. For a settled on-ramp it is the received crypto amount; for an off-ramp it is the received fiat amount. | { kind: "awaiting_payment"; provider; reference } | { kind: "settling"; provider; reference } | { kind: "settled"; provider; reference; receivedAmount?; settlement? } | { kind: "failed"; provider; reference; error?; settlement? } | { kind: "expired"; provider; reference; error?; settlement? } | { kind: "ignore"; provider; reason } ``` ## process `process(c, environment, event)` is thin — ignore or apply: ```ts async process(c: AppContext, _environment: SdpEnvironment, event: RampSettlementEvent) { if (event.kind === "ignore") return; await applyRampSettlementEvent(c, event); } ``` Then register the class in `RAMP_PROVIDER_WEBHOOK_PROCESSOR` in `apps/sdp-api/src/routes/webhooks/handlers.ts`, or explicitly extend the excluded no-webhook provider union. `applyRampSettlementEvent` finds the transfer by `(provider, reference)`, skips terminal rows on redelivery, maps `kind → status`, persists received amounts for the relevant direction plus settlement economics, and records failed/expired errors. ## Beyond settlement: provisioning auto-advance A provider whose counterparty provisioning is async (KYC verification, wallet creation) also moves requirement state from webhooks: `parse` returns a wider provider event union — one event kind per upstream type (customer status changes, wallet creation/status changes, payment status changes) — and `process` switches exhaustively on `event.kind`, routing settlement kinds to `applyRampSettlementEvent`-based helpers and provisioning kinds to helpers that update `counterparty_provider_accounts` rows (never `counterparties.provider_data` — it's deprecated). The requirements GET then reflects the advanced state (`counterparty-requirements`). The standard contract a new provider implements is the settlement path above; add provisioning kinds only when the upstream provisions asynchronously. ## Rules + verify Shared rules live in `integrate-ramp-provider`. Hot here: - Verify the signature before trusting anything; never skip on a missing header — throw `UNAUTHORIZED`. - No swallowed errors in your own logic; the orchestration owns the 2xx + background write. - Event-type maps are `as const satisfies Record<string, RampSettlementEvent["kind"]>`; no `any` past the `verify` boundary. - Verify focused webhook tests for valid, missing, malformed, stale, and replayed signatures; unknown events; duplicate terminal delivery; and settlement persistence. Run API typecheck/tests plus `@sdp/payments` checks when event types change.
在 GitHub 查看