Skip to content

OpenReceive v0.3.1

Choose a tag to compare

@OpenReceive OpenReceive released this 28 Aug 18:14
· 77 commits to master since this release

Three changes where a component or an engine was guessing about the host, and
answering wrongly in the safe-looking direction: a swap panel telling payers to
bookmark a page that would not bring them back, a console warning that fired at
correct integrations, and a Rails app serving checkouts through the generated
allow-all placeholder without ever saying so. Around them, the demo is a new
shop on four stacks, and CI is running again.

The swap refund-return warning stops guessing

SwapDisplayModel.refundReturnLabel picks one of two warnings, and the wrong
one strands money: a payer who is told to come back to a page that has no route
to it cannot reach a swap deposit's refund. The component was inferring the
answer from syncUrl / routeReference, which only knows about URLs the
component itself writes.

resumable is now an explicit prop on <Checkout> and PaymentWizard — React,
Vue, Svelte, Angular, @openreceive/browser and the custom elements. Unset, it
is still inferred from syncUrl / routeReference; on the React wizard it
defaults to false, so the safe copy is what a host gets by saying nothing. Set
it when the host owns a per-order route the component cannot see, which is most
snapshot-mode apps.

Packaged asset URLs resolve when they are read

@openreceive/provider-data's icon and tutorial tables were built eagerly with
Object.fromEntries, so assetUrl — and its file:// warning — ran at import
time, before any host resolver could be consulted. A host doing exactly the
right thing, serving the packaged dist/assets trees itself and passing an
AssetUrlResolver everywhere, was told on the console that its icons "cannot
load" about icons that loaded fine. That inverts the warning: nobody reading
their own console could tell a correct integration from a broken one.

The new lazyAssetUrlTable export builds the same shape — own, enumerable,
frozen, undefined for a missing key — with getters, so a packaged URL resolves
when something actually reads it. The warning becomes evidence again.

Rails says out loud what the generated initializer left in place

config.authorize from openreceive:install allows every request, treating
possession of the reference as the authorization. That is a fine five-minute
demo and a bad production posture, and nothing distinguished the two.

  • OpenReceive::ALLOW_ALL_AUTHORIZE is a named constant, for the same reason
    LOGGING_ON_PAID is: the engine can detect it at boot by identity and warn
    that anyone holding an order id can mint invoices, poll status and request
    refunds for it.
  • bin/rails openreceive:doctor is step 0 of the agent directions as one
    command.
    It reports credential PRESENCE only — every line is set or
    unset, no secret is printed or partially shown — plus whether configure
    ran, which hooks are missing or still placeholders, where the engine is
    mounted, and a best-effort wallet preflight that reports rather than raises.
    "Look for NWC_URI in this app's server environment" has a different answer
    on every host shape, and in a containerised app grepping the repo finds the
    name and proves nothing about the value. This asks the process.

CI is on again, and it had been off for three releases

Repository-level CI had been disabled since 2026-08-23, so 0.2.4, 0.3.0 and the
PR #1 merge all landed ungated. Switching it back on surfaced three failures,
none of them new: the Rails example's Gemfile.lock carried arm64-darwin
alone, so bundler exited 16 on x86_64-linux before a test ran; the ruby-engine
suite leaned on an ActiveRecord::Base.stub that minitest 6 moved out to a
separate gem; and one core test raced an 8s budget waiting out a real 1800ms
timer, which installFastTimers — exported since it was written and never
called — now clamps.

Buy a Button replaces Hello Fruit, on four stacks instead of one

