M9: Medusa v2 payment provider connector
A minimal receive-only GoblinPay payment provider for Medusa v2 under connectors/medusa (service + module registration + types, README + INSTALL), modeled on connectors/woocommerce and the medusa-plugin-btcpay reference: create-invoice on initiate, an HMAC-verified webhook flips the payment to captured, status polling as the webhook-miss fallback. Refunds throw (receive-only, manual). Also refresh the store.rs docstring now that WooCommerce and Medusa have shipped.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Installing GoblinPay for Medusa
|
||||
|
||||
This is a Medusa v2 payment-module provider. There are two ways to add it.
|
||||
|
||||
## 1. Add the provider to your Medusa app
|
||||
|
||||
### Option A: copy the source (simplest)
|
||||
|
||||
Copy this `medusa` directory into your Medusa app as a module, for example
|
||||
`src/modules/goblinpay`, keeping the `src/` files (`index.ts`, `service.ts`,
|
||||
`types.ts`). Medusa compiles it with the rest of your app.
|
||||
|
||||
### Option B: install as a package
|
||||
|
||||
Publish or vendor `medusa-payment-goblinpay` and add it to your app's
|
||||
dependencies. Build it first with `npm run build` (emits `dist/`).
|
||||
|
||||
## 2. Register it in `medusa-config.ts`
|
||||
|
||||
Add GoblinPay to the payment module's `providers`. Use `id: "goblinpay"` so the
|
||||
webhook route is predictable (see step 4):
|
||||
|
||||
```ts
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: [
|
||||
{
|
||||
resolve: "@medusajs/medusa/payment",
|
||||
options: {
|
||||
providers: [
|
||||
{
|
||||
// Option A: the path to the copied module.
|
||||
resolve: "./src/modules/goblinpay",
|
||||
// Option B: the package name, "medusa-payment-goblinpay".
|
||||
id: "goblinpay",
|
||||
options: {
|
||||
baseUrl: process.env.GOBLINPAY_URL,
|
||||
apiToken: process.env.GOBLINPAY_API_TOKEN,
|
||||
webhookSecret: process.env.GOBLINPAY_WEBHOOK_SECRET,
|
||||
matchMode: "derived",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## 3. Set the environment
|
||||
|
||||
In your Medusa app's `.env`:
|
||||
|
||||
```
|
||||
GOBLINPAY_URL=https://pay.example
|
||||
GOBLINPAY_API_TOKEN=<the same value as GP_API_TOKEN on the server>
|
||||
GOBLINPAY_WEBHOOK_SECRET=<the same value as GP_WEBHOOK_SECRET on the server>
|
||||
```
|
||||
|
||||
Then enable the `goblinpay` provider in the region(s) that should offer Grin,
|
||||
via the Medusa admin (Settings, then Regions, then Payment Providers).
|
||||
|
||||
## 4. Register the webhook in GoblinPay
|
||||
|
||||
Point your GoblinPay server at the Medusa payment webhook route. The route id is
|
||||
`<provider id>_<identifier>`, both `goblinpay`, so set these on the GoblinPay
|
||||
side:
|
||||
|
||||
- `GP_WEBHOOK_URL` = `https://YOUR-MEDUSA-HOST/hooks/payment/goblinpay_goblinpay`
|
||||
- `GP_WEBHOOK_SECRET` = the same secret you set as `webhookSecret`.
|
||||
- `GP_API_TOKEN` = the same token you set as `apiToken`.
|
||||
|
||||
GoblinPay signs each delivery with `X-GoblinPay-Signature: sha256=<hmac>` over
|
||||
the raw body and sends an idempotency key in `X-GoblinPay-Delivery`. The provider
|
||||
verifies the signature (constant-time) and flips the payment to captured.
|
||||
|
||||
Make sure the Medusa host is reachable from the GoblinPay host. If a webhook is
|
||||
ever missed, Medusa's `getPaymentStatus` polls
|
||||
`GET {baseUrl}/invoice/{invoice_id}` (with the bearer token) as a fallback.
|
||||
|
||||
## 5. Test
|
||||
|
||||
Place a test order, choose Grin (GoblinPay), and confirm:
|
||||
|
||||
- The storefront shows the GoblinPay QR / redirects to the `/pay/<token>` page
|
||||
(the checkout details are on the payment session's `data.goblinpay`).
|
||||
- Paying from a Goblin Wallet moves the order's payment to captured once
|
||||
GoblinPay delivers the `payment.received` webhook.
|
||||
|
||||
## Refund caveat
|
||||
|
||||
Refunds are not automated. GoblinPay is receive-only and never sends Grin, so
|
||||
any refund is a manual Grin send performed by the merchant from a wallet under
|
||||
their control. `refundPayment` throws to make this explicit.
|
||||
@@ -0,0 +1,90 @@
|
||||
# GoblinPay for Medusa
|
||||
|
||||
Accept Grin (GRIN / MimbleWimble) payments in a Medusa v2 store through a
|
||||
self-hosted GoblinPay server. The customer pays from their Goblin Wallet by
|
||||
scanning an `nprofile` QR code. The payment travels as a gift-wrapped slatepack
|
||||
over Nostr (optionally over the Nym mixnet). GoblinPay receives it, returns the
|
||||
reply slatepack to the payer, watches the chain to confirm, and notifies Medusa
|
||||
through a signed webhook.
|
||||
|
||||
This provider is a thin client. All of the Grin and Nostr work happens in
|
||||
GoblinPay; Medusa only talks HTTP to your GoblinPay instance. No BTCPay, no node
|
||||
exposed to the store, no wallet RPC.
|
||||
|
||||
## What it does
|
||||
|
||||
- Registers a `goblinpay` payment provider in the Medusa v2 payment module.
|
||||
- On checkout, calls GoblinPay to create an invoice for the payment session and
|
||||
stores the checkout details (`pay_url`, `nprofile`, `qr_svg`) on the session
|
||||
so your storefront can render the QR or redirect to GoblinPay's hosted
|
||||
`/pay/<token>` page.
|
||||
- Captures the payment when GoblinPay reports it, via a signed webhook. If a
|
||||
webhook is missed, `authorizePayment` and `getPaymentStatus` poll GoblinPay
|
||||
for the invoice status as a fallback.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Medusa v2 (built against `@medusajs/framework` 2.12; 2.x expected to work).
|
||||
- Node 20 or newer.
|
||||
- A running GoblinPay server reachable from the Medusa host.
|
||||
|
||||
## Options
|
||||
|
||||
Set these per-provider in `medusa-config.ts` (see INSTALL.md):
|
||||
|
||||
- `baseUrl`: base URL of your GoblinPay server, no trailing slash, for example
|
||||
`https://pay.example`.
|
||||
- `apiToken`: the GoblinPay create-invoice bearer token (`GP_API_TOKEN` on the
|
||||
server).
|
||||
- `webhookSecret`: the shared HMAC secret (`GP_WEBHOOK_SECRET` on the server).
|
||||
- `matchMode` (optional): how GoblinPay ties an incoming payment to the order.
|
||||
`derived` (per-invoice identity, recommended) gives each order its own QR and
|
||||
is the most reliable. `memo` and `amount` are also available. Omit to use the
|
||||
server default.
|
||||
- `expirySecs` (optional): invoice expiry in seconds from creation.
|
||||
|
||||
## Webhook
|
||||
|
||||
GoblinPay reports payments to the Medusa payment module's built-in webhook
|
||||
route. Point your GoblinPay server's `GP_WEBHOOK_URL` at:
|
||||
|
||||
```
|
||||
https://YOUR-MEDUSA-HOST/hooks/payment/goblinpay_goblinpay
|
||||
```
|
||||
|
||||
The provider verifies the `X-GoblinPay-Signature: sha256=<hmac>` header against
|
||||
the exact raw body (constant-time) before acting.
|
||||
|
||||
## Status mapping
|
||||
|
||||
| GoblinPay | Medusa payment session |
|
||||
|---|---|
|
||||
| invoice `open` | `pending` |
|
||||
| invoice `paid` | `captured` |
|
||||
| invoice `expired` | `canceled` |
|
||||
| webhook `payment.received` | captured (SUCCESSFUL) |
|
||||
| webhook `payment.confirmed` | captured (SUCCESSFUL, idempotent) |
|
||||
|
||||
For a receive-only till, a received payment (the reply slatepack is back and the
|
||||
funds are in the merchant wallet) is treated as paid, the same as the
|
||||
WooCommerce connector. The later on-chain confirmation is an idempotent no-op.
|
||||
|
||||
## Refunds
|
||||
|
||||
Refunds are not automated. GoblinPay is receive-only: it never sends Grin. A
|
||||
refund is therefore a manual, out-of-band Grin send by the merchant from a
|
||||
wallet under their control. `refundPayment` throws to make this explicit, the
|
||||
same caveat the Grin BTCPay connector carries.
|
||||
|
||||
## Security notes
|
||||
|
||||
- The webhook is authenticated by an HMAC-SHA256 signature over the exact raw
|
||||
request body, compared in constant time. A bad or missing signature is
|
||||
rejected and the payment is not flipped.
|
||||
- The capture amount is read from the Medusa payment session (its own
|
||||
store-currency amount), not from untrusted webhook fields.
|
||||
- Secrets live in the provider options / environment, never in code.
|
||||
|
||||
## Credit
|
||||
|
||||
Built by Claude (Anthropic) for the Goblin project.
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "medusa-payment-goblinpay",
|
||||
"version": "1.0.0",
|
||||
"description": "GoblinPay payment provider for Medusa v2: accept Grin (GRIN / MimbleWimble) payments through a self-hosted, receive-only GoblinPay server.",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"medusa",
|
||||
"medusa-v2",
|
||||
"medusa-plugin",
|
||||
"medusa-plugin-payment",
|
||||
"payment",
|
||||
"grin",
|
||||
"mimblewimble",
|
||||
"goblinpay"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.us-ea.st/GRIN/GoblinPay"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md",
|
||||
"INSTALL.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@medusajs/framework": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/framework": "^2.12.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
|
||||
|
||||
import GoblinPayProviderService from "./service"
|
||||
|
||||
// Register GoblinPay as a Medusa v2 payment-module provider. Referenced from
|
||||
// medusa-config.ts under the payment module's `providers` (see INSTALL.md).
|
||||
export default ModuleProvider(Modules.PAYMENT, {
|
||||
services: [GoblinPayProviderService],
|
||||
})
|
||||
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* GoblinPay payment provider for Medusa v2 (tested against @medusajs 2.12).
|
||||
*
|
||||
* Modeled on connectors/woocommerce and on the reference
|
||||
* github.com/SGFGOV/medusa-payment-plugins (packages/medusa-plugin-btcpay).
|
||||
*
|
||||
* Flow: `initiatePayment` creates a GoblinPay invoice for the order and stashes
|
||||
* the checkout details (pay_url, nprofile, qr_svg) on the session so the
|
||||
* storefront can render or redirect. The customer pays from their Goblin Wallet;
|
||||
* GoblinPay receives it, returns the reply slatepack, watches the chain, and
|
||||
* POSTs a signed webhook. `getWebhookActionAndData` verifies the HMAC and flips
|
||||
* the Medusa payment to captured. Status polling (`authorizePayment`,
|
||||
* `getPaymentStatus`) is the webhook-miss fallback.
|
||||
*
|
||||
* Refunds are NOT automated: GoblinPay is receive-only (it never sends Grin), so
|
||||
* `refundPayment` throws. A refund is a manual, out-of-band Grin send by the
|
||||
* merchant. See README.md.
|
||||
*/
|
||||
import crypto from "node:crypto"
|
||||
|
||||
import {
|
||||
AbstractPaymentProvider,
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
Modules,
|
||||
PaymentActions,
|
||||
} from "@medusajs/framework/utils"
|
||||
import type {
|
||||
AuthorizePaymentInput,
|
||||
AuthorizePaymentOutput,
|
||||
CancelPaymentInput,
|
||||
CancelPaymentOutput,
|
||||
CapturePaymentInput,
|
||||
CapturePaymentOutput,
|
||||
DeletePaymentInput,
|
||||
DeletePaymentOutput,
|
||||
GetPaymentStatusInput,
|
||||
GetPaymentStatusOutput,
|
||||
InitiatePaymentInput,
|
||||
InitiatePaymentOutput,
|
||||
IPaymentModuleService,
|
||||
Logger,
|
||||
ProviderWebhookPayload,
|
||||
RefundPaymentInput,
|
||||
RefundPaymentOutput,
|
||||
RetrievePaymentInput,
|
||||
RetrievePaymentOutput,
|
||||
UpdatePaymentInput,
|
||||
UpdatePaymentOutput,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/framework/types"
|
||||
|
||||
import type { GoblinPayInvoice, GoblinPayOptions } from "./types"
|
||||
|
||||
class GoblinPayProviderService extends AbstractPaymentProvider<GoblinPayOptions> {
|
||||
static identifier = "goblinpay"
|
||||
|
||||
protected readonly options_: GoblinPayOptions
|
||||
protected readonly logger_: Logger
|
||||
protected readonly paymentService_: IPaymentModuleService
|
||||
|
||||
constructor(container: Record<string, unknown>, options: GoblinPayOptions) {
|
||||
super(container as never, options)
|
||||
this.options_ = options
|
||||
this.logger_ = container[ContainerRegistrationKeys.LOGGER] as Logger
|
||||
this.paymentService_ = container[Modules.PAYMENT] as IPaymentModuleService
|
||||
|
||||
if (!options?.baseUrl || !options?.apiToken || !options?.webhookSecret) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"GoblinPay provider requires baseUrl, apiToken, and webhookSecret options"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get base(): string {
|
||||
return this.options_.baseUrl.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
/** Call the GoblinPay REST API with the bearer token. */
|
||||
private async request<T>(
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
body?: unknown
|
||||
): Promise<T> {
|
||||
const res = await fetch(`${this.base}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.options_.apiToken}`,
|
||||
...(body ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
const text = await res.text()
|
||||
const json = text ? JSON.parse(text) : {}
|
||||
if (!res.ok) {
|
||||
const err =
|
||||
(json && (json.error as string)) || `GoblinPay HTTP ${res.status}`
|
||||
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, err)
|
||||
}
|
||||
return json as T
|
||||
}
|
||||
|
||||
/** Map a GoblinPay invoice status to a Medusa payment session status. */
|
||||
private static mapStatus(
|
||||
status: string
|
||||
): "captured" | "canceled" | "pending" {
|
||||
switch (status) {
|
||||
case "paid":
|
||||
return "captured"
|
||||
case "expired":
|
||||
return "canceled"
|
||||
default:
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
async initiatePayment(
|
||||
input: InitiatePaymentInput
|
||||
): Promise<InitiatePaymentOutput> {
|
||||
const sessionId = input.context?.idempotency_key
|
||||
if (!sessionId) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Idempotency key (payment session id) is required to initiate payment"
|
||||
)
|
||||
}
|
||||
|
||||
// GoblinPay prices the fiat order into Grin via its own oracle. The order's
|
||||
// session id is the order_ref, so the signed webhook echoes it back to us.
|
||||
const invoice = await this.request<GoblinPayInvoice>("POST", "/invoice", {
|
||||
order_ref: sessionId,
|
||||
amount_fiat: input.amount.toString(),
|
||||
currency: input.currency_code,
|
||||
memo: `Medusa order ${sessionId}`,
|
||||
...(this.options_.matchMode ? { match_mode: this.options_.matchMode } : {}),
|
||||
...(this.options_.expirySecs ? { expiry_secs: this.options_.expirySecs } : {}),
|
||||
})
|
||||
|
||||
return {
|
||||
id: sessionId,
|
||||
data: { ...input.data, goblinpay: invoice },
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-read the current invoice from GoblinPay using the stored invoice_id. */
|
||||
private async fetchInvoice(
|
||||
data: Record<string, unknown> | undefined
|
||||
): Promise<GoblinPayInvoice> {
|
||||
const stored = (data?.goblinpay ?? {}) as GoblinPayInvoice
|
||||
if (!stored.invoice_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"No GoblinPay invoice_id on the payment session"
|
||||
)
|
||||
}
|
||||
return this.request<GoblinPayInvoice>(
|
||||
"GET",
|
||||
`/invoice/${encodeURIComponent(stored.invoice_id)}`
|
||||
)
|
||||
}
|
||||
|
||||
async authorizePayment(
|
||||
input: AuthorizePaymentInput
|
||||
): Promise<AuthorizePaymentOutput> {
|
||||
const invoice = await this.fetchInvoice(input.data)
|
||||
return {
|
||||
status: GoblinPayProviderService.mapStatus(invoice.status),
|
||||
data: { ...input.data, goblinpay: invoice },
|
||||
}
|
||||
}
|
||||
|
||||
async getPaymentStatus(
|
||||
input: GetPaymentStatusInput
|
||||
): Promise<GetPaymentStatusOutput> {
|
||||
const invoice = await this.fetchInvoice(input.data)
|
||||
return {
|
||||
status: GoblinPayProviderService.mapStatus(invoice.status),
|
||||
data: { ...input.data, goblinpay: invoice },
|
||||
}
|
||||
}
|
||||
|
||||
async capturePayment(
|
||||
input: CapturePaymentInput
|
||||
): Promise<CapturePaymentOutput> {
|
||||
// GoblinPay is receive-only: once the payment is received the funds are
|
||||
// already in the merchant wallet, so capture is a no-op acknowledgement.
|
||||
return { data: input.data ?? {} }
|
||||
}
|
||||
|
||||
async cancelPayment(
|
||||
input: CancelPaymentInput
|
||||
): Promise<CancelPaymentOutput> {
|
||||
// Nothing to cancel server-side; an unpaid GoblinPay invoice simply expires.
|
||||
return { data: input.data ?? {} }
|
||||
}
|
||||
|
||||
async deletePayment(
|
||||
input: DeletePaymentInput
|
||||
): Promise<DeletePaymentOutput> {
|
||||
return { data: input.data ?? {} }
|
||||
}
|
||||
|
||||
async refundPayment(
|
||||
_input: RefundPaymentInput
|
||||
): Promise<RefundPaymentOutput> {
|
||||
// Receive-only: GoblinPay never sends Grin, so refunds cannot be automated.
|
||||
// A refund is a manual, out-of-band Grin send by the merchant.
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"GoblinPay is receive-only; refunds must be issued manually by the merchant (out-of-band Grin send)."
|
||||
)
|
||||
}
|
||||
|
||||
async retrievePayment(
|
||||
input: RetrievePaymentInput
|
||||
): Promise<RetrievePaymentOutput> {
|
||||
const invoice = await this.fetchInvoice(input.data)
|
||||
return { data: { ...input.data, goblinpay: invoice } }
|
||||
}
|
||||
|
||||
async updatePayment(
|
||||
input: UpdatePaymentInput
|
||||
): Promise<UpdatePaymentOutput> {
|
||||
return { data: input.data ?? {} }
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the HMAC-SHA256 over the EXACT raw body, constant-time. Mirrors the
|
||||
* WooCommerce connector and GoblinPay's webhook contract:
|
||||
* X-GoblinPay-Signature: sha256=<hex(HMAC-SHA256(secret, raw_body))>
|
||||
*/
|
||||
private verifySignature(payload: ProviderWebhookPayload["payload"]): boolean {
|
||||
const raw = payload.rawData
|
||||
if (!raw) {
|
||||
return false
|
||||
}
|
||||
const provided = (payload.headers?.["x-goblinpay-signature"] as string) ?? ""
|
||||
const expected =
|
||||
"sha256=" +
|
||||
crypto
|
||||
.createHmac("sha256", this.options_.webhookSecret)
|
||||
.update(raw as string | Buffer)
|
||||
.digest("hex")
|
||||
const a = Buffer.from(provided, "utf8")
|
||||
const b = Buffer.from(expected, "utf8")
|
||||
return a.length === b.length && crypto.timingSafeEqual(a, b)
|
||||
}
|
||||
|
||||
async getWebhookActionAndData(
|
||||
payload: ProviderWebhookPayload["payload"]
|
||||
): Promise<WebhookActionResult> {
|
||||
if (!this.verifySignature(payload)) {
|
||||
this.logger_.warn("goblinpay: webhook signature mismatch")
|
||||
return { action: PaymentActions.FAILED }
|
||||
}
|
||||
|
||||
const data = (payload.data ?? {}) as {
|
||||
event_type?: string
|
||||
order_ref?: string
|
||||
}
|
||||
const sessionId = data.order_ref
|
||||
if (!sessionId) {
|
||||
return { action: PaymentActions.NOT_SUPPORTED }
|
||||
}
|
||||
|
||||
// payment.received (funds in hand, S2 returned) and payment.confirmed
|
||||
// (on-chain) both mean paid for a receive-only till: flip to captured. The
|
||||
// capture is idempotent, so a later confirmation after a received event is a
|
||||
// no-op. Capture the session's own (store-currency) amount.
|
||||
if (
|
||||
data.event_type === "payment.received" ||
|
||||
data.event_type === "payment.confirmed"
|
||||
) {
|
||||
const amount = await this.sessionAmount(sessionId)
|
||||
return {
|
||||
action: PaymentActions.SUCCESSFUL,
|
||||
data: { session_id: sessionId, amount },
|
||||
}
|
||||
}
|
||||
|
||||
return { action: PaymentActions.NOT_SUPPORTED }
|
||||
}
|
||||
|
||||
/** The payment session's authorized amount, for the webhook capture. */
|
||||
private async sessionAmount(sessionId: string): Promise<number> {
|
||||
try {
|
||||
const session = await this.paymentService_.retrievePaymentSession(sessionId)
|
||||
return Number(session.amount)
|
||||
} catch (e) {
|
||||
this.logger_.warn(
|
||||
`goblinpay: could not read session ${sessionId} amount: ${
|
||||
(e as Error).message
|
||||
}`
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GoblinPayProviderService
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Options + wire types for the GoblinPay Medusa v2 payment provider.
|
||||
*
|
||||
* GoblinPay is a receive-only Grin payment server. This provider is a thin
|
||||
* client: it calls GoblinPay's REST API to create an invoice and reads back the
|
||||
* checkout details, then flips the Medusa payment on a signed webhook. All Grin
|
||||
* and Nostr work happens in GoblinPay; Medusa only speaks HTTP to it.
|
||||
*/
|
||||
|
||||
/** Provider options, set per-provider in `medusa-config.ts`. */
|
||||
export interface GoblinPayOptions {
|
||||
/** Base URL of your GoblinPay server, no trailing slash (e.g. https://pay.example). */
|
||||
baseUrl: string
|
||||
/** Bearer token for the create-invoice API (`GP_API_TOKEN` on the server). */
|
||||
apiToken: string
|
||||
/** Shared HMAC secret for webhook verification (`GP_WEBHOOK_SECRET`). */
|
||||
webhookSecret: string
|
||||
/**
|
||||
* How GoblinPay matches an incoming payment to this order. `derived`
|
||||
* (per-invoice identity, recommended) gives each order its own QR. Omit to
|
||||
* use the server default.
|
||||
*/
|
||||
matchMode?: "memo" | "derived" | "amount"
|
||||
/** Optional invoice expiry in seconds from creation. */
|
||||
expirySecs?: number
|
||||
}
|
||||
|
||||
/** The subset of GoblinPay's `/invoice` response this provider stores/uses. */
|
||||
export interface GoblinPayInvoice {
|
||||
invoice_id: string
|
||||
token?: string
|
||||
pay_url: string
|
||||
nprofile?: string
|
||||
npub?: string
|
||||
qr_svg?: string
|
||||
amount?: string
|
||||
/** GoblinPay invoice lifecycle: `open` | `paid` | `expired`. */
|
||||
status: string
|
||||
order_ref?: string
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2021"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The store-connector seam.
|
||||
//!
|
||||
//! Every store integration (the built-in generic REST connector, the
|
||||
//! WooCommerce and Medusa plugins that arrive in a later milestone, and the
|
||||
//! future pop-up Nostr store) drives GoblinPay through one uniform contract:
|
||||
//! Every store integration (the built-in generic REST connector, the shipped
|
||||
//! WooCommerce and Medusa plugins under `connectors/`, and the future pop-up
|
||||
//! Nostr store) drives GoblinPay through one uniform contract:
|
||||
//! a create-invoice request in, a hosted checkout + signed webhook out. This
|
||||
//! trait keeps that mapping in one place so the core never grows per-store
|
||||
//! branches: a connector only decides how a store's order becomes invoice
|
||||
|
||||
Reference in New Issue
Block a user