@openreceive/http 0.3.2 → 0.3.3

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.
@@ -7,7 +7,7 @@ import {
7
7
  isServiceErrorShape,
8
8
  mapHostRouteError,
9
9
  startNotificationWorker
10
- } from "./chunk-EZAV3TC3.js";
10
+ } from "./chunk-YASZ4HDW.js";
11
11
  export {
12
12
  HostError,
13
13
  HttpError,
@@ -464,6 +464,14 @@ var RECONCILE_GATE_CAS_RETRIES = 6;
464
464
  var META_CLOCK_SKEW_SECONDS = 60;
465
465
  var SCHEMA_VERSION_META_KEY = "schema_version";
466
466
  var OPENRECEIVE_RECONCILE_BATCH_SIZE = 200;
467
+ function isMissingTableError(error, dialect) {
468
+ const message = error instanceof Error ? error.message : String(error);
469
+ if (dialect === "postgres") {
470
+ const code = error?.code;
471
+ return code === "42P01" || /relation .+ does not exist/i.test(message);
472
+ }
473
+ return /no such table/i.test(message);
474
+ }
467
475
  function parseClaimedAt(value) {
468
476
  try {
469
477
  const parsed = JSON.parse(String(value));
@@ -511,8 +519,11 @@ function createSqlPayments(db, options = {}) {
511
519
  );
512
520
  const value = rows[0]?.value;
513
521
  stored = value === void 0 ? void 0 : Number(asString(value, "value"));
514
- } catch {
515
- return;
522
+ } catch (error) {
523
+ if (!isMissingTableError(error, adapter.dialect)) return;
524
+ throw new TypeError(
525
+ `The ${metaTable} table does not exist \u2014 the OpenReceive tables have not been migrated in this database. Run \`npx openreceive scaffold payments --orm <your orm>\` and apply the emitted migration through your normal workflow (or execute paymentsSchemaSql(dialect) directly for bare drivers). https://openreceive.org/guides/storage.md`
526
+ );
516
527
  }
517
528
  if (stored === void 0 || !Number.isInteger(stored)) return;
518
529
  if (stored > OPENRECEIVE_PAYMENTS_SCHEMA_VERSION) {
@@ -785,11 +796,13 @@ function warnFailure(event, prefix, error) {
785
796
  }
786
797
  function createHost(options) {
787
798
  if (options?.amountFor === void 0) {
788
- throw new TypeError("OpenReceive host requires amountFor.");
799
+ throw new TypeError(
800
+ 'OpenReceive host requires amountFor \u2014 the host owns prices. Pass amountFor(reference) returning the amount to charge (for example { currency: "USD", value: "9.99" }), or null for an unknown reference. https://openreceive.org/guides/api-reference.md#createhost'
801
+ );
789
802
  }
790
803
  if (options.onPaid === void 0) {
791
804
  throw new TypeError(
792
- "OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw settlement event in custom repository mode)."
805
+ "OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw settlement event in custom repository mode). Pass onPaid to fulfill the order \u2014 it runs exactly once per reference. https://openreceive.org/guides/api-reference.md#onpaid"
793
806
  );
794
807
  }
795
808
  let payments;
@@ -806,20 +819,28 @@ function createHost(options) {
806
819
  };
807
820
  } else {
808
821
  if (options.payments?.listForReference === void 0) {
809
- throw new TypeError("OpenReceive host requires db or payments.listForReference.");
822
+ throw new TypeError(
823
+ "OpenReceive host requires db or payments.listForReference. Pass db (your database handle; the library owns the openreceive tables in it), or a complete custom PaymentRepository. https://openreceive.org/guides/storage.md"
824
+ );
810
825
  }
811
826
  if (options.payments.commitAttempt === void 0) {
812
- throw new TypeError("OpenReceive host requires payments.commitAttempt.");
827
+ throw new TypeError(
828
+ "OpenReceive host requires payments.commitAttempt \u2014 the attempt write committed before payer instructions are exposed. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
829
+ );
813
830
  }
814
831
  if (options.payments.listReconcilableAttempts === void 0) {
815
- throw new TypeError("OpenReceive host requires payments.listReconcilableAttempts.");
832
+ throw new TypeError(
833
+ "OpenReceive host requires payments.listReconcilableAttempts \u2014 the pending-attempt batch reconciliation scans. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
834
+ );
816
835
  }
817
836
  if (options.payments.recordReconciliation === void 0) {
818
- throw new TypeError("OpenReceive host requires payments.recordReconciliation.");
837
+ throw new TypeError(
838
+ "OpenReceive host requires payments.recordReconciliation \u2014 how a scan outcome (settled/expired/attention) is written back. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
839
+ );
819
840
  }
820
841
  if (typeof options.payments.recordSettlement !== "function") {
821
842
  throw new TypeError(
822
- "OpenReceive host requires payments.recordSettlement (the write-once settlement claim)."
843
+ "OpenReceive host requires payments.recordSettlement (the write-once settlement claim). Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
823
844
  );
824
845
  }
825
846
  payments = options.payments;
@@ -1419,7 +1440,7 @@ function createIpRateLimit(config = {}) {
1419
1440
  const count = config.countAttemptsFromIp;
1420
1441
  if (count === void 0) {
1421
1442
  throw new TypeError(
1422
- "rateLimiting requires persistent counting. Implement countAttemptsFromIp on the payment repository (the built-in SQL repository already does), pass countAttemptsFromIp in the rateLimiting config, or disable rateLimiting and use a custom rateLimitHook backed by your own store. There is deliberately no in-memory fallback: per-process counts reset on restart and multiply per instance behind a load balancer."
1443
+ "rateLimiting requires persistent counting. Implement countAttemptsFromIp on the payment repository (the built-in SQL repository already does), pass countAttemptsFromIp in the rateLimiting config, or disable rateLimiting and use a custom rateLimitHook backed by your own store. There is deliberately no in-memory fallback: per-process counts reset on restart and multiply per instance behind a load balancer. https://openreceive.org/guides/rate-limiting.md"
1423
1444
  );
1424
1445
  }
1425
1446
  const message = config.message ?? DEFAULT_MESSAGE;
@@ -1434,7 +1455,7 @@ function createIpRateLimit(config = {}) {
1434
1455
  if (!warnedUnattributable) {
1435
1456
  warnedUnattributable = true;
1436
1457
  console.warn(
1437
- "[openreceive] rateLimiting allowed a request with no attributable client IP (fail-open). If every request logs no IP, the adapter is not supplying one and rate limiting is inactive \u2014 see the rate limiting guide."
1458
+ "[openreceive] rateLimiting allowed a request with no attributable client IP (fail-open). If every request logs no IP, the adapter is not supplying one and rate limiting is inactive \u2014 see https://openreceive.org/guides/rate-limiting.md"
1438
1459
  );
1439
1460
  }
1440
1461
  return true;
@@ -1521,19 +1542,31 @@ function tooManyAttempts(message) {
1521
1542
 
1522
1543
  // src/handler.ts
1523
1544
  function createHttpHandler(options) {
1524
- if (options?.service === void 0) throw new TypeError("HTTP handler requires service.");
1545
+ if (options?.service === void 0) {
1546
+ throw new TypeError(
1547
+ "HTTP handler requires service: pass the wallet service built by createOpenReceive(). https://openreceive.org/guides/api-reference.md#createopenreceive"
1548
+ );
1549
+ }
1525
1550
  if (options.authorize === void 0) {
1526
- throw new TypeError("HTTP handler requires authorize; authentication belongs to the host.");
1551
+ throw new TypeError(
1552
+ "HTTP handler requires authorize; authentication belongs to the host. Pass an authorize hook that checks the payer's session may act on context.resource.reference. https://openreceive.org/guides/authorization.md"
1553
+ );
1554
+ }
1555
+ if (options.host === void 0) {
1556
+ throw new TypeError(
1557
+ "HTTP handler requires host: build it with createHost({ amountFor, onPaid, db }) and pass it beside service and authorize. https://openreceive.org/guides/api-reference.md#createhost"
1558
+ );
1527
1559
  }
1528
- if (options.host === void 0) throw new TypeError("HTTP handler requires host.");
1529
1560
  if (options.rateLimiting !== void 0 && options.rateLimiting !== false && options.rateLimitHook !== void 0) {
1530
- throw new TypeError("Pass either rateLimiting or a custom rateLimitHook, not both.");
1561
+ throw new TypeError(
1562
+ "Pass either rateLimiting or a custom rateLimitHook, not both: rateLimiting is the built-in per-IP limiter, rateLimitHook replaces it with your own policy. https://openreceive.org/guides/rate-limiting.md"
1563
+ );
1531
1564
  }
1532
1565
  const rateLimit = options.rateLimitHook ?? resolveRateLimiting(options);
1533
1566
  const reconcile = options.opportunisticReconcile === false ? void 0 : typeof options.opportunisticReconcile === "object" ? options.opportunisticReconcile : {};
1534
1567
  if (reconcile !== void 0 && typeof options.host.payments.claimReconcileGate !== "function") {
1535
1568
  throw new TypeError(
1536
- "Opportunistic reconcile (on by default) requires payments.claimReconcileGate \u2014 a durable compare-and-set gate shared by every worker (the built-in SQL repository implements it over openreceive_meta). Implement it on the custom repository, or pass opportunisticReconcile: false and run your own settlement worker."
1569
+ "Opportunistic reconcile (on by default) requires payments.claimReconcileGate \u2014 a durable compare-and-set gate shared by every worker (the built-in SQL repository implements it over openreceive_meta). Implement it on the custom repository, or pass opportunisticReconcile: false and run your own settlement worker. https://openreceive.org/guides/storage.md"
1537
1570
  );
1538
1571
  }
1539
1572
  const rawExtractClientIp = (typeof options.rateLimiting === "object" ? options.rateLimiting.ip : void 0) ?? resolveClientIp;
@@ -1830,7 +1863,11 @@ async function enforceRateLimit(runtime, action, request, resource, native) {
1830
1863
  }
1831
1864
  async function enforceAuthorize(runtime, action, request, resource, native) {
1832
1865
  if (!await runtime.authorize({ action, request, resource, native })) {
1833
- throw new HttpError(403, "FORBIDDEN", "Not authorized for this action.");
1866
+ throw new HttpError(
1867
+ 403,
1868
+ "FORBIDDEN",
1869
+ "Not authorized for this action. The application's authorize hook denied it; if this is unexpected, check that the payer's session reaches the checkout routes. https://openreceive.org/guides/authorization.md"
1870
+ );
1834
1871
  }
1835
1872
  }
1836
1873
  async function persistCheckoutAttempt(runtime, input) {
@@ -2205,7 +2242,7 @@ function isStackOptions(options) {
2205
2242
  return true;
2206
2243
  }
2207
2244
  throw new TypeError(
2208
- "OpenReceive composed options require host: pass { service, host, authorize }, or use the all-in-one form with wallet/storage/amountFor."
2245
+ "OpenReceive composed options require host: pass { service, host, authorize } (host built by createHost), or use the all-in-one form with wallet/storage/amountFor. https://openreceive.org/guides/api-reference.md#framework-adapters"
2209
2246
  );
2210
2247
  }
2211
2248
 
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  startReconciler,
35
35
  typeOrmDb,
36
36
  webRequest
37
- } from "./chunk-EZAV3TC3.js";
37
+ } from "./chunk-YASZ4HDW.js";
38
38
  export {
39
39
  HostError,
40
40
  HttpError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openreceive/http",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Framework-neutral (Web Request/Response) HTTP handler for OpenReceive checkout that builds on @openreceive/node and requires its Node runtime: request routing, host integration, and payment-attempt persistence.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -17,8 +17,8 @@
17
17
  "main": "./dist/index.js",
18
18
  "types": "./dist/index.d.ts",
19
19
  "dependencies": {
20
- "@openreceive/core": "0.3.2",
21
- "@openreceive/node": "0.3.2"
20
+ "@openreceive/core": "0.3.3",
21
+ "@openreceive/node": "0.3.3"
22
22
  },
23
23
  "exports": {
24
24
  ".": {
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
+ "skills",
35
36
  "README.md",
36
37
  "LICENSE"
37
38
  ],
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: debug-openreceive-payment
3
+ description: >
4
+ Diagnose a failing OpenReceive integration. Use when an OpenReceive-powered
5
+ checkout misbehaves: the server refuses to boot, checkout routes return 403,
6
+ 404, 409, or 5xx, a paid invoice never settles, a swap refund seems
7
+ unreachable, or the checkout UI renders nothing.
8
+ license: MIT
9
+ ---
10
+
11
+ # Debug an OpenReceive payment
12
+
13
+ Work top-down: configuration, then the request, then settlement. Every guide
14
+ URL below is raw markdown — fetch it when the step needs it.
15
+
16
+ ## 1. Run the doctor first
17
+
18
+ ```sh
19
+ npx openreceive doctor # Node version, NWC_URI, swap config, wallet probe
20
+ npx openreceive doctor --db <db> # + are openreceive_payments/openreceive_meta migrated?
21
+ npx openreceive doctor --url http://localhost:3000 # + are the routes actually mounted?
22
+ ```
23
+
24
+ Each failing line states its own fix. `npx openreceive debug-report` prints the
25
+ same diagnostics redacted, always exit 0 — safe to share.
26
+
27
+ ## 2. Boot failures
28
+
29
+ | Symptom | Cause and fix |
30
+ | --- | --- |
31
+ | `MISSING_NWC` / "needs a receive-only NWC code" | `NWC_URI` is not in the server process env. A `.env` file alone is not enough — something must load it (`dotenv/config`, Next auto-load). Get a code: https://openreceive.org/get_a_nwc_code_to_receive_payments |
32
+ | `INVALID_NWC` / "not a valid NWC code" | The value is malformed (must be `nostr+walletconnect://` with 64-hex pubkey and secret, ≥1 `wss` relay). Re-copy it from the wallet. |
33
+ | "NOT receive-only" / spend methods advertised | The wallet minted a spend-capable code; OpenReceive fails closed because a leak would drain the wallet. Mint a receive-only code. Overriding (`allowSpendCapableWallet` / `OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC`) is a last resort. |
34
+ | Wallet preflight failed (methods/encryption) | The wallet must advertise `make_invoice` + `list_transactions` and NIP-04 or NIP-44 v2. Use a compatible wallet. |
35
+ | "The openreceive_meta table does not exist" / raw `no such table: openreceive_payments` | The migration was never applied. Node: `npx openreceive scaffold payments --orm <yours>`, then run the emitted migration through the app's normal workflow. Rails: `bin/rails generate openreceive:install`, then `bin/rails db:migrate`. https://openreceive.org/guides/storage.md |
36
+ | "requires amountFor / onPaid / authorize / host" | The factory is missing a required hook — see the host contract in https://openreceive.org/guides/api-reference.md |
37
+
38
+ ## 3. Request-time errors from the routes
39
+
40
+ | Status | Meaning | Where to look |
41
+ | --- | --- | --- |
42
+ | 403 FORBIDDEN | Your own `authorize` hook denied it, or the request looked cross-site. Check the session/cookie actually reaches the checkout routes. https://openreceive.org/guides/authorization.md |
43
+ | 404 NOT_FOUND | `amountFor` returned `null` (unknown reference), or the `payment_hash` does not belong to that reference. |
44
+ | 409 CONFLICT | **Normal state, not a bug**: the reference already settled, or an unpaid checkout for that method is already live. Show it as order state; never retry-loop. |
45
+ | 503 retryable | The host hook failed while persisting the attempt (instructions withheld), or the wallet is unavailable. Read the server log for the underlying error. |
46
+ | Framework 404 / HTML error page | The router is not mounted, or mounted at a different prefix than the UI's `prefix` prop. `doctor --url` distinguishes these. |
47
+
48
+ ## 4. Paid but never settles
49
+
50
+ - Settlement is opportunistic: any OpenReceive request runs one reconcile pass
51
+ through a durable gate (min 2s between wallet scans, stretched by invoice
52
+ age). A quiet server settles on the next request — or run the optional
53
+ notification worker. No timer is missing; that is the design.
54
+ - An unpaid attempt closes only after a successful wallet scan at/after expiry
55
+ plus a 900s grace constant — a local clock alone never closes one. `expired`
56
+ arriving "late" is correct.
57
+ - `onPaid` runs once per reference, first settled attempt only, inside the
58
+ settlement transaction. If your fulfillment did not run, check whether the
59
+ guarded `UPDATE … WHERE` matched zero rows (already transitioned).
60
+ https://openreceive.org/guides/storage.md
61
+
62
+ ## 5. Swaps and refunds
63
+
64
+ - A deposit that arrives short or late becomes `refund_required`; the payer
65
+ claims it on a second visit. That needs a per-order URL you serve
66
+ (`/checkout/:reference`, `syncUrl` on the drop-ins). Keep the
67
+ `payment_hash`: `POST /swaps/status` reopens the attempt with no expiry
68
+ window, while re-picking the coin mints a new deposit after ~30 minutes.
69
+ - Refunds exist only for swap deposits from `refund_required`. There is **no
70
+ Lightning refund** — the wallet cannot spend. Do not chase one.
71
+ https://openreceive.org/guides/swap-refunds.md
72
+
73
+ ## 6. Checkout UI shows nothing
74
+
75
+ - The components require `prefix` — the exact base path the routes are mounted
76
+ at (`"/openreceive"` unless you changed it).
77
+ - Import the stylesheet (`@openreceive/react/styles.css` or the elements
78
+ sheet).
79
+ - "invoice must not be an NWC connection string" means a server secret leaked
80
+ into a browser payload — stop and fix the server response; never render it.
81
+ https://openreceive.org/guides/frontend-checkout.md
82
+
83
+ ## Still stuck
84
+
85
+ The full route/option/error reference:
86
+ https://openreceive.org/guides/api-reference.md · machine-readable contract:
87
+ https://openreceive.org/openapi.yaml · library bug reports:
88
+ https://openreceive.org/contact
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: integrate-openreceive
3
+ description: >
4
+ Integrate OpenReceive inbound Bitcoin Lightning payments into an application.
5
+ Use when adding Bitcoin, Lightning, or crypto checkout to a Node.js, Express,
6
+ Fastify, Next.js, Rails, React, Vue, Svelte, Angular, or plain-HTML
7
+ application with OpenReceive (the @openreceive/* npm packages or the
8
+ openreceive-rails gem).
9
+ license: MIT
10
+ ---
11
+
12
+ # Integrate OpenReceive
13
+
14
+ OpenReceive is a payment library that runs inside the application you are
15
+ editing. It mounts HTTP routes there, issues Lightning invoices against a
16
+ wallet the merchant already controls, and calls back into your code when one
17
+ settles. There is no OpenReceive account and no API key; funds land directly in
18
+ the merchant's wallet. The one required credential is a **receive-only NWC
19
+ code** (`NWC_URI`).
20
+
21
+ ## Pick the stack, then follow its directions
22
+
23
+ 1. Identify the server stack of the application you are in.
24
+ 2. Open the matching reference — it is complete (quickstart inlined) and needs
25
+ no network access:
26
+ - Node (Express / Fastify / Next.js): [references/node.md](references/node.md)
27
+ - Rails: [references/rails.md](references/rails.md)
28
+ 3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
29
+ environment before writing code. Never print the value; never invent a
30
+ placeholder.
31
+
32
+ Install (Node): `npm install @openreceive/express @openreceive/react` — swap
33
+ the adapter (`fastify`, `next`) and UI package (`vue`, `svelte`, `angular`,
34
+ `elements`) for the stack. Install (Rails): `bundle add openreceive-rails`.
35
+
36
+ ## The three server objects
37
+
38
+ | Object | Built with | Talks to |
39
+ | --- | --- | --- |
40
+ | Wallet client | `createOpenReceive()` | the merchant's wallet — mints invoices, reads settlement, holds the NWC code |
41
+ | Host | `createHost()` | your database — your hooks plus the `openreceive_payments` table |
42
+ | HTTP routes | `openReceiveExpress()` / `openReceiveFastify()` / `openReceiveNext()` / the Rails engine | the browser — mounted at `/openreceive` by default |
43
+
44
+ The quickstart's one-factory form (`openReceiveExpress({ wallet, storage,
45
+ amountFor, authorize })`) builds all three; compose them separately only for a
46
+ shared wallet client or a custom repository. The checkout UI
47
+ (`<Checkout reference={...} prefix="/openreceive" />`) is the optional fourth
48
+ piece.
49
+
50
+ ## The host contract: authorize, amountFor, onPaid
51
+
52
+ Your application keeps orders, users, prices, and fulfillment. Three hooks are
53
+ the entire bridge — wire them to the models this app already has, never to
54
+ copied demo models:
55
+
56
+ - `amountFor(reference)` — the authoritative price, read from your own data.
57
+ Return `{ currency, value, description }` with `value` a **decimal string**
58
+ (never a float, never payer input), or `null` when there is nothing to pay
59
+ for. The `reference` is your order id: one per thing you fulfill, created
60
+ before checkout, kept across retries, never reused.
61
+ - `authorize({ action, request, resource })` — your own access check, run on
62
+ every request. `resource.reference` is a claim the payer made, not proof;
63
+ read a real session.
64
+ - `onPaid({ reference, paidAt, query })` — fulfillment, run once per reference
65
+ inside the settlement transaction, only for the first settled attempt. Use
66
+ the provided `query`, not your ORM's other connection, and guard the
67
+ transition (`UPDATE … WHERE state = 'awaiting_payment'`).
68
+
69
+ ## 409 is a state, not a failure
70
+
71
+ The library serializes attempts per reference. A create that returns **409
72
+ CONFLICT** is normal checkout flow: the reference already settled, or an unpaid
73
+ checkout for that payment method is already in progress. Surface it as order
74
+ state; do not retry-loop it, and do not build an idempotency store around it —
75
+ that serialization is the library's job. (A hook failure while persisting an
76
+ attempt is a **503 retryable**, deliberately distinct.)
77
+
78
+ ## Secrets
79
+
80
+ `NWC_URI` and `LSC_URI_*` are server-only. Never put them in browser code,
81
+ logs, assets, or tests. Boot fails closed if the NWC code advertises spend
82
+ methods such as `pay_invoice` — mint a receive-only code
83
+ (https://openreceive.org/get_a_nwc_code_to_receive_payments) instead of
84
+ overriding.
85
+
86
+ ## Database tables
87
+
88
+ ```sh
89
+ npx openreceive scaffold payments --orm prisma # or drizzle | typeorm | sequelize | knex
90
+ ```
91
+
92
+ emits the `openreceive_payments` + `openreceive_meta` migration for THIS app's
93
+ database (Rails: `bin/rails generate openreceive:install`); run it through the
94
+ app's normal migration workflow. The tables sit beside your models — no
95
+ relations to them, no separate database, no Redis.
96
+
97
+ ## Verify, and test without a real wallet
98
+
99
+ `npx openreceive doctor` checks the configuration and says what to fix.
100
+
101
+ For tests, inject a fake wallet at the stable seams — `client` on
102
+ `createOpenReceive` (any object with `preflight`, `makeInvoice`,
103
+ `listTransactions`) or `config.nwc_client` in Rails — plus
104
+ `StaticPriceProvider` for fiat pricing without a network. Your routes,
105
+ persistence, reconcile, and `onPaid` then run the production code paths.
106
+ Details: https://openreceive.org/guides/host-testing.md
107
+
108
+ ## Deeper documentation
109
+
110
+ Fetch on demand — each URL is raw markdown:
111
+ https://openreceive.org/guides/authorization.md ·
112
+ https://openreceive.org/guides/storage.md ·
113
+ https://openreceive.org/guides/api-reference.md ·
114
+ https://openreceive.org/guides/security.md ·
115
+ https://openreceive.org/openapi.yaml (the normative HTTP contract) ·
116
+ https://openreceive.org/llms.txt (the full index)