The reference shop is examples/buttons/, and it exists in four hosts that sell
the same catalog: node-express (3000), static-html-small-api (3001),
nextjs-fullstack (3002) and rails (3003). Hello Fruit is deleted — 80 files
and its whole shared tree.

  • The stacks share a shop, not a checkout. shared/server-node is one
    persistence layer behind all three Node hosts, and the interesting seam is
    renderCheckout: the same order reaches a React mount, a vanilla
    @openreceive/elements mount, and the Next.js app router, and the shop code
    around it does not change. client/ and client-vanilla/ never import each
    other, which is why the static-html workspace carries no @mantine/* or
    mobx*.
  • Settlement arrives differently per host, on purpose. Rails pushes over
    ActionCable; the Node stacks poll. The shared stores expose push seams they
    know nothing about the implementation of, which is the point.
  • The Playwright suite runs against node-express. All eight specs:
    four framework tabs through a full Lightning checkout, both swap paths, the
    remint regression, and persistence.spec.ts in place of resume.spec.ts.
  • Two stale references only surfaced when their target vanished. The Rails
    demo's Dockerfile copied a Hello Fruit shared tree it never needed, and the
    live NWC smoke read product.invoice_expiry_seconds from a product.json
    that never carried that key, so expiry went out undefined on every live run.
    It now prices the cheapest button from shop-catalog.json and passes the
    engine's own 600s default.
  • check-demo-containers had a rule that was wrong, not merely stale. It
    forbade compose volumes outright. The intent — the engine must never carry a
    datastore of its own — is enforced by forbidRuntimePersistence; the blanket
    ban also forbade the HOST keeping its own database, without which a demo
    stops demonstrating "survives a restart" on the first up --build. Named
    *-data volumes are allowed; bind mounts are still rejected.

The release tool bumps the examples again

Deleting Hello Fruit took examples/hello-fruit/server out of
npm-release.mjs's workspace roots and put nothing back. The four buttons
examples are root workspaces, so release:prepare left their @openreceive/*
pins on the old version and the lockfile refresh went to the registry for
@openreceive/testkit — a package that is private by construction and has
never been published. Caught cutting this release.

Agent directions say how host tables sit next to the library

Both payloads told agents OpenReceive never owns orders, then pointed them at
cloning the repository. An agent working in an existing app does not have that
repo, and copying a demo's ShopOrder / signed-cookie visitor over the tables
already there is the failure the buttons demo exists to prevent. The directions
now spell the combination recipe: this app's order id is the reference,
products price the order, users authorize it, the order is unpaid or paid, and
the library's payment rows are not joined from host models. The inlined
quickstarts' onPaid / on_paid are the guarded UPDATE … WHERE state = awaiting_payment. Node's extra: pass this app's db handle, use the query
the settlement transaction hands you, and do not invent a PaymentRepository.

The docs the agent directions point at are fetchable now

The reading list at the end of both agent-directions payloads named page URLs —
https://openreceive.org/guides/authorization. openreceive.org renders guides in
the browser, so fetching one returned an application shell with an empty
<div id="root"> and none of the guide in it. Every link in that list was a
200 with no words behind it, and an agent cannot tell that from a blocked
network: the failure the site contract exists to prevent, arriving as a success.

Every page the site renders from a source here is now also published as raw
markdown at the same URL with .md appended, and the directions link that:

https://openreceive.org/guides/authorization.md
https://openreceive.org/guides.md
https://openreceive.org/api_docs.md
  • docs/site-contract.json is contract_version: 2. Every publish[] entry
    rendered from a source here gains markdown_path beside path. The bump is
    deliberate rather than an additive field: the payloads shipped alongside it
    link markdown_path, so a site that ignored it would 404 the whole reading
    list. docs/internal/site-build.md has the obligations.
  • The inlined quickstart's sibling links point at .md too. They are
    followed by whatever the payload was pasted into, which still has no browser.
  • npm run check:docs checks the twins. A payload may link
    /guides/<slug>.md for a public slug, and /guides.md or /api_docs.md; a
    .md on any other site-owned page fails the gate, because nothing generates
    one.

All four Buy a Button stacks — node-express, static-html-small-api,
nextjs-fullstack and rails — were rebuilt against the 0.3.1 packages, and
the Playwright suite runs against node-express. openreceive (the core gem)
is byte-identical to 0.3.0 and ships to keep the one-version-for-everything
rule; openreceive-rails carries the doctor task and the allow-all warning.

The live wallet smoke (npm run test:live) was NOT run for this release: it
needs a funded NWC connection. Note that the smoke script itself changed here —
it had been reading product.invoice_expiry_seconds from a product.json that
never carried that key, so expiry went out undefined on every live run, and it
now prices the cheapest button from shop-catalog.json. That correction is
therefore unexercised against a real wallet.

This is a PATCH bump: nothing left the public API. resumable and
lazyAssetUrlTable are additive, and both are optional.