nuxt-openreceive 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Grygorii Liashenko (frontender-ua)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # nuxt-openreceive
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/aisoul-dev/nuxt-openreceive/actions/workflows/ci.yml/badge.svg)](https://github.com/aisoul-dev/nuxt-openreceive/actions/workflows/ci.yml)
4
+
5
+ Nuxt module for [OpenReceive](https://openreceive.org): accept Bitcoin over Lightning, plus optional USDT / USDC / ETH / SOL through a swap provider. Payments settle straight into **your own wallet**. You don't need an account or an API key, and no third party ever holds your funds.
6
+
7
+ OpenReceive doesn't ship a Nuxt package. Its docs have you copy a server route, a lazy singleton, IP wiring and a checkout page by hand. This module does that wiring for you, so all you write is the host file:
8
+
9
+ - mounts the payment routes at `/openreceive/**` (configurable)
10
+ - builds the OpenReceive stack lazily on the first request (builds and prerendering never touch the wallet) and retries if boot fails
11
+ - reads secrets from Nuxt `runtimeConfig` (`NUXT_OPENRECEIVE_*`), falling back to OpenReceive's own `NWC_URI`, `LSC_URI_*` and `DATABASE_URL`
12
+ - manages a `pg` pool for you, or takes your own handle, Supabase, or a custom repository
13
+ - gives `authorize` a typed `event: H3Event`, so it can read your existing session
14
+ - turns on per-IP rate limiting with `getRequestIP` wired in
15
+ - registers `<OpenReceiveCheckout>` globally with the prefix already filled in, and handles CSS, `build.transpile` and Vite pre-bundling for the raw SFC
16
+ - stops the build on edge presets, since the wallet client and SQL driver need Node
17
+ - closes the wallet connection and the pool when Nitro shuts down
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ pnpm add nuxt-openreceive pg
23
+ ```
24
+
25
+ ```ts
26
+ // nuxt.config.ts
27
+ export default defineNuxtConfig({
28
+ modules: ['nuxt-openreceive'],
29
+ })
30
+ ```
31
+
32
+ ```sh
33
+ # .env (server only, never NUXT_PUBLIC_*)
34
+ NUXT_OPENRECEIVE_NWC_URI=nostr+walletconnect://... # receive-only code from your wallet
35
+ NUXT_OPENRECEIVE_DATABASE_URL=postgres://... # or DATABASE_URL
36
+ # optional, for USDT/USDC/ETH/SOL swaps:
37
+ NUXT_OPENRECEIVE_LSC_URI_PRIMARY=lightning+swapconnect://...
38
+ ```
39
+
40
+ To get a receive-only NWC code, use Rizful (menu → NWC → *Receive-only*) or Alby Hub (Connections → Add Connection → *Read Only*). See the [guide](https://openreceive.org/get_a_nwc_code_to_receive_payments). OpenReceive refuses to boot with a code that can spend.
41
+
42
+ Create the payment tables once through your own migrations:
43
+
44
+ ```ts
45
+ // e.g. a one-off Nitro task or your migration tool: the DDL is idempotent
46
+ await db.query(openReceiveSchemaSql('postgres')) // auto-imported on the server
47
+ ```
48
+
49
+ `npx openreceive scaffold payments --orm drizzle` (or prisma, knex, ...) emits the same tables as an ORM migration.
50
+
51
+ Or set `openreceive: { migrate: true }` to run the DDL on boot when the module manages the pool.
52
+
53
+ ## The host file
54
+
55
+ `server/openreceive.ts` is how OpenReceive talks to your app: three hooks mapped onto **your** existing order and user tables.
56
+
57
+ ```ts
58
+ // server/openreceive.ts
59
+ export default defineOpenReceiveHost({
60
+ // The trusted price, from YOUR order row. Never from the request.
61
+ amountFor: async (reference) => {
62
+ const order = await findOrder(reference)
63
+ return order?.state === 'awaiting_payment'
64
+ ? { currency: 'USD', value: order.total, description: order.title } // decimal string
65
+ : null // → 404
66
+ },
67
+
68
+ // Runs on every payment request. `resource.reference` is a claim, not proof.
69
+ authorize: async ({ event, resource }) => {
70
+ const { user } = await getUserSession(event) // nuxt-auth-utils, Supabase, ...
71
+ const order = resource.reference ? await findOrder(resource.reference) : undefined
72
+ return Boolean(user && order?.userId === user.id)
73
+ },
74
+
75
+ // Once per order, INSIDE the settlement transaction. Write through `query`.
76
+ onPaid: async ({ reference, paidAt, query }) => {
77
+ await query(
78
+ `UPDATE orders SET state = 'paid', paid_at = to_timestamp($1)
79
+ WHERE id = $2 AND state = 'awaiting_payment'`,
80
+ [paidAt, reference],
81
+ )
82
+ },
83
+ })
84
+ ```
85
+
86
+ Pass a function instead (`defineOpenReceiveHost(() => ({ ... }))`) to defer work until the first payment request.
87
+
88
+ ### Rules that matter
89
+
90
+ - **`reference` is your order id.** Create the order before checkout, keep its id across retries, and never reuse it. If you generate a fresh id on every page load, one order can be paid twice.
91
+ - **`onPaid` is for database writes only, and it must be idempotent.** Send emails and webhooks after the commit, from your own worker or an outbox table. Writes made through your ORM's own connection survive a rolled-back settlement; writes made through `query` don't.
92
+ - **Lightning payments can't be refunded by the merchant.** The wallet code is receive-only. The only refunds are swap refunds (see below).
93
+
94
+ ### Bring your own storage
95
+
96
+ ```ts
97
+ export default defineOpenReceiveHost(() => ({
98
+ amountFor, authorize,
99
+ // your existing pg Pool (Drizzle: pass the Pool underneath it), mysql2, node:sqlite, better-sqlite3
100
+ storage: { db: usePgPool(), onPaid },
101
+ // or Supabase over HTTPS (fulfillment is the SQL function public.openreceive_on_paid):
102
+ // storage: { supabase: { url: process.env.SUPABASE_URL!, key: process.env.SUPABASE_SECRET_KEY! } },
103
+ }))
104
+ ```
105
+
106
+ ## The checkout page
107
+
108
+ ```vue
109
+ <!-- app/pages/checkout/[reference].vue -->
110
+ <script setup lang="ts">
111
+ const route = useRoute()
112
+ // your own route that 404s unless the visitor owns the order
113
+ const { data, refresh } = await useFetch(`/api/checkout/${route.params.reference}`)
114
+ const settledHere = ref(false)
115
+ </script>
116
+
117
+ <template>
118
+ <p>{{ data?.state === 'paid' ? 'Paid' : 'Awaiting payment' }}</p>
119
+ <!-- show it only while unpaid, but keep it mounted once it settles: it is the receipt -->
120
+ <OpenReceiveCheckout
121
+ v-if="data && (data.state !== 'paid' || settledHere)"
122
+ :reference="data.reference"
123
+ :resumable="true"
124
+ :onSettled="() => { settledHere = true; refresh() }"
125
+ />
126
+ </template>
127
+ ```
128
+
129
+ Every `@openreceive/vue` prop passes through. Event handlers (`onSettled`, `onState`, …) are **props** (`:onSettled`), not `@settled`.
130
+
131
+ **Swaps:** if you set `LSC_URI_PRIMARY`, the checkout must live at a per-order URL the payer can come back to (`/checkout/:reference`), and `authorize` must still recognise them hours later. A deposit that arrives short or late becomes `refund_required` and is claimed on that second visit. See [swap refunds](https://openreceive.org/guides/swap-refunds.md).
132
+
133
+ ## Options
134
+
135
+ ```ts
136
+ openreceive: {
137
+ prefix: '/openreceive', // route prefix; the component uses it too
138
+ host: '~~/server/openreceive', // host definition file
139
+ component: true, // true → <OpenReceiveCheckout>, string → custom name, false → none
140
+ css: true, // add @openreceive/vue/styles.css
141
+ rateLimiting: true, // or { limitPerHour, limitPerDay, message }
142
+ trustProxy: false, // read x-forwarded-for (only behind YOUR proxy)
143
+ migrate: false, // run the payment-table DDL on boot (managed pool only)
144
+ }
145
+ ```
146
+
147
+ Server auto-imports: `defineOpenReceiveHost`, `useOpenReceive()` (the booted stack, if you need `handler` in a custom route), and `openReceiveSchemaSql(dialect)`.
148
+
149
+ ## Deploying
150
+
151
+ - Runs on `node-server` and on Node serverless presets (Vercel, Netlify, AWS Lambda). On serverless, use a **pooled** database URL. Edge presets are refused at build time.
152
+ - You don't need a worker or a cron job. Every payment request runs a reconcile pass through a lock in your database, so an abandoned checkout settles on the next request. For push settlement, run OpenReceive's optional `startNotificationWorker` in a separate process.
153
+ - The built server doesn't read `.env`. Set the variables in its environment, or start it with `node --env-file=.env .output/server/index.mjs`.
154
+
155
+ ## Development
156
+
157
+ ```sh
158
+ pnpm install
159
+ pnpm dev:prepare
160
+ DATABASE_URL=postgres://... OPENRECEIVE_TEST_WALLET=1 pnpm dev # playground on an in-memory test wallet
161
+ DATABASE_URL=postgres://... pnpm test # builds the playground and runs a full payment e2e
162
+ ```
163
+
164
+ `OPENRECEIVE_TEST_WALLET=1` swaps in a fake receive-only wallet, so you can run the whole flow without a real NWC code. It is **playground only**. `POST /api/dev/settle/:hash` marks an invoice paid.
165
+
166
+ Known noise: the Vite build prints `postcss` "Lexical error" warnings for `mod()` / `round()` in OpenReceive's stylesheet. These are harmless: the CSS is valid and works in browsers.
167
+
168
+ ## License
169
+
170
+ MIT © Grygorii Liashenko ([frontender-ua](https://github.com/frontender-ua)) · [aisoul](https://aisoul.dev). OpenReceive itself is MIT, © its authors.
@@ -0,0 +1,66 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { IpRateLimitConfig } from '@openreceive/http';
3
+
4
+ interface ModuleOptions {
5
+ /** Turn the whole module off without removing it. Default `true`. */
6
+ enabled: boolean;
7
+ /**
8
+ * Where the payment routes are mounted. The checkout component talks to the
9
+ * same prefix by default. Default `/openreceive`.
10
+ */
11
+ prefix: string;
12
+ /**
13
+ * Your host definition: the file whose default export is
14
+ * `defineOpenReceiveHost({ amountFor, authorize, onPaid })`.
15
+ * Resolved like any Nuxt path. Default `~~/server/openreceive`.
16
+ */
17
+ host: string;
18
+ /**
19
+ * Register the checkout component globally. `true` uses the name
20
+ * `OpenReceiveCheckout`; a string sets your own name; `false` skips it.
21
+ */
22
+ component: boolean | string;
23
+ /** Add `@openreceive/vue/styles.css` to the app. Default `true`. */
24
+ css: boolean;
25
+ /**
26
+ * Per-IP invoice rate limiting (counted from `openreceive_payments`, so it
27
+ * survives restarts and spans instances). Default `true`. The module wires
28
+ * the client IP from the H3 event for you.
29
+ */
30
+ rateLimiting: boolean | Omit<IpRateLimitConfig, 'ip'>;
31
+ /**
32
+ * Read the client IP from `x-forwarded-for`. Only enable behind YOUR own
33
+ * reverse proxy that sets the header, otherwise payers can spoof it.
34
+ */
35
+ trustProxy: boolean;
36
+ /**
37
+ * Create `openreceive_payments` / `openreceive_meta` on boot when the module
38
+ * manages the Postgres pool. The DDL is idempotent. Default `false`: prefer
39
+ * running it through your own migrations.
40
+ */
41
+ migrate: boolean;
42
+ }
43
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
44
+
45
+ declare module '@nuxt/schema' {
46
+ interface RuntimeConfig {
47
+ openreceive: {
48
+ nwcUri: string;
49
+ lscUriPrimary: string;
50
+ lscUriBackup: string;
51
+ databaseUrl: string;
52
+ prefix: string;
53
+ rateLimiting: ModuleOptions['rateLimiting'];
54
+ trustProxy: boolean;
55
+ migrate: boolean;
56
+ };
57
+ }
58
+ interface PublicRuntimeConfig {
59
+ openreceive: {
60
+ prefix: string;
61
+ };
62
+ }
63
+ }
64
+
65
+ export { _default as default };
66
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-openreceive",
3
+ "configKey": "openreceive",
4
+ "compatibility": {
5
+ "nuxt": ">=3.13.0"
6
+ },
7
+ "version": "0.1.1",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "unknown"
11
+ }
12
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { defineNuxtModule, useLogger, createResolver, findPath, addServerHandler, addServerPlugin, addServerImports, addTypeTemplate, addComponent } from '@nuxt/kit';
3
+ import { defu } from 'defu';
4
+
5
+ const EDGE_PRESETS = /^(cloudflare|vercel-edge|netlify-edge|deno-deploy|deno|bun-edge|winterjs|lagon)/;
6
+ const module$1 = defineNuxtModule({
7
+ meta: {
8
+ name: "nuxt-openreceive",
9
+ configKey: "openreceive",
10
+ compatibility: { nuxt: ">=3.13.0" }
11
+ },
12
+ defaults: {
13
+ enabled: true,
14
+ prefix: "/openreceive",
15
+ host: "~~/server/openreceive",
16
+ component: true,
17
+ css: true,
18
+ rateLimiting: true,
19
+ trustProxy: false,
20
+ migrate: false
21
+ },
22
+ async setup(options, nuxt) {
23
+ const logger = useLogger("openreceive");
24
+ if (!options.enabled) return;
25
+ const { resolve } = createResolver(import.meta.url);
26
+ const prefix = normalizePrefix(options.prefix);
27
+ nuxt.options.runtimeConfig.openreceive = defu(
28
+ nuxt.options.runtimeConfig.openreceive,
29
+ {
30
+ nwcUri: "",
31
+ lscUriPrimary: "",
32
+ lscUriBackup: "",
33
+ databaseUrl: "",
34
+ prefix,
35
+ rateLimiting: options.rateLimiting,
36
+ trustProxy: options.trustProxy,
37
+ migrate: options.migrate
38
+ }
39
+ );
40
+ nuxt.options.runtimeConfig.public.openreceive = defu(
41
+ nuxt.options.runtimeConfig.public.openreceive,
42
+ { prefix }
43
+ );
44
+ const hostPath = await findPath(options.host, { extensions: [".ts", ".mts", ".js", ".mjs"] });
45
+ if (!hostPath || !existsSync(hostPath)) {
46
+ logger.warn(
47
+ `No host definition found at \`${options.host}\`. Payment routes under \`${prefix}\` will answer 503 until you create it (export default defineOpenReceiveHost({ amountFor, authorize, onPaid })).`
48
+ );
49
+ }
50
+ nuxt.hook("nitro:config", (nitroConfig) => {
51
+ nitroConfig.virtual ||= {};
52
+ nitroConfig.virtual["#openreceive/host"] = hostPath ? `export { default } from ${JSON.stringify(hostPath)}` : "export default undefined";
53
+ nitroConfig.externals ||= {};
54
+ nitroConfig.externals.inline ||= [];
55
+ nitroConfig.externals.inline.push(resolve("./runtime"));
56
+ });
57
+ nuxt.hook("nitro:init", (nitro) => {
58
+ const preset = String(nitro.options.preset || "");
59
+ if (EDGE_PRESETS.test(preset)) {
60
+ throw new Error(
61
+ `[nuxt-openreceive] Nitro preset "${preset}" is an edge runtime. OpenReceive needs Node (NWC wallet client + SQL driver). Use node-server, or a Node serverless preset (vercel, netlify, aws-lambda).`
62
+ );
63
+ }
64
+ });
65
+ addServerHandler({
66
+ route: `${prefix}/**`,
67
+ handler: resolve("./runtime/server/handler")
68
+ });
69
+ addServerPlugin(resolve("./runtime/server/plugin"));
70
+ addServerImports([
71
+ { name: "defineOpenReceiveHost", from: resolve("./runtime/server/utils/openreceive") },
72
+ { name: "useOpenReceive", from: resolve("./runtime/server/utils/openreceive") },
73
+ { name: "openReceiveSchemaSql", from: resolve("./runtime/server/utils/openreceive") }
74
+ ]);
75
+ addTypeTemplate({
76
+ filename: "types/openreceive-host.d.ts",
77
+ getContents: () => [
78
+ `declare module '#openreceive/host' {`,
79
+ ` const host: import(${JSON.stringify(resolve("./runtime/server/utils/openreceive"))}).OpenReceiveHostInput | undefined`,
80
+ ` export default host`,
81
+ `}`
82
+ ].join("\n")
83
+ }, { nitro: true, nuxt: true });
84
+ nuxt.options.build.transpile.push("@openreceive/vue");
85
+ nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, {
86
+ exclude: ["@openreceive/vue"],
87
+ // Vite pre-bundles by exact specifier, so list the subpaths the raw SFC
88
+ // reaches. This also converts the CommonJS `qrcode` dependency for dev.
89
+ include: [
90
+ "@openreceive/vue > @openreceive/elements",
91
+ "@openreceive/vue > @openreceive/elements/wrapper-shared",
92
+ "@openreceive/vue > @openreceive/browser",
93
+ "@openreceive/vue > @openreceive/browser/headless"
94
+ ]
95
+ });
96
+ if (options.css) {
97
+ nuxt.options.css.push("@openreceive/vue/styles.css");
98
+ }
99
+ if (options.component) {
100
+ addComponent({
101
+ name: typeof options.component === "string" ? options.component : "OpenReceiveCheckout",
102
+ filePath: resolve("./runtime/components/OpenReceiveCheckout")
103
+ });
104
+ }
105
+ logger.info(`OpenReceive routes mounted at \`${prefix}/**\``);
106
+ }
107
+ });
108
+ function normalizePrefix(prefix) {
109
+ const trimmed = `/${prefix.trim().replace(/^\/+|\/+$/g, "")}`;
110
+ return trimmed === "/" ? "/openreceive" : trimmed;
111
+ }
112
+
113
+ export { module$1 as default };
@@ -0,0 +1,4 @@
1
+ import { type DefineComponent } from 'vue';
2
+ import { type VueCheckoutProps } from '@openreceive/vue/checkout.vue';
3
+ declare const _default: DefineComponent<VueCheckoutProps>;
4
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { defineComponent, h } from "vue";
2
+ import Checkout from "@openreceive/vue/checkout.vue";
3
+ import { useRuntimeConfig } from "#imports";
4
+ const OpenReceiveCheckout = defineComponent({
5
+ name: "OpenReceiveCheckout",
6
+ inheritAttrs: false,
7
+ setup(_, { attrs, slots }) {
8
+ const { prefix } = useRuntimeConfig().public.openreceive;
9
+ return () => h(Checkout, { prefix, ...attrs }, slots);
10
+ }
11
+ });
12
+ export default OpenReceiveCheckout;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Response>>;
2
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import { createRequestId, errorResponse, HttpError } from "@openreceive/http";
2
+ import { defineEventHandler, toWebRequest } from "h3";
3
+ import { useOpenReceive } from "./utils/openreceive.js";
4
+ let warned = false;
5
+ export default defineEventHandler(async (event) => {
6
+ let stack;
7
+ try {
8
+ stack = await useOpenReceive();
9
+ } catch (error) {
10
+ if (!warned || import.meta.dev) {
11
+ warned = true;
12
+ console.error(`[openreceive] ${error instanceof Error ? error.message : String(error)}`);
13
+ }
14
+ return errorResponse(
15
+ new HttpError(503, "WALLET_UNAVAILABLE", "The payment service is not available. Please try again shortly.", {
16
+ retryable: true,
17
+ retryAfterSeconds: 5
18
+ }),
19
+ createRequestId()
20
+ );
21
+ }
22
+ return stack.handler(toWebRequest(event), { native: event });
23
+ });
@@ -0,0 +1,7 @@
1
+ interface NitroAppLike {
2
+ hooks: {
3
+ hook: (name: 'close', fn: () => Promise<void>) => unknown;
4
+ };
5
+ }
6
+ export default function openReceiveNitroPlugin(nitroApp: NitroAppLike): void;
7
+ export {};
@@ -0,0 +1,4 @@
1
+ import { closeOpenReceive } from "./utils/openreceive.js";
2
+ export default function openReceiveNitroPlugin(nitroApp) {
3
+ nitroApp.hooks.hook("close", closeOpenReceive);
4
+ }
@@ -0,0 +1,60 @@
1
+ import { paymentsSchemaSql, type AuthorizeContext, type CreateStackOptions, type IpRateLimitConfig, type PaymentSettlementHook, type Stack, type StackStorage, type StackWallet } from '@openreceive/http';
2
+ import { type H3Event } from 'h3';
3
+ /** `authorize` context with the request's H3 event, typed. */
4
+ export interface OpenReceiveAuthorizeContext extends AuthorizeContext {
5
+ /** The H3 event of this request: read your session/cookies from it. */
6
+ readonly event: H3Event;
7
+ }
8
+ type PassThroughOptions = Omit<CreateStackOptions, 'wallet' | 'storage' | 'authorize' | 'prefix' | 'amountFor' | 'rateLimiting'>;
9
+ export interface OpenReceiveHost extends PassThroughOptions {
10
+ /**
11
+ * The trusted price for an order id (`reference`), read from YOUR data.
12
+ * Return `null` when there is nothing to pay (answered 404). Never trust an
13
+ * amount the payer sends.
14
+ */
15
+ amountFor: CreateStackOptions['amountFor'];
16
+ /**
17
+ * Runs on every payment request. `resource.reference` is a claim the payer
18
+ * made, not proof: check ownership against the session on `event`.
19
+ */
20
+ authorize: (context: OpenReceiveAuthorizeContext) => boolean | Promise<boolean>;
21
+ /**
22
+ * Fulfillment, when the module manages the Postgres pool (DATABASE_URL).
23
+ * Runs once per reference INSIDE the settlement transaction: do your writes
24
+ * through the supplied `query` (`$1` placeholders), keep it idempotent, and
25
+ * send emails/webhooks only after commit, from your own worker.
26
+ */
27
+ onPaid?: PaymentSettlementHook;
28
+ /**
29
+ * Bring your own storage instead of the managed pool: `{ db, onPaid }` with
30
+ * an existing pg Pool / mysql2 / sqlite handle, `{ supabase: { url, key } }`
31
+ * for Supabase over HTTPS, or `{ payments, onPaid }` for a custom repository.
32
+ */
33
+ storage?: StackStorage;
34
+ /**
35
+ * Override the wallet (tests, a pre-built service). By default the module
36
+ * builds it from `NUXT_OPENRECEIVE_NWC_URI` / `NWC_URI`.
37
+ */
38
+ wallet?: StackWallet;
39
+ /** Override the module-level `rateLimiting` option for this host. */
40
+ rateLimiting?: boolean | Omit<IpRateLimitConfig, 'ip'> | IpRateLimitConfig;
41
+ }
42
+ export type OpenReceiveHostFactory = () => OpenReceiveHost | Promise<OpenReceiveHost>;
43
+ export type OpenReceiveHostInput = OpenReceiveHost | OpenReceiveHostFactory;
44
+ /**
45
+ * Define how OpenReceive talks to your app. Put it in `server/openreceive.ts`
46
+ * as the default export. Pass a function to defer work (opening your DB,
47
+ * reading config) until the first payment request.
48
+ */
49
+ export declare function defineOpenReceiveHost<T extends OpenReceiveHostInput>(host: T): T;
50
+ /** The OpenReceive payment tables DDL for your own migrations. Idempotent. */
51
+ export declare function openReceiveSchemaSql(dialect?: Parameters<typeof paymentsSchemaSql>[0]): string;
52
+ /**
53
+ * The booted OpenReceive stack for this server process. Built on first use,
54
+ * never at import, so `nuxi build` and prerendering never touch the wallet.
55
+ * A failed boot is retried after a short pause instead of sticking forever.
56
+ */
57
+ export declare function useOpenReceive(): Promise<Stack>;
58
+ /** Close the wallet connection and the managed pool (called on Nitro close). */
59
+ export declare function closeOpenReceive(): Promise<void>;
60
+ export {};
@@ -0,0 +1,134 @@
1
+ import {
2
+ createStack,
3
+ paymentsSchemaSql
4
+ } from "@openreceive/http";
5
+ import { createOpenReceive } from "@openreceive/node";
6
+ import { getRequestIP } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import hostInput from "#openreceive/host";
9
+ export function defineOpenReceiveHost(host) {
10
+ return host;
11
+ }
12
+ export function openReceiveSchemaSql(dialect = "postgres") {
13
+ return paymentsSchemaSql(dialect);
14
+ }
15
+ const RETRY_AFTER_MS = 5e3;
16
+ let booting;
17
+ let failedAt = 0;
18
+ let lastError;
19
+ export async function useOpenReceive() {
20
+ if (!booting) {
21
+ if (failedAt && Date.now() - failedAt < RETRY_AFTER_MS) throw lastError;
22
+ booting = boot().catch((error) => {
23
+ booting = void 0;
24
+ failedAt = Date.now();
25
+ lastError = error;
26
+ throw error;
27
+ });
28
+ }
29
+ return (await booting).stack;
30
+ }
31
+ export async function closeOpenReceive() {
32
+ const current = booting;
33
+ booting = void 0;
34
+ if (!current) return;
35
+ try {
36
+ await (await current).close();
37
+ } catch {
38
+ }
39
+ }
40
+ async function boot() {
41
+ if (!hostInput) {
42
+ throw new Error(
43
+ "nuxt-openreceive: no host definition. Create server/openreceive.ts with `export default defineOpenReceiveHost({ amountFor, authorize, onPaid })`."
44
+ );
45
+ }
46
+ const config = useRuntimeConfig().openreceive;
47
+ const host = typeof hostInput === "function" ? await hostInput() : hostInput;
48
+ const { authorize, onPaid, storage, wallet, rateLimiting, ...rest } = host;
49
+ const cleanups = [];
50
+ let resolvedStorage;
51
+ if (storage) {
52
+ if (onPaid) {
53
+ console.warn("[openreceive] `onPaid` is ignored when `storage` is set: put it inside `storage`.");
54
+ }
55
+ resolvedStorage = storage;
56
+ } else {
57
+ if (!onPaid) {
58
+ throw new Error("nuxt-openreceive: define `onPaid` (managed pool) or `storage` in your host.");
59
+ }
60
+ const url = config.databaseUrl || process.env.DATABASE_URL;
61
+ if (!url) {
62
+ throw new Error(
63
+ "nuxt-openreceive: no database. Set NUXT_OPENRECEIVE_DATABASE_URL (or DATABASE_URL), or pass `storage: { db, onPaid }` in your host."
64
+ );
65
+ }
66
+ const { default: pg } = await import("pg");
67
+ const pool = new pg.Pool({ connectionString: url });
68
+ cleanups.push(() => pool.end());
69
+ if (config.migrate) {
70
+ await pool.query(paymentsSchemaSql("postgres"));
71
+ }
72
+ resolvedStorage = { db: pool, onPaid };
73
+ }
74
+ let resolvedWallet;
75
+ if (wallet) {
76
+ resolvedWallet = wallet;
77
+ } else {
78
+ const env = withoutEmpty({
79
+ ...process.env,
80
+ NWC_URI: config.nwcUri || process.env.NWC_URI,
81
+ LSC_URI_PRIMARY: config.lscUriPrimary || process.env.LSC_URI_PRIMARY,
82
+ LSC_URI_BACKUP: config.lscUriBackup || process.env.LSC_URI_BACKUP
83
+ });
84
+ if (!env.NWC_URI) {
85
+ await runAll(cleanups);
86
+ throw new Error(
87
+ "nuxt-openreceive: no wallet. Set NUXT_OPENRECEIVE_NWC_URI (or NWC_URI) to a receive-only NWC code. See https://openreceive.org/get_a_nwc_code_to_receive_payments"
88
+ );
89
+ }
90
+ const service = createOpenReceive({ env });
91
+ cleanups.push(async () => (await service).close());
92
+ resolvedWallet = { service };
93
+ }
94
+ const stack = createStack({
95
+ ...rest,
96
+ prefix: config.prefix,
97
+ wallet: resolvedWallet,
98
+ storage: resolvedStorage,
99
+ authorize: (context) => authorize({ ...context, event: context.native }),
100
+ rateLimiting: resolveRateLimiting(rateLimiting ?? config.rateLimiting, config.trustProxy)
101
+ });
102
+ cleanups.unshift(() => stack.close());
103
+ try {
104
+ await stack.ready;
105
+ } catch (error) {
106
+ await runAll(cleanups);
107
+ throw error;
108
+ }
109
+ return { stack, close: () => runAll(cleanups) };
110
+ }
111
+ function resolveRateLimiting(option, trustProxy) {
112
+ if (!option) return false;
113
+ const base = option === true ? {} : option;
114
+ if (base.ip) return base;
115
+ return {
116
+ ...base,
117
+ ip: ({ native }) => getRequestIP(native, { xForwardedFor: trustProxy })
118
+ };
119
+ }
120
+ function withoutEmpty(env) {
121
+ const out = {};
122
+ for (const [key, value] of Object.entries(env)) {
123
+ if (value) out[key] = value;
124
+ }
125
+ return out;
126
+ }
127
+ async function runAll(tasks) {
128
+ for (const task of tasks) {
129
+ try {
130
+ await task();
131
+ } catch {
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "nuxt-openreceive",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Nuxt module for OpenReceive: Bitcoin Lightning (and optional USDT/USDC/ETH/SOL swap) checkout that settles straight into your own wallet.",
5
+ "license": "MIT",
6
+ "author": "Grygorii Liashenko (https://github.com/frontender-ua)",
7
+ "homepage": "https://github.com/aisoul-dev/nuxt-openreceive#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/aisoul-dev/nuxt-openreceive.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/aisoul-dev/nuxt-openreceive/issues"
14
+ },
15
+ "type": "module",
16
+ "keywords": [
17
+ "nuxt",
18
+ "nuxt-module",
19
+ "openreceive",
20
+ "bitcoin",
21
+ "lightning",
22
+ "nwc",
23
+ "payments",
24
+ "checkout"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/types.d.mts",
29
+ "import": "./dist/module.mjs"
30
+ }
31
+ },
32
+ "main": "./dist/module.mjs",
33
+ "typesVersions": {
34
+ "*": {
35
+ ".": [
36
+ "./dist/types.d.mts"
37
+ ]
38
+ }
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "dependencies": {
47
+ "@nuxt/kit": "^4.2.0",
48
+ "@openreceive/http": "^0.4.31",
49
+ "@openreceive/node": "^0.4.31",
50
+ "@openreceive/vue": "^0.4.31",
51
+ "defu": "^6.1.4",
52
+ "h3": "^1.15.0"
53
+ },
54
+ "peerDependencies": {
55
+ "pg": "^8.11.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "pg": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "devDependencies": {
63
+ "@nuxt/module-builder": "^1.0.3",
64
+ "@nuxt/schema": "^4.6.4",
65
+ "@types/node": "^22.10.0",
66
+ "@types/pg": "^8.11.10",
67
+ "nuxt": "^4.6.4",
68
+ "pg": "^8.23.1",
69
+ "typescript": "~5.9.3",
70
+ "vue-tsc": "^3.3.14"
71
+ },
72
+ "scripts": {
73
+ "build": "nuxt-module-build build",
74
+ "dev": "nuxi dev playground",
75
+ "dev:build": "nuxi build playground",
76
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
77
+ "typecheck": "vue-tsc --noEmit -p playground/.nuxt/tsconfig.app.json && vue-tsc --noEmit -p playground/.nuxt/tsconfig.server.json",
78
+ "test": "node --test test/*.test.mjs"
79
+ }
6
80
  }