@openreceive/node 0.2.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 +7 -0
- package/bin/openreceive.mjs +22 -0
- package/dist/chunk-6HALOJYF.js +1482 -0
- package/dist/cli.d.ts +63 -0
- package/dist/cli.js +1009 -0
- package/dist/index.d.ts +774 -0
- package/dist/index.js +2510 -0
- package/package.json +62 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readLscConnectionsFromEnvironment,
|
|
3
|
+
redactSecrets
|
|
4
|
+
} from "./chunk-6HALOJYF.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { formatInvalidNwcMessage, NwcUriParseError, parseNwcUri } from "@openreceive/core";
|
|
8
|
+
|
|
9
|
+
// src/scaffold/index.ts
|
|
10
|
+
import { createInterface } from "readline/promises";
|
|
11
|
+
import { stdin as defaultStdin, stdout as defaultStdout } from "process";
|
|
12
|
+
|
|
13
|
+
// src/scaffold/shared.ts
|
|
14
|
+
import { paymentsDdlStatements } from "@openreceive/core";
|
|
15
|
+
function assertPaymentsTableName(value, flag) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) {
|
|
18
|
+
throw new Error(`${flag} must be a lowercase SQL identifier.`);
|
|
19
|
+
}
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
function isSqlite(options) {
|
|
23
|
+
return options.dialect === "sqlite";
|
|
24
|
+
}
|
|
25
|
+
function canonicalPaymentsDdlStatements(options) {
|
|
26
|
+
return paymentsDdlStatements({
|
|
27
|
+
dialect: options.dialect,
|
|
28
|
+
tableName: options.tableName,
|
|
29
|
+
metaTableName: options.metaTableName
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/scaffold/types.ts
|
|
34
|
+
var OPENRECEIVE_ORMS = ["prisma", "drizzle", "typeorm", "sequelize", "knex"];
|
|
35
|
+
var OPENRECEIVE_DIALECTS = ["postgres", "sqlite"];
|
|
36
|
+
|
|
37
|
+
// src/scaffold/parse-args.ts
|
|
38
|
+
function parseScaffoldPaymentsArgv(argv) {
|
|
39
|
+
let help = false;
|
|
40
|
+
let interactive = false;
|
|
41
|
+
let orm;
|
|
42
|
+
let dialect;
|
|
43
|
+
let tableName;
|
|
44
|
+
let metaTableName;
|
|
45
|
+
let force = false;
|
|
46
|
+
let outDir = ".";
|
|
47
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
48
|
+
const arg = argv[index];
|
|
49
|
+
if (arg === void 0) break;
|
|
50
|
+
if (arg === "-h" || arg === "--help") {
|
|
51
|
+
help = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--interactive" || arg === "-i") {
|
|
55
|
+
interactive = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === "--force") {
|
|
59
|
+
force = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === "--orm") {
|
|
63
|
+
orm = readEnum(argv[++index], OPENRECEIVE_ORMS, "--orm");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg.startsWith("--orm=")) {
|
|
67
|
+
orm = readEnum(arg.slice("--orm=".length), OPENRECEIVE_ORMS, "--orm");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--dialect") {
|
|
71
|
+
dialect = readEnum(argv[++index], OPENRECEIVE_DIALECTS, "--dialect");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg.startsWith("--dialect=")) {
|
|
75
|
+
dialect = readEnum(arg.slice("--dialect=".length), OPENRECEIVE_DIALECTS, "--dialect");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--table-name") {
|
|
79
|
+
tableName = assertPaymentsTableName(
|
|
80
|
+
requiredValue(argv[++index], "--table-name"),
|
|
81
|
+
"--table-name"
|
|
82
|
+
);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg.startsWith("--table-name=")) {
|
|
86
|
+
tableName = assertPaymentsTableName(arg.slice("--table-name=".length), "--table-name");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg === "--meta-table-name") {
|
|
90
|
+
metaTableName = assertPaymentsTableName(
|
|
91
|
+
requiredValue(argv[++index], "--meta-table-name"),
|
|
92
|
+
"--meta-table-name"
|
|
93
|
+
);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg.startsWith("--meta-table-name=")) {
|
|
97
|
+
metaTableName = assertPaymentsTableName(
|
|
98
|
+
arg.slice("--meta-table-name=".length),
|
|
99
|
+
"--meta-table-name"
|
|
100
|
+
);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg === "--out-dir") {
|
|
104
|
+
outDir = requiredValue(argv[++index], "--out-dir");
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg.startsWith("--out-dir=")) {
|
|
108
|
+
outDir = arg.slice("--out-dir=".length);
|
|
109
|
+
if (!outDir) throw new Error("--out-dir requires a path.");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Unexpected option: ${arg}`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
help,
|
|
116
|
+
interactive,
|
|
117
|
+
partial: {
|
|
118
|
+
...orm === void 0 ? {} : { orm },
|
|
119
|
+
...dialect === void 0 ? {} : { dialect },
|
|
120
|
+
...tableName === void 0 ? {} : { tableName },
|
|
121
|
+
...metaTableName === void 0 ? {} : { metaTableName },
|
|
122
|
+
force,
|
|
123
|
+
outDir
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function finalizeScaffoldOptions(partial) {
|
|
128
|
+
if (partial.orm === void 0) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Missing --orm. Use --orm prisma|drizzle|typeorm|sequelize|knex, or run with --interactive."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
orm: partial.orm,
|
|
135
|
+
dialect: partial.dialect ?? "postgres",
|
|
136
|
+
tableName: assertPaymentsTableName(partial.tableName ?? "openreceive_payments", "--table-name"),
|
|
137
|
+
metaTableName: assertPaymentsTableName(
|
|
138
|
+
partial.metaTableName ?? "openreceive_meta",
|
|
139
|
+
"--meta-table-name"
|
|
140
|
+
),
|
|
141
|
+
outDir: partial.outDir,
|
|
142
|
+
force: partial.force
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function requiredValue(value, flag) {
|
|
146
|
+
if (value === void 0 || value.length === 0) {
|
|
147
|
+
throw new Error(`${flag} requires a value.`);
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function readEnum(value, allowed, flag) {
|
|
152
|
+
const raw = requiredValue(value, flag);
|
|
153
|
+
if (allowed.includes(raw)) return raw;
|
|
154
|
+
throw new Error(`${flag} must be one of: ${allowed.join(", ")}.`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/scaffold/orms/drizzle.ts
|
|
158
|
+
import {
|
|
159
|
+
fulfillmentNote,
|
|
160
|
+
paymentsHashCheckSql,
|
|
161
|
+
paymentsIndexName,
|
|
162
|
+
paymentsStatusCheckSql
|
|
163
|
+
} from "@openreceive/core";
|
|
164
|
+
|
|
165
|
+
// src/scaffold/wiring-guide.ts
|
|
166
|
+
import { fulfillmentNoteMarkdown, paymentsColumnNames, paymentsSeedSql } from "@openreceive/core";
|
|
167
|
+
var PLACEHOLDER_NOTE = "Custom adapters receive SQL already written for their own dialect (`?` on sqlite, `$1`-style on postgres) \u2014 pass it to the driver verbatim \u2014 and must return SELECT rows as plain objects.";
|
|
168
|
+
function wiringGuideMarkdown(options) {
|
|
169
|
+
const columnList = paymentsColumnNames().map((name) => `\`${name}\``).join(", ");
|
|
170
|
+
return `# OpenReceive payments wiring
|
|
171
|
+
|
|
172
|
+
Generated for **${options.orm}** (${options.dialect}). This scaffold emits only the
|
|
173
|
+
\`${options.tableName}\` + \`${options.metaTableName}\` schema/migration and this guide.
|
|
174
|
+
The OpenReceive library owns the payment-attempt repository at runtime \u2014 commit
|
|
175
|
+
locking, settlement write-once, the durable reconcile gate, and reconciliation
|
|
176
|
+
state transitions all run inside \`@openreceive/http\`, never in generated or
|
|
177
|
+
hand-written host code.
|
|
178
|
+
|
|
179
|
+
## 1. Run the migration
|
|
180
|
+
|
|
181
|
+
${migrationStep(options)}
|
|
182
|
+
|
|
183
|
+
Keep every column: ${columnList}. The timestamp columns (\`paid_at\`,
|
|
184
|
+
\`expires_at\`, \`created_at\`, \`updated_at\`, \`inserted_at\`) are
|
|
185
|
+
unix-seconds integers, never datetime columns. \`inserted_at\` is stamped from the host's
|
|
186
|
+
local clock and is what the per-IP rate limiter counts on
|
|
187
|
+
(\`countAttemptsFromIp\`): dropping it silently disables DB-backed rate
|
|
188
|
+
limiting. Keep the sibling \`${options.metaTableName}\` table (\`key\`, \`value\`,
|
|
189
|
+
\`rev\`) too: it is the durable reconcile gate every worker on this database
|
|
190
|
+
shares.
|
|
191
|
+
|
|
192
|
+
## 2. Wire the host integration
|
|
193
|
+
|
|
194
|
+
\`\`\`ts
|
|
195
|
+
import { createHost } from "@openreceive/http";
|
|
196
|
+
|
|
197
|
+
const host = createHost({ db, amountFor, onPaid });
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
- \`amountFor(reference)\` returns the trusted amount \u2014 never a payer-supplied
|
|
201
|
+
value \u2014 or \`null\` for a 404.
|
|
202
|
+
- \`onPaid\` fires for the first settled attempt for a reference only, so give
|
|
203
|
+
every order its own reference and never reuse one. Mark the order paid with
|
|
204
|
+
your own ORM:
|
|
205
|
+
|
|
206
|
+
\`\`\`ts
|
|
207
|
+
const onPaid = async ({ reference }) => {
|
|
208
|
+
${onPaidUpdateLine(options)}
|
|
209
|
+
};
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
\`onPaid\` also receives \`{ paymentHash, paidAt, query }\`. \`query\` runs
|
|
213
|
+
inside the settlement transaction \u2014 use it for transactional outbox rows or to
|
|
214
|
+
make the order update atomic with the payment record. Plain ORM calls are
|
|
215
|
+
fine: delivery is at-least-once and retried until \`onPaid\` succeeds, so make
|
|
216
|
+
it idempotent.
|
|
217
|
+
|
|
218
|
+
The one-liner above is deliberately the simplest thing that works \u2014 see
|
|
219
|
+
[section 3](#3-fulfilling-exactly-once) before shipping it.
|
|
220
|
+
|
|
221
|
+
Pass the same \`host\` to your mounted HTTP adapter. No background process is
|
|
222
|
+
needed: every mounted OpenReceive route runs an opportunistic, durably gated
|
|
223
|
+
reconcile pass by default (\`opportunisticReconcile\`), so abandoned checkouts
|
|
224
|
+
settle on any later OpenReceive call \u2014 serverless included.
|
|
225
|
+
|
|
226
|
+
Optionally, for push settlement the moment the wallet reports
|
|
227
|
+
\`payment_received\`, run the notifications worker as its own long-lived
|
|
228
|
+
process; it listens for NWC-02 notifications AND reconciles periodically (the
|
|
229
|
+
safety net for notifications missed while it was down):
|
|
230
|
+
|
|
231
|
+
\`\`\`ts
|
|
232
|
+
// worker.ts \u2014 run with: node worker.ts (e.g. a package.json "worker" script)
|
|
233
|
+
import { startNotificationWorker } from "@openreceive/http";
|
|
234
|
+
|
|
235
|
+
const worker = await startNotificationWorker({ service, host });
|
|
236
|
+
process.once("SIGINT", () => void worker.stop());
|
|
237
|
+
process.once("SIGTERM", () => void worker.stop());
|
|
238
|
+
\`\`\`
|
|
239
|
+
|
|
240
|
+
## 3. Fulfilling exactly once
|
|
241
|
+
|
|
242
|
+
${fulfillmentNoteMarkdown(options.tableName)}
|
|
243
|
+
|
|
244
|
+
In this project's terms, the guarded write goes inside \`onPaid\`:
|
|
245
|
+
|
|
246
|
+
\`\`\`ts
|
|
247
|
+
const onPaid = async ({ reference, paidAt, query }) => {
|
|
248
|
+
// \`query\` runs in OpenReceive's settlement transaction, so the order
|
|
249
|
+
// transition and the payment record commit or roll back together.
|
|
250
|
+
const claimed = await query(
|
|
251
|
+
${guardedUpdateSql(options)},
|
|
252
|
+
[paidAt, reference],
|
|
253
|
+
);
|
|
254
|
+
if (claimed.length === 0) return; // someone else already fulfilled it
|
|
255
|
+
await shipOrder(reference);
|
|
256
|
+
};
|
|
257
|
+
\`\`\`
|
|
258
|
+
|
|
259
|
+
## 4. What to pass as \`db\`
|
|
260
|
+
|
|
261
|
+
${dbSection(options)}
|
|
262
|
+
|
|
263
|
+
Never expose \`swap_data\` / \`swapData\` from application APIs, logs, or browser bundles.
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
function guardedUpdateSql(options) {
|
|
267
|
+
const [paidAt, reference] = isSqlite(options) ? ["?", "?"] : ["$1", "$2"];
|
|
268
|
+
return [
|
|
269
|
+
"`UPDATE orders",
|
|
270
|
+
` SET state = 'paid', paid_at = ${paidAt}`,
|
|
271
|
+
` WHERE id = ${reference}`,
|
|
272
|
+
" AND state = 'awaiting_payment'",
|
|
273
|
+
" RETURNING id`"
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function onPaidUpdateLine(options) {
|
|
277
|
+
switch (options.orm) {
|
|
278
|
+
case "prisma":
|
|
279
|
+
return 'await prisma.order.update({ where: { id: reference }, data: { state: "paid" } });';
|
|
280
|
+
case "drizzle":
|
|
281
|
+
return 'await orm.update(orders).set({ state: "paid" }).where(eq(orders.id, reference));';
|
|
282
|
+
case "typeorm":
|
|
283
|
+
return 'await dataSource.getRepository(Order).update({ id: reference }, { state: "paid" });';
|
|
284
|
+
case "sequelize":
|
|
285
|
+
return 'await Order.update({ state: "paid" }, { where: { id: reference } });';
|
|
286
|
+
case "knex":
|
|
287
|
+
return 'await knex("orders").where({ id: reference }).update({ state: "paid" });';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function migrationStep(options) {
|
|
291
|
+
switch (options.orm) {
|
|
292
|
+
case "prisma":
|
|
293
|
+
return `Merge \`prisma/schema.openreceive.prisma\` (both models) into your Prisma schema,
|
|
294
|
+
then run \`npx prisma migrate dev --create-only --name create_openreceive_tables\`.
|
|
295
|
+
Prisma's schema language cannot express the canonical CHECK constraints or the
|
|
296
|
+
\`schema_version\` seed row, so before applying the draft migration, add the
|
|
297
|
+
statements from \`prisma/openreceive-constraints.sql\` to it (the file says
|
|
298
|
+
how), then run \`npx prisma migrate dev\`.`;
|
|
299
|
+
case "drizzle":
|
|
300
|
+
return `Export \`payments\` and \`meta\` from
|
|
301
|
+
\`src/db/openreceive-tables.ts\` in your Drizzle schema entrypoint, then run
|
|
302
|
+
\`drizzle-kit generate\` and your usual migrate step. The schema carries both
|
|
303
|
+
canonical CHECK constraints; the \`schema_version\` seed row cannot be expressed
|
|
304
|
+
in schema, so also create a custom migration for it \u2014
|
|
305
|
+
\`drizzle-kit generate --custom --name openreceive-seed\` \u2014 containing:
|
|
306
|
+
|
|
307
|
+
\`\`\`sql
|
|
308
|
+
${paymentsSeedSql(options.dialect, options.metaTableName)};
|
|
309
|
+
\`\`\``;
|
|
310
|
+
case "typeorm":
|
|
311
|
+
return "Register `src/migrations/20260101000000-create-openreceive-tables.ts` in your DataSource `migrations` list, then run migrations through your usual workflow (for example `npx typeorm migration:run`). The migration executes the canonical OpenReceive DDL directly; no entity class is needed.";
|
|
312
|
+
case "sequelize":
|
|
313
|
+
return "Keep `migrations/20260101000000-create-openreceive-tables.cjs` in your sequelize-cli migrations folder, then run `npx sequelize-cli db:migrate`. The migration executes the canonical OpenReceive DDL directly; no model class is needed.";
|
|
314
|
+
case "knex":
|
|
315
|
+
return "Keep `db/migrations/20260101000000_create_openreceive_tables.mjs` in your Knex migrations directory, then run `npx knex migrate:latest`. The migration executes the canonical OpenReceive DDL directly.";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function dbSection(options) {
|
|
319
|
+
switch (options.orm) {
|
|
320
|
+
case "prisma":
|
|
321
|
+
return prismaDbSection(options);
|
|
322
|
+
case "drizzle":
|
|
323
|
+
return drizzleDbSection(options);
|
|
324
|
+
case "typeorm":
|
|
325
|
+
return typeOrmDbSection(options);
|
|
326
|
+
case "sequelize":
|
|
327
|
+
return sequelizeDbSection(options);
|
|
328
|
+
case "knex":
|
|
329
|
+
return knexDbSection(options);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function prismaDbSection(options) {
|
|
333
|
+
return `Prisma keeps its connection pool private, so it needs an adapter \u2014
|
|
334
|
+
\`prismaDb\` is the shipped one. It routes each statement to
|
|
335
|
+
\`$queryRawUnsafe\` or \`$executeRawUnsafe\` by whether it returns rows;
|
|
336
|
+
hand-rolling that router is where custom Prisma adapters go wrong. Set the
|
|
337
|
+
dialect to match your Prisma datasource provider. ${PLACEHOLDER_NOTE}
|
|
338
|
+
|
|
339
|
+
\`\`\`ts
|
|
340
|
+
import { PrismaClient } from "@prisma/client";
|
|
341
|
+
import { prismaDb } from "@openreceive/http";
|
|
342
|
+
|
|
343
|
+
const prisma = new PrismaClient();
|
|
344
|
+
export const db = prismaDb(prisma, "${options.dialect}");
|
|
345
|
+
\`\`\``;
|
|
346
|
+
}
|
|
347
|
+
function knexDbSection(options) {
|
|
348
|
+
return `Knex needs an adapter \u2014 \`knexDb\` is the shipped one, and it
|
|
349
|
+
already handles the sqlite-versus-postgres difference in what \`raw\`
|
|
350
|
+
returns. ${PLACEHOLDER_NOTE}
|
|
351
|
+
|
|
352
|
+
\`\`\`ts
|
|
353
|
+
import { knexDb } from "@openreceive/http";
|
|
354
|
+
import { knex } from "./db.ts"; // your configured Knex instance
|
|
355
|
+
|
|
356
|
+
export const db = knexDb(knex, "${options.dialect}");
|
|
357
|
+
\`\`\``;
|
|
358
|
+
}
|
|
359
|
+
function typeOrmDbSection(options) {
|
|
360
|
+
return `TypeORM needs an adapter \u2014 \`typeOrmDb\` is the shipped one, built
|
|
361
|
+
on \`dataSource.transaction\` + \`manager.query\`. ${PLACEHOLDER_NOTE}
|
|
362
|
+
|
|
363
|
+
\`\`\`ts
|
|
364
|
+
import { typeOrmDb } from "@openreceive/http";
|
|
365
|
+
import { dataSource } from "./data-source.ts"; // your initialized DataSource
|
|
366
|
+
|
|
367
|
+
export const db = typeOrmDb(dataSource, "${options.dialect}");
|
|
368
|
+
\`\`\``;
|
|
369
|
+
}
|
|
370
|
+
function drizzleDbSection(options) {
|
|
371
|
+
const snippet = isSqlite(options) ? `\`\`\`ts
|
|
372
|
+
import Database from "better-sqlite3"; // or DatabaseSync from "node:sqlite"
|
|
373
|
+
import { drizzle } from "drizzle-orm/better-sqlite3"; // node:sqlite users: drizzle-orm/node-sqlite
|
|
374
|
+
|
|
375
|
+
const sqlite = new Database("app.db");
|
|
376
|
+
const orm = drizzle(sqlite);
|
|
377
|
+
const host = createHost({ db: sqlite, amountFor, onPaid });
|
|
378
|
+
\`\`\`` : `\`\`\`ts
|
|
379
|
+
import { Pool } from "pg";
|
|
380
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
381
|
+
|
|
382
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
383
|
+
const orm = drizzle(pool);
|
|
384
|
+
const host = createHost({ db: pool, amountFor, onPaid });
|
|
385
|
+
\`\`\``;
|
|
386
|
+
const handle = isSqlite(options) ? "the better-sqlite3 / `node:sqlite` Database" : "the `pg` Pool";
|
|
387
|
+
return `Pass the underlying driver handle you already give \`drizzle(...)\` \u2014
|
|
388
|
+
${handle} \u2014 straight through as \`db\`. The library binds to the driver, so no
|
|
389
|
+
adapter is needed.
|
|
390
|
+
|
|
391
|
+
${snippet}`;
|
|
392
|
+
}
|
|
393
|
+
function sequelizeDbSection(options) {
|
|
394
|
+
return `Sequelize needs an adapter \u2014 \`sequelizeDb\` is the shipped one. It
|
|
395
|
+
binds parameters through Sequelize's \`bind\` option and threads the managed
|
|
396
|
+
transaction into every statement inside it, so settlement never runs outside
|
|
397
|
+
the transaction. ${PLACEHOLDER_NOTE}
|
|
398
|
+
|
|
399
|
+
\`\`\`ts
|
|
400
|
+
import { sequelizeDb } from "@openreceive/http";
|
|
401
|
+
import { sequelize } from "./db.ts"; // your configured Sequelize instance
|
|
402
|
+
|
|
403
|
+
export const db = sequelizeDb(sequelize, "${options.dialect}");
|
|
404
|
+
\`\`\``;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/scaffold/orms/drizzle.ts
|
|
408
|
+
function renderDrizzleFiles(options) {
|
|
409
|
+
const sqlite = isSqlite(options);
|
|
410
|
+
const table = options.tableName;
|
|
411
|
+
const referenceLine = `reference: text("reference").notNull(),`;
|
|
412
|
+
const indexName = (suffix) => paymentsIndexName(table, suffix);
|
|
413
|
+
const constraints = [
|
|
414
|
+
` uniqueIndex("${indexName("hash_uidx")}").on(table.paymentHash),`,
|
|
415
|
+
` index("${indexName("reference_created_idx")}").on(table.reference, table.createdAt),`,
|
|
416
|
+
` index("${indexName("status_created_idx")}").on(table.status, table.createdAt),`,
|
|
417
|
+
` index("${indexName("client_ip_inserted_idx")}").on(table.clientIp, table.insertedAt),`,
|
|
418
|
+
// Both canonical CHECK constraints, so drizzle-kit migrations enforce the
|
|
419
|
+
// same row shape the library's repository relies on.
|
|
420
|
+
` check("${indexName("status_check")}", sql\`${paymentsStatusCheckSql()}\`),`,
|
|
421
|
+
` check("${indexName("hash_check")}", sql\`${paymentsHashCheckSql(options.dialect)}\`),`
|
|
422
|
+
].join("\n");
|
|
423
|
+
const header = `// Generated by \`npx openreceive scaffold payments\` \u2014 schema only.
|
|
424
|
+
// OpenReceive owns the payment-attempt repository at runtime
|
|
425
|
+
// (createHost in @openreceive/http); these two tables exist so
|
|
426
|
+
// \`drizzle-kit generate\` creates the canonical DDL. Keep every column and
|
|
427
|
+
// CHECK constraint. Timestamps are unix-seconds integer columns, on purpose.
|
|
428
|
+
// The schema_version seed row cannot be expressed here \u2014 the wiring guide's
|
|
429
|
+
// migration step adds it as a custom migration.
|
|
430
|
+
//
|
|
431
|
+
${fulfillmentNote("// ", options.tableName)}
|
|
432
|
+
import { sql } from "drizzle-orm";
|
|
433
|
+
`;
|
|
434
|
+
const schema = sqlite ? `${header}import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
435
|
+
|
|
436
|
+
export const payments = sqliteTable(
|
|
437
|
+
"${table}",
|
|
438
|
+
{
|
|
439
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
440
|
+
${referenceLine}
|
|
441
|
+
paymentHash: text("payment_hash").notNull(),
|
|
442
|
+
status: text("status").notNull().default("pending"),
|
|
443
|
+
statusReason: text("status_reason"),
|
|
444
|
+
paidAt: integer("paid_at"),
|
|
445
|
+
expiresAt: integer("expires_at").notNull(),
|
|
446
|
+
createdAt: integer("created_at").notNull(),
|
|
447
|
+
updatedAt: integer("updated_at").notNull(),
|
|
448
|
+
insertedAt: integer("inserted_at").notNull(),
|
|
449
|
+
checkoutData: text("checkout_data").notNull(),
|
|
450
|
+
swapData: text("swap_data"),
|
|
451
|
+
clientIp: text("client_ip"),
|
|
452
|
+
},
|
|
453
|
+
(table) => [
|
|
454
|
+
${constraints}
|
|
455
|
+
],
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// The durable reconcile gate every worker on this database shares
|
|
459
|
+
// (key/value/rev compare-and-set). Same host database, never a second one.
|
|
460
|
+
export const meta = sqliteTable("${options.metaTableName}", {
|
|
461
|
+
key: text("key").primaryKey(),
|
|
462
|
+
value: text("value").notNull(),
|
|
463
|
+
rev: integer("rev").notNull().default(0),
|
|
464
|
+
});
|
|
465
|
+
` : `${header}import { ${pgImports()} } from "drizzle-orm/pg-core";
|
|
466
|
+
|
|
467
|
+
export const payments = pgTable(
|
|
468
|
+
"${table}",
|
|
469
|
+
{
|
|
470
|
+
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
|
471
|
+
${referenceLine}
|
|
472
|
+
paymentHash: text("payment_hash").notNull(),
|
|
473
|
+
status: text("status").notNull().default("pending"),
|
|
474
|
+
statusReason: text("status_reason"),
|
|
475
|
+
paidAt: bigint("paid_at", { mode: "number" }),
|
|
476
|
+
expiresAt: bigint("expires_at", { mode: "number" }).notNull(),
|
|
477
|
+
createdAt: bigint("created_at", { mode: "number" }).notNull(),
|
|
478
|
+
updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
|
|
479
|
+
insertedAt: bigint("inserted_at", { mode: "number" }).notNull(),
|
|
480
|
+
checkoutData: text("checkout_data").notNull(),
|
|
481
|
+
swapData: text("swap_data"),
|
|
482
|
+
clientIp: text("client_ip"),
|
|
483
|
+
},
|
|
484
|
+
(table) => [
|
|
485
|
+
${constraints}
|
|
486
|
+
],
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// The durable reconcile gate every worker on this database shares
|
|
490
|
+
// (key/value/rev compare-and-set). Same host database, never a second one.
|
|
491
|
+
export const meta = pgTable("${options.metaTableName}", {
|
|
492
|
+
key: text("key").primaryKey(),
|
|
493
|
+
value: text("value").notNull(),
|
|
494
|
+
rev: bigint("rev", { mode: "number" }).notNull().default(0),
|
|
495
|
+
});
|
|
496
|
+
`;
|
|
497
|
+
return [
|
|
498
|
+
{ path: "src/db/openreceive-tables.ts", contents: schema },
|
|
499
|
+
{ path: "OPENRECEIVE_PAYMENTS.md", contents: wiringGuideMarkdown(options) }
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
function pgImports() {
|
|
503
|
+
return ["bigint", "check", "index", "pgTable", "text", "uniqueIndex"].join(", ");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/scaffold/orms/knex.ts
|
|
507
|
+
import { fulfillmentNote as fulfillmentNote2 } from "@openreceive/core";
|
|
508
|
+
function renderKnexFiles(options) {
|
|
509
|
+
const body = canonicalPaymentsDdlStatements(options).map((statement) => ` await knex.raw(\`${statement}\`);`).join("\n");
|
|
510
|
+
const migration = `/**
|
|
511
|
+
* Generated by \`npx openreceive scaffold payments\` \u2014 migration only.
|
|
512
|
+
* Executes the canonical OpenReceive DDL: \`${options.tableName}\` and the
|
|
513
|
+
* \`${options.metaTableName}\` reconcile gate, CHECK constraints and the
|
|
514
|
+
* schema_version seed row included. OpenReceive owns the payment-attempt
|
|
515
|
+
* repository at runtime (createHost in @openreceive/http), so no
|
|
516
|
+
* repository code is generated. Keep every column and constraint; timestamps
|
|
517
|
+
* are unix-seconds integer columns, on purpose.
|
|
518
|
+
*
|
|
519
|
+
* Dialect: ${options.dialect}
|
|
520
|
+
*
|
|
521
|
+
${fulfillmentNote2(" * ", options.tableName)}
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
/** @param {import("knex").Knex} knex */
|
|
525
|
+
export async function up(knex) {
|
|
526
|
+
${body}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** @param {import("knex").Knex} knex */
|
|
530
|
+
export async function down(knex) {
|
|
531
|
+
await knex.raw("DROP TABLE IF EXISTS ${options.metaTableName}");
|
|
532
|
+
await knex.raw("DROP TABLE IF EXISTS ${options.tableName}");
|
|
533
|
+
}
|
|
534
|
+
`;
|
|
535
|
+
return [
|
|
536
|
+
{
|
|
537
|
+
// .mjs: the migration uses ESM syntax, which a "type": "commonjs" host
|
|
538
|
+
// would reject in a .js file. Knex loads .mjs migrations natively.
|
|
539
|
+
path: "db/migrations/20260101000000_create_openreceive_tables.mjs",
|
|
540
|
+
contents: migration
|
|
541
|
+
},
|
|
542
|
+
{ path: "OPENRECEIVE_PAYMENTS.md", contents: wiringGuideMarkdown(options) }
|
|
543
|
+
];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/scaffold/orms/prisma.ts
|
|
547
|
+
import {
|
|
548
|
+
fulfillmentNote as fulfillmentNote3,
|
|
549
|
+
paymentsHashCheckSql as paymentsHashCheckSql2,
|
|
550
|
+
paymentsIndexName as paymentsIndexName2,
|
|
551
|
+
paymentsSeedSql as paymentsSeedSql2,
|
|
552
|
+
paymentsStatusCheckSql as paymentsStatusCheckSql2
|
|
553
|
+
} from "@openreceive/core";
|
|
554
|
+
function renderPrismaFiles(options) {
|
|
555
|
+
const idField = isSqlite(options) ? "Int @id @default(autoincrement())" : "BigInt @id @default(autoincrement())";
|
|
556
|
+
const schema = `// Generated by \`npx openreceive scaffold payments\` \u2014 schema only.
|
|
557
|
+
// OpenReceive owns the payment-attempt repository at runtime
|
|
558
|
+
// (createHost in @openreceive/http); these two models exist so
|
|
559
|
+
// \`prisma migrate\` creates the canonical tables. Keep every column.
|
|
560
|
+
// Timestamps are unix-seconds integer columns (BigInt), on purpose.
|
|
561
|
+
//
|
|
562
|
+
// Prisma's schema language cannot express the canonical CHECK constraints or
|
|
563
|
+
// the schema_version seed row: apply prisma/openreceive-constraints.sql as
|
|
564
|
+
// described in OPENRECEIVE_PAYMENTS.md when creating the migration.
|
|
565
|
+
//
|
|
566
|
+
// Merge both models into your Prisma schema, then run:
|
|
567
|
+
// npx prisma migrate dev --create-only --name create_openreceive_tables
|
|
568
|
+
//
|
|
569
|
+
// Dialect: ${options.dialect}
|
|
570
|
+
//
|
|
571
|
+
${fulfillmentNote3("// ", options.tableName)}
|
|
572
|
+
|
|
573
|
+
model OpenReceivePayment {
|
|
574
|
+
id ${idField}
|
|
575
|
+
reference String @map("reference")
|
|
576
|
+
paymentHash String @unique @map("payment_hash")
|
|
577
|
+
status String @default("pending")
|
|
578
|
+
statusReason String? @map("status_reason")
|
|
579
|
+
paidAt BigInt? @map("paid_at")
|
|
580
|
+
expiresAt BigInt @map("expires_at")
|
|
581
|
+
createdAt BigInt @map("created_at")
|
|
582
|
+
updatedAt BigInt @map("updated_at")
|
|
583
|
+
insertedAt BigInt @map("inserted_at")
|
|
584
|
+
checkoutData String @map("checkout_data")
|
|
585
|
+
swapData String? @map("swap_data")
|
|
586
|
+
clientIp String? @map("client_ip")
|
|
587
|
+
|
|
588
|
+
@@index([reference, createdAt])
|
|
589
|
+
@@index([status, createdAt])
|
|
590
|
+
@@index([clientIp, insertedAt])
|
|
591
|
+
@@map("${options.tableName}")
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// The durable reconcile gate every worker on this database shares (key/value/rev
|
|
595
|
+
// compare-and-set). Same host database as ${options.tableName}.
|
|
596
|
+
model OpenReceiveMeta {
|
|
597
|
+
key String @id
|
|
598
|
+
value String
|
|
599
|
+
rev BigInt @default(0)
|
|
600
|
+
|
|
601
|
+
@@map("${options.metaTableName}")
|
|
602
|
+
}
|
|
603
|
+
`;
|
|
604
|
+
return [
|
|
605
|
+
{ path: "prisma/schema.openreceive.prisma", contents: schema },
|
|
606
|
+
{ path: "prisma/openreceive-constraints.sql", contents: constraintsSql(options) },
|
|
607
|
+
{ path: "OPENRECEIVE_PAYMENTS.md", contents: wiringGuideMarkdown(options) }
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
function constraintsSql(options) {
|
|
611
|
+
const statusCheck = paymentsStatusCheckSql2();
|
|
612
|
+
const hashCheck = paymentsHashCheckSql2(options.dialect);
|
|
613
|
+
const seed = paymentsSeedSql2(options.dialect, options.metaTableName);
|
|
614
|
+
if (options.dialect === "postgres") {
|
|
615
|
+
return `-- Generated by \`npx openreceive scaffold payments\`.
|
|
616
|
+
-- Prisma's schema language cannot express these; append this file to the draft
|
|
617
|
+
-- migration created by \`npx prisma migrate dev --create-only\` before applying.
|
|
618
|
+
ALTER TABLE "${options.tableName}" ADD CONSTRAINT "${paymentsIndexName2(options.tableName, "status_check")}" CHECK (${statusCheck});
|
|
619
|
+
ALTER TABLE "${options.tableName}" ADD CONSTRAINT "${paymentsIndexName2(options.tableName, "hash_check")}" CHECK (${hashCheck});
|
|
620
|
+
${seed};
|
|
621
|
+
`;
|
|
622
|
+
}
|
|
623
|
+
return `-- Generated by \`npx openreceive scaffold payments\`.
|
|
624
|
+
-- Prisma's schema language cannot express these, and sqlite cannot ALTER TABLE
|
|
625
|
+
-- ... ADD CHECK. After \`npx prisma migrate dev --create-only\`, edit the draft
|
|
626
|
+
-- migration: paste the two CHECK lines below into the
|
|
627
|
+
-- CREATE TABLE "${options.tableName}" (...) column list, then append the INSERT.
|
|
628
|
+
--
|
|
629
|
+
-- CHECK (${statusCheck}),
|
|
630
|
+
-- CHECK (${hashCheck})
|
|
631
|
+
--
|
|
632
|
+
${seed};
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/scaffold/orms/sequelize.ts
|
|
637
|
+
import { fulfillmentNote as fulfillmentNote4 } from "@openreceive/core";
|
|
638
|
+
function renderSequelizeFiles(options) {
|
|
639
|
+
const body = canonicalPaymentsDdlStatements(options).map((statement) => ` await queryInterface.sequelize.query(\`${statement}\`);`).join("\n");
|
|
640
|
+
const migration = `"use strict";
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Generated by \`npx openreceive scaffold payments\` \u2014 migration only.
|
|
644
|
+
* Executes the canonical OpenReceive DDL: \`${options.tableName}\` and the
|
|
645
|
+
* \`${options.metaTableName}\` reconcile gate, CHECK constraints and the
|
|
646
|
+
* schema_version seed row included. OpenReceive owns the payment-attempt
|
|
647
|
+
* repository at runtime (createHost in @openreceive/http), so no
|
|
648
|
+
* model or repository code is generated. Keep every column and constraint;
|
|
649
|
+
* timestamps are unix-seconds integer columns, on purpose.
|
|
650
|
+
*
|
|
651
|
+
* Dialect: ${options.dialect}
|
|
652
|
+
*
|
|
653
|
+
${fulfillmentNote4(" * ", options.tableName)}
|
|
654
|
+
*/
|
|
655
|
+
module.exports = {
|
|
656
|
+
async up(queryInterface) {
|
|
657
|
+
${body}
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
async down(queryInterface) {
|
|
661
|
+
await queryInterface.sequelize.query("DROP TABLE IF EXISTS ${options.metaTableName}");
|
|
662
|
+
await queryInterface.sequelize.query("DROP TABLE IF EXISTS ${options.tableName}");
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
`;
|
|
666
|
+
return [
|
|
667
|
+
{
|
|
668
|
+
// .cjs: the migration uses module.exports, which a "type": "module" host
|
|
669
|
+
// would reject in a .js file. sequelize-cli loads .cjs migrations natively.
|
|
670
|
+
path: "migrations/20260101000000-create-openreceive-tables.cjs",
|
|
671
|
+
contents: migration
|
|
672
|
+
},
|
|
673
|
+
{ path: "OPENRECEIVE_PAYMENTS.md", contents: wiringGuideMarkdown(options) }
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/scaffold/orms/typeorm.ts
|
|
678
|
+
import { fulfillmentNote as fulfillmentNote5 } from "@openreceive/core";
|
|
679
|
+
function renderTypeOrmFiles(options) {
|
|
680
|
+
const body = canonicalPaymentsDdlStatements(options).map((statement) => ` await queryRunner.query(\`${statement}\`);`).join("\n");
|
|
681
|
+
const migration = `// Generated by \`npx openreceive scaffold payments\` \u2014 migration only.
|
|
682
|
+
// Executes the canonical OpenReceive DDL: \`${options.tableName}\` and the
|
|
683
|
+
// \`${options.metaTableName}\` reconcile gate, both tables in this one migration.
|
|
684
|
+
// OpenReceive owns the payment-attempt repository at runtime
|
|
685
|
+
// (createHost in @openreceive/http), so there is no entity class to
|
|
686
|
+
// register. Keep every column; timestamps are unix-seconds integer columns, on
|
|
687
|
+
// purpose.
|
|
688
|
+
//
|
|
689
|
+
${fulfillmentNote5("// ", options.tableName)}
|
|
690
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
691
|
+
|
|
692
|
+
export class CreateOpenReceiveTables20260101000000 implements MigrationInterface {
|
|
693
|
+
name = "CreateOpenReceiveTables20260101000000";
|
|
694
|
+
|
|
695
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
696
|
+
${body}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
700
|
+
await queryRunner.query("DROP TABLE IF EXISTS ${options.metaTableName}");
|
|
701
|
+
await queryRunner.query("DROP TABLE IF EXISTS ${options.tableName}");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
`;
|
|
705
|
+
return [
|
|
706
|
+
{
|
|
707
|
+
path: "src/migrations/20260101000000-create-openreceive-tables.ts",
|
|
708
|
+
contents: migration
|
|
709
|
+
},
|
|
710
|
+
{ path: "OPENRECEIVE_PAYMENTS.md", contents: wiringGuideMarkdown(options) }
|
|
711
|
+
];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/scaffold/render.ts
|
|
715
|
+
function renderScaffoldPaymentsFiles(options) {
|
|
716
|
+
switch (options.orm) {
|
|
717
|
+
case "prisma":
|
|
718
|
+
return renderPrismaFiles(options);
|
|
719
|
+
case "drizzle":
|
|
720
|
+
return renderDrizzleFiles(options);
|
|
721
|
+
case "typeorm":
|
|
722
|
+
return renderTypeOrmFiles(options);
|
|
723
|
+
case "sequelize":
|
|
724
|
+
return renderSequelizeFiles(options);
|
|
725
|
+
case "knex":
|
|
726
|
+
return renderKnexFiles(options);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/scaffold/wizard.ts
|
|
731
|
+
async function resolveScaffoldPaymentsOptions(input) {
|
|
732
|
+
const { partial } = input.parsed;
|
|
733
|
+
const wantsWizard = input.parsed.interactive || partial.orm === void 0 && input.canPrompt;
|
|
734
|
+
if (!wantsWizard) {
|
|
735
|
+
return finalizeScaffoldOptions(partial);
|
|
736
|
+
}
|
|
737
|
+
if (!input.canPrompt) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
"Interactive scaffold requires a TTY. Pass --orm explicitly for non-interactive use."
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const orm = partial.orm ?? await promptChoice(input.prompt, "ORM", OPENRECEIVE_ORMS, void 0);
|
|
743
|
+
const dialect = partial.dialect ?? await promptChoice(input.prompt, "SQL dialect", OPENRECEIVE_DIALECTS, "postgres");
|
|
744
|
+
const outDir = partial.outDir === "." ? await promptText(input.prompt, "Output directory", ".") : partial.outDir;
|
|
745
|
+
return {
|
|
746
|
+
orm,
|
|
747
|
+
dialect,
|
|
748
|
+
tableName: partial.tableName ?? "openreceive_payments",
|
|
749
|
+
metaTableName: partial.metaTableName ?? "openreceive_meta",
|
|
750
|
+
outDir,
|
|
751
|
+
force: partial.force
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function promptText(prompt, label, fallback) {
|
|
755
|
+
const answer = (await prompt(`${label} [${fallback}]: `)).trim();
|
|
756
|
+
return answer.length === 0 ? fallback : answer;
|
|
757
|
+
}
|
|
758
|
+
async function promptChoice(prompt, label, choices, fallback) {
|
|
759
|
+
const listed = choices.join(", ");
|
|
760
|
+
const suffix = fallback === void 0 ? "" : ` [${fallback}]`;
|
|
761
|
+
const answer = (await prompt(`${label} (${listed})${suffix}: `)).trim().toLowerCase();
|
|
762
|
+
const selected = answer.length === 0 ? fallback : answer;
|
|
763
|
+
if (selected !== void 0 && choices.includes(selected)) {
|
|
764
|
+
return selected;
|
|
765
|
+
}
|
|
766
|
+
throw new Error(`${label} must be one of: ${listed}.`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/scaffold/write-files.ts
|
|
770
|
+
import { mkdir, writeFile, access } from "fs/promises";
|
|
771
|
+
import path from "path";
|
|
772
|
+
async function writeScaffoldFiles(input) {
|
|
773
|
+
const root = path.resolve(input.cwd, input.outDir);
|
|
774
|
+
const planned = input.files.map((file) => ({
|
|
775
|
+
file,
|
|
776
|
+
absolute: path.join(root, file.path),
|
|
777
|
+
relative: path.relative(input.cwd, path.join(root, file.path))
|
|
778
|
+
}));
|
|
779
|
+
const existing = [];
|
|
780
|
+
for (const entry of planned) {
|
|
781
|
+
if (await exists(entry.absolute)) existing.push(entry.relative);
|
|
782
|
+
}
|
|
783
|
+
if (existing.length > 0 && !input.force) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Refusing to overwrite existing files without --force:
|
|
786
|
+
${existing.map((entry) => ` - ${entry}`).join("\n")}`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const written = [];
|
|
790
|
+
for (const entry of planned) {
|
|
791
|
+
await mkdir(path.dirname(entry.absolute), { recursive: true });
|
|
792
|
+
await writeFile(entry.absolute, entry.file.contents, "utf8");
|
|
793
|
+
written.push(entry.relative);
|
|
794
|
+
}
|
|
795
|
+
return { files: input.files, written };
|
|
796
|
+
}
|
|
797
|
+
async function exists(filePath) {
|
|
798
|
+
try {
|
|
799
|
+
await access(filePath);
|
|
800
|
+
return true;
|
|
801
|
+
} catch {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/scaffold/index.ts
|
|
807
|
+
var SCAFFOLD_PAYMENTS_HELP = `
|
|
808
|
+
Usage: openreceive scaffold payments [options]
|
|
809
|
+
|
|
810
|
+
Emits one schema/migration file for your ORM \u2014 openreceive_payments and the
|
|
811
|
+
openreceive_meta reconcile gate together \u2014 plus an OPENRECEIVE_PAYMENTS.md
|
|
812
|
+
wiring guide, nothing else. OpenReceive owns the
|
|
813
|
+
payment-attempt repository logic (locking, settlement write-once,
|
|
814
|
+
reconciliation) at runtime; the generated files never contain it.
|
|
815
|
+
OpenReceive never opens a database connection or runs migrations.
|
|
816
|
+
|
|
817
|
+
Options:
|
|
818
|
+
--orm <name> prisma | drizzle | typeorm | sequelize | knex
|
|
819
|
+
--dialect <name> postgres | sqlite (default: postgres)
|
|
820
|
+
--table-name <name> Payment attempts table (default: openreceive_payments)
|
|
821
|
+
--meta-table-name <name> Reconcile-gate table (default: openreceive_meta)
|
|
822
|
+
--out-dir <path> Output root (default: .)
|
|
823
|
+
--force Overwrite existing generated files
|
|
824
|
+
-i, --interactive Ask for missing options (default on TTY when --orm omitted)
|
|
825
|
+
-h, --help Show this help
|
|
826
|
+
|
|
827
|
+
Examples:
|
|
828
|
+
npx openreceive scaffold payments
|
|
829
|
+
npx openreceive scaffold payments --orm prisma
|
|
830
|
+
npx openreceive scaffold payments --orm knex --dialect sqlite
|
|
831
|
+
npx openreceive scaffold payments --orm drizzle --dialect sqlite --out-dir ./backend
|
|
832
|
+
`.trim();
|
|
833
|
+
async function runScaffoldPayments(input) {
|
|
834
|
+
const parsed = parseScaffoldPaymentsArgv(input.argv);
|
|
835
|
+
if (parsed.help) {
|
|
836
|
+
input.stdout.write(`${SCAFFOLD_PAYMENTS_HELP}
|
|
837
|
+
`);
|
|
838
|
+
return 0;
|
|
839
|
+
}
|
|
840
|
+
const canPrompt = input.isTTY ?? Boolean(
|
|
841
|
+
input.stdin?.isTTY ?? defaultStdin.isTTY
|
|
842
|
+
);
|
|
843
|
+
const prompt = input.prompt ?? createReadlinePrompt(input);
|
|
844
|
+
const options = await resolveScaffoldPaymentsOptions({
|
|
845
|
+
parsed,
|
|
846
|
+
canPrompt,
|
|
847
|
+
prompt
|
|
848
|
+
});
|
|
849
|
+
finalizeScaffoldOptions(options);
|
|
850
|
+
printPlan(input.stdout, options);
|
|
851
|
+
const files = renderScaffoldPaymentsFiles(options);
|
|
852
|
+
const result = await writeScaffoldFiles({
|
|
853
|
+
cwd: input.cwd,
|
|
854
|
+
outDir: options.outDir,
|
|
855
|
+
force: options.force,
|
|
856
|
+
files
|
|
857
|
+
});
|
|
858
|
+
printSummary(input.stdout, options, result);
|
|
859
|
+
return 0;
|
|
860
|
+
}
|
|
861
|
+
function printPlan(stdout, options) {
|
|
862
|
+
stdout.write("OpenReceive scaffold payments\n");
|
|
863
|
+
stdout.write(` orm: ${options.orm}
|
|
864
|
+
`);
|
|
865
|
+
stdout.write(` dialect: ${options.dialect}
|
|
866
|
+
`);
|
|
867
|
+
stdout.write(` tables: ${options.tableName}, ${options.metaTableName}
|
|
868
|
+
`);
|
|
869
|
+
stdout.write(` out-dir: ${options.outDir}
|
|
870
|
+
`);
|
|
871
|
+
if (options.dialect === "sqlite") {
|
|
872
|
+
stdout.write(
|
|
873
|
+
" note: SQLite uses a single-writer transaction (no Postgres row locks)\n"
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
stdout.write("\nWriting files\u2026\n");
|
|
877
|
+
}
|
|
878
|
+
function printSummary(stdout, options, result) {
|
|
879
|
+
for (const file of result.written) {
|
|
880
|
+
stdout.write(` wrote ${file}
|
|
881
|
+
`);
|
|
882
|
+
}
|
|
883
|
+
stdout.write("\nDone.\n");
|
|
884
|
+
stdout.write("Next:\n");
|
|
885
|
+
stdout.write(" 1. Read OPENRECEIVE_PAYMENTS.md\n");
|
|
886
|
+
stdout.write(` 2. Run the schema/migration through your normal ${options.orm} workflow
|
|
887
|
+
`);
|
|
888
|
+
stdout.write(
|
|
889
|
+
" 3. Wire createHost({ db, amountFor, onPaid }) \u2014\n OpenReceive owns the repository logic at runtime; settlement piggybacks\n on mounted routes by default (no background process needed)\n"
|
|
890
|
+
);
|
|
891
|
+
stdout.write(
|
|
892
|
+
" 4. Make onPaid idempotent if anything OTHER than OpenReceive can also\n fulfill an order \u2014 the generated files show the guarded UPDATE\n"
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
function createReadlinePrompt(input) {
|
|
896
|
+
return async (question) => {
|
|
897
|
+
const rl = createInterface({
|
|
898
|
+
input: input.stdin ?? defaultStdin,
|
|
899
|
+
output: defaultStdout,
|
|
900
|
+
terminal: input.isTTY ?? true
|
|
901
|
+
});
|
|
902
|
+
try {
|
|
903
|
+
return await rl.question(question);
|
|
904
|
+
} finally {
|
|
905
|
+
rl.close();
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/cli.ts
|
|
911
|
+
var HELP = `
|
|
912
|
+
Usage: openreceive <command> [options]
|
|
913
|
+
|
|
914
|
+
Commands:
|
|
915
|
+
doctor Validate server configuration (Node, NWC_URI, swap providers).
|
|
916
|
+
debug-report Print the same diagnostics as a redacted support report
|
|
917
|
+
(alias of doctor; always exits 0).
|
|
918
|
+
scaffold payments Emit the openreceive_payments + openreceive_meta migration and wiring guide for your ORM.
|
|
919
|
+
|
|
920
|
+
Options:
|
|
921
|
+
-h, --help Show this help.
|
|
922
|
+
`.trim();
|
|
923
|
+
async function runCli(options) {
|
|
924
|
+
const stdout = options.stdout ?? process.stdout;
|
|
925
|
+
const stderr = options.stderr ?? process.stderr;
|
|
926
|
+
const env = options.env ?? process.env;
|
|
927
|
+
const cwd = options.cwd ?? process.cwd();
|
|
928
|
+
const [command = "help", ...args] = options.argv;
|
|
929
|
+
try {
|
|
930
|
+
if (["help", "--help", "-h"].includes(command)) {
|
|
931
|
+
stdout.write(`${HELP}
|
|
932
|
+
`);
|
|
933
|
+
return 0;
|
|
934
|
+
}
|
|
935
|
+
if (command === "doctor" || command === "debug-report") {
|
|
936
|
+
if (args.length > 0) throw new Error(`Unexpected option: ${args[0]}`);
|
|
937
|
+
return runDiagnostics({ command, env, cwd, stdout });
|
|
938
|
+
}
|
|
939
|
+
if (command === "scaffold") {
|
|
940
|
+
const [target = "help", ...scaffoldArgs] = args;
|
|
941
|
+
if (target === "help" || target === "--help" || target === "-h") {
|
|
942
|
+
stdout.write(`${SCAFFOLD_PAYMENTS_HELP}
|
|
943
|
+
`);
|
|
944
|
+
return 0;
|
|
945
|
+
}
|
|
946
|
+
if (target !== "payments") {
|
|
947
|
+
throw new Error(`Unknown scaffold target: ${target}. Only "payments" is supported.`);
|
|
948
|
+
}
|
|
949
|
+
return await runScaffoldPayments({
|
|
950
|
+
argv: scaffoldArgs,
|
|
951
|
+
cwd,
|
|
952
|
+
stdout,
|
|
953
|
+
stderr,
|
|
954
|
+
stdin: options.stdin,
|
|
955
|
+
isTTY: options.isTTY,
|
|
956
|
+
prompt: options.prompt
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
stderr.write(`Unknown OpenReceive command: ${command}
|
|
960
|
+
|
|
961
|
+
${HELP}
|
|
962
|
+
`);
|
|
963
|
+
return 1;
|
|
964
|
+
} catch (error) {
|
|
965
|
+
stderr.write(`${safeErrorMessage(error)}
|
|
966
|
+
`);
|
|
967
|
+
return 1;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function runDiagnostics(input) {
|
|
971
|
+
const nwc = input.env.NWC_URI?.trim();
|
|
972
|
+
let nwcError;
|
|
973
|
+
try {
|
|
974
|
+
if (nwc) parseNwcUri(nwc);
|
|
975
|
+
} catch (error) {
|
|
976
|
+
nwcError = error instanceof NwcUriParseError ? new Error(formatInvalidNwcMessage({ reason: error.description })) : error;
|
|
977
|
+
}
|
|
978
|
+
let lscConnections = 0;
|
|
979
|
+
let lscError;
|
|
980
|
+
try {
|
|
981
|
+
lscConnections = readLscConnectionsFromEnvironment(input.env).length;
|
|
982
|
+
} catch (error) {
|
|
983
|
+
lscError = error;
|
|
984
|
+
}
|
|
985
|
+
const lines = [
|
|
986
|
+
`OpenReceive ${input.command}`,
|
|
987
|
+
`node: ${process.version}`,
|
|
988
|
+
`cwd: ${input.cwd}`,
|
|
989
|
+
"storage: payment-attempt rows live in the host database (no separate store)",
|
|
990
|
+
`NWC_URI: ${nwcError === void 0 ? nwc ? "present-redacted" : "missing" : safeErrorMessage(nwcError)}`,
|
|
991
|
+
`LSC_URI connections: ${lscError === void 0 ? lscConnections : safeErrorMessage(lscError)}`
|
|
992
|
+
];
|
|
993
|
+
input.stdout.write(`${lines.join("\n")}
|
|
994
|
+
`);
|
|
995
|
+
if (input.command === "debug-report") return 0;
|
|
996
|
+
return nwcError !== void 0 || !nwc || lscError !== void 0 ? 1 : 0;
|
|
997
|
+
}
|
|
998
|
+
function safeErrorMessage(error) {
|
|
999
|
+
if (error instanceof Error) return redactSecrets(error.message);
|
|
1000
|
+
if (typeof error === "string") return redactSecrets(error);
|
|
1001
|
+
return "OpenReceive command failed.";
|
|
1002
|
+
}
|
|
1003
|
+
export {
|
|
1004
|
+
finalizeScaffoldOptions,
|
|
1005
|
+
parseScaffoldPaymentsArgv,
|
|
1006
|
+
renderScaffoldPaymentsFiles,
|
|
1007
|
+
runCli,
|
|
1008
|
+
runScaffoldPayments
|
|
1009
|
+
};
|