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 +21 -0
- package/README.md +169 -2
- package/dist/module.d.mts +66 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +113 -0
- package/dist/runtime/components/OpenReceiveCheckout.d.ts +4 -0
- package/dist/runtime/components/OpenReceiveCheckout.js +12 -0
- package/dist/runtime/server/handler.d.ts +2 -0
- package/dist/runtime/server/handler.js +23 -0
- package/dist/runtime/server/plugin.d.ts +7 -0
- package/dist/runtime/server/plugin.js +4 -0
- package/dist/runtime/server/utils/openreceive.d.ts +60 -0
- package/dist/runtime/server/utils/openreceive.js +134 -0
- package/dist/types.d.mts +3 -0
- package/package.json +77 -3
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
|
-
#
|
|
1
|
+
# nuxt-openreceive
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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,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,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
|
+
}
|
package/dist/types.d.mts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-openreceive",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|