Decisions 1 through 4 are built on both sides, so the status section says
what landed rather than what is planned, and decision 4a is marked
superseded.
4a is kept rather than deleted. It is the calibration for what a warning
dialog is worth: it depended on an operator reading it at the end of a
refill round, and it could not help at all when the stale value was
already in the form. The dialog and the endpoint behind it are both gone
now, which is a stronger guarantee than any wording could be.
Also records the one gap left open on purpose. An operation recorded
while the relay is unreachable waits for the operator's next action,
because only an operator action triggers a publish.
Two things learned after the ADR was first written.
The dashboard's publish dialog already warns that the publish overwrites
the ATM's tracked counts and that decrements since the last baseline will
be lost, and says v2 reconciliation will replace it. That changes how the
gap should be read: a known risk with a human-factors mitigation, not an
oversight, and the product had already reached the same conclusion these
decisions formalise. Worth stating that a warning is the weakest control
available — it depends on an operator reading a dialog, and cannot help
when the stale value is the one already in the form.
Also records that decisions 5 to 8 were verified live on sintra rather
than only by unit test, and which of the listed failures those decisions
do not close.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This protocol spans two repos and decides how much cash a machine will
pay out, and its only specification was a closed issue and a chat log.
That is how four separate divergence bugs went unnoticed.
Records what the transport actually permits — an addressable event is an
unconditional overwrite ordered by a second-granularity clock, and a
relay acknowledges an event it then discards, so a losing writer is
never told — and why that rules out compare-and-swap and leads to the
ATM owning the count while the operator publishes operations.
Decisions 5 through 8 shipped in #104 and spirekeeper#44; 1 through 4
are the v2 operations wire and are not yet built.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/boltcard-session.md is the /session wire contract (sibling of
boltcard-receive-resolver.md), including the trust boundary: the session
URL is derived from the card's own host, so open enrollment is still not
a security boundary (#91). ADR-003's amendment now records verified entry
via /session and the hidden-by-default balance display.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ADR-003, .env.example, the access module's headers and the provisioning
schema still described the planned npub-QR → UID → serial-reader path.
What shipped (#86) is Bolt Card tap-to-enter over the main-process
pcscd reader with external_id as the identity, soft entry and
verify-at-payment. Nothing ever called availableAccessReaders(): the
camera npub-QR reader, the mock reader and the AccessReader seam were
dead, so they go; services/access now holds authorize, the card parser
and the credential types. The unused 'uid' scan variant goes with them;
'npub' (+PIN) and the 'challenge' seam stay.
The ADR gets an amendment section recording the differences, including
that open enrollment is not a security boundary and that the audit is
still a stub (both tracked as issues).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squashed skeleton (was 11 commits on feat/access-control-skeleton) for a
clean rebase onto dev. Adds a `locked` gate the terminal boots into until a
credential is presented; opt-in and non-breaking (defaults off → boots
straight to idle as before).
- state-machine: `locked` state + ACCESS_GRANTED/ACCESS_DENIED/DEV_UNLOCK
events + accessBypass/devUnlockAllowed guards (packages/state-machine).
- services/access: reader abstraction, npub+PIN authorize() (nostr-tools
nip19; accepts nostr:/nprofile), camera npub-QR reader, mock reader.
- LockedView.vue + ColorModeToggle: branded viewfinder, PIN pad, denied
reason, dev-unlock; camera off-by-default + idle return.
- store/main/electron.d.ts: seed gate config, grant/deny/devUnlock wiring,
access.json provisioning (no rebuild), get-config surface.
- deploy: access.example.json + provision-access.sh; ADR-003.
Credential union is npub today; UID (NFC tap) is the next step.
The access/recovery plane (SSH/NetBird) stands and may carry recovery
procedures, but it is explicitly NOT the only or first-line recovery. Add
a layered, cheapest-first recovery model: (1) app auto-recovery of its own
relay/Lightning connectivity, (2) an on-screen Retry for an operator at the
kiosk, (3) SSH/NetBird as the last-resort remote plane for genuine app/OS
failure. A public kiosk must not need remote shell access to recover from a
transient/boot-before-network outage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Separate payment (Nostr↔LNbits, SaaS-operator-owned), fleet control
(Nostr #42, machine-operator-owned), and access/recovery (SSH) planes
by trust owner. Recovery access is provisioned at install and
app-independent. Adopt NetBird for the access plane (scale + fully FOSS
self-hostable control plane; rejects Tailscale's closed control plane).
Reject a dashboard 'revoke SaaS-operator access' toggle as a false
promise — the SaaS operator controls LNbits and the default control
plane, so exclusion is by ownership (operator self-hosts), not by
toggle.
Second + final batch of the doc refresh. README + CLAUDE went out in
8c9ae29; deploy/nixos/README + obsolete-flow flags in 924844f. This
commit covers everything left.
docs/machine-installation.md
Was describing a manual AppImage scp deploy + a `lamassu-kiosk`
systemd unit that hasn't been the deployment path for months.
Replaced with a high-level "what the pipeline does and why"
overview that points at deploy/nixos/README.md for the full
command-by-command walkthrough. Includes the BATM3 chassis-mod
note (custom Dell OptiPlex retrofit, not a stock Dell).
docs/architecture-comparison.md
Rewrote the comparison to be lamassu-server (≤ v8.1.5) vs bitSpire
(LNbits-backed) instead of the original lamassu-server vs LP-backed
lamassu-next framing. Updated the cash-out + cash-in flow diagrams
to show the actual nostr-transport path (LNbits-bundled nostrrelay
extension at ws://<host>:5001/nostrrelay/test, no separate strfry
container). Replaced the migration-path section with a softer
"when to choose what" framing that includes Lamassu's current
commercial offering as a legitimate third option. Added a header
pointer to the Acknowledgements section.
docs/business-model.md
Light touch-ups: Lightning.Pub → LNbits where it appeared, swapped
the [[ndebit-cash-in-flow]] link for [[architecture-comparison]],
noted the kind-30078 service beacon for availability broadcasts.
docs/device-configuration.md
Dropped "Lamassu" branding from the machine-model headings
(Sintra / tejo / douro / batm3 are referenced by hardware identity
here, not by Lamassu's product line). Added the Sintra-specific
ttyS4-vs-placeholder-ttyS1..3 gotcha we hard-learned during the
first flash. Corrected the BATM3 entry: stock GeneralBytes chassis
with a Dell OptiPlex 9030 AIO motherboard physically grafted in,
NOT a Dell out of the box. Updated the example /dev/ttyJ* symlink
output to match what a healthy Sintra actually shows.
docs/adr/001-hal-architecture.md
ADRs are historical artifacts — kept the original decision text
intact. Added a postscript noting:
- The package rename @lamassu/hal → @bitSpire/hal
- The v8.1.5 boundary on any lamassu-machine source-tree
references (Lamassu's 2024-01-26 license transition)
- That the "Remaining Work" list is complete and the first
successful Sintra hardware integration ran on 2026-05-13
.claude/skills/lightning-check.md
Rewrote end-to-end. Was Lightning.Pub-flavoured with CLINK kinds
21001/21002 as the primary flows; now validates the LNbits nostr-
transport surface (kind-21000 envelope, NIP-44 v2 encryption,
subscribe_payments filter discipline, lnurlw link composition).
Preserved a --clink mode for the still-live kind-21003 operator-
management surface. Includes a "what to check" rubric for cash-out
vs cash-in flows that mirrors the actual code in
apps/machine/src/services/lightning.ts.
.claude/skills/hal-check.md
Two pivots: (1) acknowledge ADR-001's TypeScript-not-Rust choice
and reframe all the safety checklists in TS-flavour (type safety,
discriminated unions, single-writer serial, bounded emitters)
instead of Rust-flavour (unsafe, borrow checker). (2) Add explicit
v8.1.5 provenance boundary plus a "forbidden operations" section
that prohibits diffing or porting from v8.1.6+ lamassu-machine
source. Updated the port-validation source-reference table to
list TS file paths under packages/hal/ instead of Rust paths.
.claude/skills/docs.md
@lamassu/* → @bitSpire/*. Replaced the Lightning.Pub mermaid
diagram with a current cash-out flow showing the nostr-transport
RPC + subscribe_payments push path. Left the createOffer noffer
example in the API-docs template section since it's illustrative
("here's what a good TSDoc block looks like") rather than current
reference documentation.
.claude/skills/test.md
One-line: @lamassu/nostr-client → @bitSpire/nostr-client in the
pnpm-filter example.
deploy/nixos/README.md
Single touch-up: clarified the douro/batm3 hardware-module comments
to reflect that BATM3 is a custom-installed Dell board in a
GeneralBytes BATM3 chassis (not a Dell OEM).
Files NOT touched in this sweep (intentionally):
- packages/hal/src/**/*.ts attribution comments — those reference
"lamassu-machine" in their port-source headers. Those are
factually accurate (the drivers ARE ported from there, up to
v8.1.5) and constitute necessary license/attribution metadata.
Editing them would erase the provenance trail.
- .claude/skills/{nostr-check,security}.md — already protocol-
neutral, no LP/lamassu references to clean up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root
The dev.sh script now supports:
- ./dev.sh up --fund # Start regtest and auto-fund ATM
- ./dev.sh fund # Fund existing ATM app
- ./dev.sh status # Show environment status
- ./dev.sh reset # Clean restart
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>