docs: finish the LNbits-era doc sweep across docs/ + .claude/skills/

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>
This commit is contained in:
Padreug 2026-05-14 08:25:18 +02:00
commit 53b0d382e8
10 changed files with 692 additions and 792 deletions

View file

@ -25,10 +25,11 @@ docs/
│ ├── lightning-flow.md # Payment flows
│ └── hardware-hal.md # HAL documentation
├── api/
│ ├── nostr-client.md # @lamassu/nostr-client API
│ ├── clink.md # @lamassu/clink API
│ ├── state-machine.md # @lamassu/state-machine API
│ └── hal.md # @lamassu/hal API
│ ├── nostr-client.md # @bitSpire/nostr-client API
│ ├── lnbits.md # @bitSpire/lnbits API (kind-21000 RPC surface)
│ ├── clink.md # @bitSpire/clink API (dormant on dev; 21003 management still wired)
│ ├── state-machine.md # @bitSpire/state-machine API
│ └── hal.md # @bitSpire/hal API
├── guides/
│ ├── development.md # Dev setup guide
│ ├── hardware-testing.md # Testing with real hardware
@ -106,22 +107,25 @@ Keep architecture diagrams current:
```mermaid
sequenceDiagram
participant U as User
participant U as Customer Wallet
participant A as ATM
participant R as Relay
participant L as Lightning.Pub
participant R as Relay (LNbits-bundled nostrrelay)
participant L as LNbits (nostr-transport)
U->>A: Insert $20 bill
A->>A: Validate bill
A->>R: Publish CLINK offer
U->>R: Send payment request
R->>A: Forward request
A->>L: Generate invoice
L->>A: Return invoice
A->>R: Send invoice to user
U->>L: Pay invoice
L->>A: Payment confirmed
A->>U: Display success
Note over A,L: cash-out flow (customer pays ATM, gets cash)
A->>R: kind-21000 RPC "create_invoice"
R->>L: forward to LNbits server pubkey
L->>R: kind-21000 ACK with BOLT11 invoice
R->>A: forward ACK
A->>U: display BOLT11 QR
A->>R: kind-21000 "subscribe_payments" by payment_hash
R->>L: forward
L->>R: kind-21000 ACK with subscription_id
R->>A: forward
U->>L: pay BOLT11 via Lightning Network
L->>R: kind-21000 push (settlement) tagged subscription_id
R->>A: forward
A->>A: dispense cash
```
### Auto-update triggers

View file

@ -1,133 +1,162 @@
# /hal-check - Hardware Abstraction Layer Agent
# /hal-check — Hardware Abstraction Layer Agent
## Purpose
Validate HAL driver implementations against existing lamassu-machine drivers and hardware specifications.
Validate HAL driver implementations in `packages/hal/` against published hardware protocol specs (JCM ID003, Fujitsu F56 DLE/STX, Puloon LCDM, MEI EBDS, etc.) and against the v8.1.5 release line of `lamassu-machine` — which is the **last** lamassu-machine release published under a fully-open license.
> **Provenance boundary.** Drivers in `packages/hal/` derive from `lamassu-machine` at v8.1.5 and earlier (plus hardware-vendor protocol specs). Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 with v8.1.6+ gated behind a paid Operator Support Agreement. **Do not** reference, port, or diff against v8.1.6+ — the only safe upstream tree for porting is `v8.1.5` or earlier. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules.
> **Language note.** ADR-001 selected TypeScript-in-Electron over Rust-in-Tauri for the HAL. Earlier versions of this skill referenced Rust patterns; that's obsolete. All checks below are TypeScript-flavored.
## Invocation
```
/hal-check [command] [driver]
```
Commands:
- `port` - Validate porting from lamassu-machine
- `protocol` - Check protocol implementation
- `safety` - Rust safety review
- `mock` - Validate mock implementation
- `port` — Validate that a driver matches its lamassu-machine v8.1.5 reference (where the driver was ported from one)
- `protocol` — Check protocol implementation against published vendor specs
- `safety` — Type safety, error handling, hardware safety review
- `mock` — Validate mock implementation completeness
Drivers:
- `id003`, `ccnet`, `cashflow`, `bnr`, `genmega`, `hcm2`, `gsr50` (validators)
- `puloon`, `f56`, `genmega`, `hcm2`, `gsr50` (dispensers)
- `nippon`, `zebra`, `genmega` (printers)
## Porting Validation
- Validators: `id003`, `ccnet`, `cashflow_sc` (EBDS), `bnr_advance`, `genmega`, `hcm2`, `gsr50`
- Dispensers: `puloon`, `f56`, `genmega`, `hcm2`, `gsr50`
- Printers: `nippon`, `zebra`, `genmega`
### Source Reference
Each Rust driver should map 1:1 with lamassu-machine JavaScript:
## Porting validation (`port` command)
| Rust File | JavaScript Source |
|-----------|-------------------|
| `validators/id003.rs` | `lib/id003/*.js` |
| `validators/ccnet.rs` | `lib/ccnet/*.js` |
| `dispensers/puloon.rs` | `lib/puloon/*.js` |
| `dispensers/f56.rs` | `lib/f56/*.js` |
### Source reference
Each TS driver in `packages/hal/` maps to (at most) one JS source in lamassu-machine v8.1.5 (the last fully-open release):
| TS Driver | JS Source (v8.1.5 release tree) |
|---|---|
| `validators/id003/*.ts` | `lib/id003/*.js` |
| `validators/ccnet/*.ts` | `lib/ccnet/*.js` |
| `validators/ebds/*.ts` | `lib/mei/cashflow_sc.js` + `lib/mei/*.js` |
| `dispensers/puloon/*.ts` | `lib/puloon/*.js` |
| `dispensers/f56/*.ts` | `lib/f56/*.js` |
If a driver in `packages/hal/` references a JS source NOT under v8.1.5 (or no JS source at all because it was re-derived from vendor specs), the attribution comment at the top of the TS file should say so.
### Porting checklist
### Porting Checklist
```
/hal-check port id003
```
Validates:
- [ ] All protocol commands implemented
- [ ] State machine matches JS FSM
- [ ] CRC/checksum calculation identical
- [ ] Timeout values match
- [ ] Error codes mapped correctly
- [ ] Denomination tables match
### Protocol Commands
- [ ] All protocol commands implemented (RESET, ENABLE, DISABLE, STACK, RETURN, STATUS — see ID003 spec or `lib/id003/id003rs232.js` from v8.1.5)
- [ ] State machine matches the JS FSM (`lib/id003/id003fsm.js`)
- [ ] CRC/checksum calculation byte-identical to the JS version
- [ ] Timeout values match the JS source
- [ ] Error codes mapped correctly to TypeScript enum/union
- [ ] Denomination tables match (currency-specific)
- [ ] Attribution comment at top of TS file points at the specific v8.1.5 file
### Protocol commands
#### ID003 (JCM)
| Command | Code | JS Reference |
|---------|------|--------------|
| RESET | 0x40 | id003.js:reset() |
| Command | Code | JS Reference (v8.1.5) |
|---|---|---|
| RESET | 0x40 | id003rs232.js:reset() |
| ENABLE | 0x13 | id003fsm.js:enable |
| DISABLE | 0x14 | id003fsm.js:disable |
| STACK | 0x15 | id003fsm.js:stack |
| RETURN | 0x16 | id003fsm.js:return |
| STATUS | 0x10 | id003fsm.js:poll |
#### Puloon
| Command | Code | JS Reference |
|---------|------|--------------|
#### Puloon LCDM
| Command | Code | JS Reference (v8.1.5) |
|---|---|---|
| RESET | 0x44 | puloonrs232.js |
| DISPENSE | 0x45 | puloonrs232.js |
| STATUS | 0x46 | puloonrs232.js |
## Safety Review (Rust-specific)
## Safety review (`safety` command)
### Memory Safety
- [ ] No `unsafe` without justification comment
- [ ] Buffer sizes validated before read/write
- [ ] No panic paths in production code
- [ ] Proper error propagation with Result
### Type safety
### Concurrency Safety
- [ ] Serial port access properly synchronized
- [ ] Event channels bounded
- [ ] No deadlock potential
- [ ] Timeout on all blocking operations
- [ ] No `any` types in driver public API
- [ ] Discriminated unions for state machine states (not just string literals)
- [ ] Error types are typed `Result<T, DriverError>` (or equivalent) at the driver boundary — never `throw` raw strings
- [ ] Buffer reads are bounds-checked; offsets validated before slicing
### Hardware Safety
- [ ] Dispenser amounts validated (prevent over-dispense)
- [ ] Bill count cross-checked
- [ ] Error states trigger hardware reset
- [ ] Graceful degradation on hardware failure
### Concurrency safety
## Mock Validation
- [ ] Serial port access is single-writer (TypeScript single-threaded helps, but ensure no two `await write()` calls can interleave with each other's expected response)
- [ ] Event emitters are bounded (max listeners set; back-pressure handled)
- [ ] Timeouts on all blocking I/O (`Promise.race` with `setTimeout`)
- [ ] No unbounded queues — a runaway validator polling loop shouldn't fill RAM
### Hardware safety
- [ ] Dispenser amounts validated against cassette inventory before dispensing
- [ ] Bill count cross-checked between requested and dispensed
- [ ] Error states trigger hardware reset (`disable + reset + enable` cycle for validators)
- [ ] Graceful degradation: partial dispense reports correct dispensed-vs-rejected counts
- [ ] Validator goes to `disable` on app shutdown — bills inserted while the app is dead get returned, not stacked uncounted
## Mock validation (`mock` command)
### Mock requirements
Mocks under `packages/hal/src/**/mock.ts` must simulate:
### Mock Requirements
Mocks must simulate:
1. Normal operation flow
2. Error conditions
3. Timing (realistic delays)
4. State persistence
2. Error conditions (jam, empty cassette, comms failure)
3. Realistic timing (validator escrow takes ~600ms; dispenser cycle takes ~2s)
4. State persistence within a session
### Mock test coverage
### Mock Test Coverage
```
/hal-check mock puloon
```
Validates mock implements:
- [ ] `connect()` - Success and failure paths
- [ ] `dispense()` - Full and partial dispense
- [ ] `reset()` - Error recovery
- [ ] Event emission timing
- [ ] Cassette state tracking
## Protocol Analysis
- [ ] `connect()` — success and failure paths
- [ ] `dispense()` — full, partial, and over-requested paths
- [ ] `reset()` — error recovery
- [ ] Event emission timing roughly matches real hardware
- [ ] Cassette state tracking (count decrements properly)
## Protocol analysis (`protocol` command)
### Packet structure validation
### Packet Structure Validation
```
/hal-check protocol id003
```
Compares Rust packet building with JS:
```rust
// Rust implementation
fn build_packet(&self, data: &[u8]) -> Vec<u8> {
let mut packet = vec![0x02]; // SYNC
packet.push(data.len() as u8 + 4);
packet.extend_from_slice(data);
let crc = self.calculate_crc(&packet);
packet.push((crc & 0xFF) as u8);
packet.push((crc >> 8) as u8);
packet
Compares TS packet building with the v8.1.5 JS reference:
```typescript
// TypeScript implementation
function buildPacket(data: Uint8Array): Uint8Array {
const packet = new Uint8Array(data.length + 4)
packet[0] = 0x02 // SYNC
packet[1] = data.length + 4 // length includes header + CRC
packet.set(data, 2)
const crc = calculateCrc(packet.slice(0, -2))
packet[packet.length - 2] = crc & 0xff
packet[packet.length - 1] = (crc >> 8) & 0xff
return packet
}
```
Against JavaScript:
Against the v8.1.5 JS reference (line numbers may vary by tag):
```javascript
// lamassu-machine/lib/id003/id003rs232.js
// lamassu-machine v8.1.5 — lib/id003/id003rs232.js
function buildPacket(data) {
const buf = Buffer.alloc(data.length + 4)
buf[0] = 0x02 // SYNC
@ -139,62 +168,72 @@ function buildPacket(data) {
}
```
## Output Format
A discrepancy here (different CRC polynomial, different framing, different endianness) is a serious port bug.
## Output format
### Port validation
### Port Validation
```markdown
## HAL Port Validation: id003
### Command Coverage
| Command | JS | Rust | Match |
|---------|-------|------|-------|
### Command coverage
| Command | v8.1.5 JS | TS | Match |
|---|---|---|---|
| RESET | ✅ | ✅ | ✅ |
| ENABLE | ✅ | ✅ | ✅ |
| STATUS | ✅ | ⚠️ | Partial |
### Protocol Differences
- [ ] `id003.rs:78` - CRC uses different polynomial than JS
### Protocol differences
- [ ] `id003-rs232.ts:78` — CRC uses different polynomial than v8.1.5 JS
### Missing Implementations
- [ ] `HOLD` command not implemented (used in id003fsm.js:holdBill)
### Missing implementations
- [ ] HOLD command not implemented (v8.1.5 `id003fsm.js:holdBill`)
### Recommendations
1. Verify CRC calculation against test vectors from JS
2. Add HOLD command for escrow mode
### Attribution
- [x] File header points at `lib/id003/id003rs232.js` (v8.1.5)
```
### Safety Review
### Safety review
```markdown
## HAL Safety Review: puloon
### Memory Safety
- [x] No unsafe blocks
- [x] Buffer bounds checked
- [ ] `dispense()` at line 145 - unwrap() could panic
### Type safety
- [x] No `any` types in public surface
- [x] Discriminated union for DispenserState
### Concurrency Safety
- [x] Serial port mutex protected
- [x] Event channel bounded (16)
### Concurrency safety
- [x] Single-writer over serial port
- [x] Event emitter has bounded listener count
- [ ] `puloon-rs232.ts:142` — no timeout on response wait; could hang forever
### Hardware Safety
- [x] Amount validation in dispense()
- [ ] No cassette empty check before dispense
### Hardware safety
- [x] Amount validated against cassette inventory in dispense()
- [ ] `puloon.ts:178` — partial dispense doesn't update cassette inventory
### Critical Issues
1. `puloon.rs:145` - Replace unwrap() with proper error handling
2. `puloon.rs:178` - Add cassette level check before dispensing
### Critical issues
1. `puloon-rs232.ts:142` — wrap response wait in `Promise.race([response, timeout(5000)])`
2. `puloon.ts:178` — decrement cassette count by actual dispensed, not requested
```
## Example Usage
## Example usage
```
/hal-check port id003
```
```
/hal-check safety packages/hal/src/dispensers/
```
```
/hal-check safety packages/hal/src/dispensers/puloon/
/hal-check mock --all
/hal-check protocol ebds # validates EBDS framing against v8.1.5 JS + MEI vendor spec
```
## Related skills
- `/security` — broader Bitcoin/Lightning/ATM vulnerability scan
- `/test` — runs HAL-specific tests, generates coverage reports
- `/lightning-check` — validates the Lightning backend side (LNbits transport)
## Forbidden operations
- Diff or read `lamassu-machine` source at v8.1.6 or later. Only `v8.1.5` (and the historical commit range leading up to it) is permissible to reference.
- "Backport" any fix or feature from v8.1.6+ JS sources into TypeScript. If a bug fix is needed, implement from the protocol spec or hardware traces.
- Include attribution comments pointing at v8.1.6+ files even if the implementation is your own — readers should be able to trust file-header attributions as accurate.

View file

@ -1,168 +1,208 @@
# /lightning-check - Lightning.Pub Conformity Agent
# /lightning-check — Lightning backend conformity
## Purpose
Validate Lightning.Pub integration and CLINK protocol conformity.
Validate the LNbits nostr-transport integration in bitSpire — wire envelope, encryption, subscription handling, invoice/withdraw correctness. Replaces the original Lightning.Pub-flavored version of this skill; the LP backend was removed from `dev` in commit `a51d7af`.
> The `--clink` mode is preserved for validating CLINK kind-21003 management commands, which are the one piece of CLINK still wired on `dev` (operator dispense commands). CLINK kinds 21001/21002 are dormant on `dev`.
## Invocation
```
/lightning-check [target] [--clink] [--wallet]
/lightning-check [target] [--lnbits] [--lnurlw] [--clink]
```
Where:
- `target` - File or directory to check
- `--clink` - Focus on CLINK offer/debit flow
- `--wallet` - Focus on wallet operations
## Lightning.Pub Integration
- `target` — file or directory to check (default: `packages/lnbits/src/` + `apps/machine/src/services/lightning.ts`)
- `--lnbits` — focus on LNbits nostr-transport RPC + subscription surface
- `--lnurlw` — focus on LNURL-withdraw cash-in flow (link creation, callback URL composition, subscription)
- `--clink` — focus on the kind-21003 management surface (operator commands)
### Connection
Lightning.Pub uses Nostr for all communication:
## What to validate
### 1. Wire envelope (kind 21000)
Each RPC request/response is a kind-21000 event with NIP-44 v2-encrypted JSON in `content`. The plaintext shape is:
```jsonc
// Request
{
"rpc_name": "create_invoice",
"request_id": "create_invoice-7-ab12cd",
"wallet_id": "<uuid>", // present for AUTH_WALLET RPCs
"body": { /* RPC-specific */ },
"query": { /* RPC-specific */ } // optional
}
// One-shot reply
{
"status": "OK" | "ERROR",
"request_id": "create_invoice-7-ab12cd",
"data": { /* RPC-specific */ },
"error": "..." // present when status==ERROR
}
// Subscription push
{
"status": "OK",
"request_id": "sub-9-ef34gh",
"subscription_id": "<server-issued>",
"data": { "payment": { /* Payment */ } } |
{ "closed": true, "reason": "ttl"|"unsubscribed" }
}
```
Required checks:
- [ ] `request_id` is unique per outbound RPC (no reuse across sessions)
- [ ] The pending map is keyed by `request_id` and cleaned up on resolve/reject
- [ ] Subscription pushes are matched by `subscription_id` (top-level field, not in `data`)
- [ ] The handler resolves the subscription's `subscriptionId` only after the ACK arrives — pre-ACK pushes (rare but possible) are handled
- [ ] The reply listener decrypts with `decryptContentV2(identity, serverPubkey, ev.content)` and silently skips events that don't decrypt (wrong sender)
- [ ] Events are signed with `finalizeEvent` from `nostr-tools` using the ATM's `identity.privateKey` — server identifies the calling account from the signature alone
### 2. Encryption (NIP-44 v2)
LNbits transport requires NIP-44 v2, NOT NIP-04. Required checks:
- [ ] All kind-21000 content is `encryptContentV2`/`decryptContentV2` (not `encryptContent`/`decryptContent`, which are the v1/NIP-04 paths kept around for CLINK compatibility)
- [ ] The shared secret is derived from `identity.privateKey × serverPubkey` (or the reverse on the server side — symmetric)
- [ ] No code logs ciphertext or shared secrets
### 3. Authentication model
LNbits derives the calling account from the event signature. There is **no admin token, no API key, no client cert** on the ATM. Required checks:
- [ ] No code stores or references an admin token
- [ ] No code makes outbound HTTP to LNbits (other than via `VITE_LNBITS_HTTP_URL` for the LNURL-withdraw callback, which is the customer wallet's path — the ATM itself doesn't hit HTTP)
- [ ] The ATM's `identity.privateKey` is loaded once from `/var/lib/bitspire/.env` and never logged
### 4. Cash-out flow (`--lnbits`)
In `services/lightning.ts → generateInvoice / watchInvoice`:
```typescript
// Connection via nprofile
const nprofile = 'nprofile1...' // Contains pubkey + relay hints
// Generate
const payment = await lnbits.createInvoice(walletId, {
amount: amountSats,
memo: 'bitSpire - Cash Out',
unit: 'sat',
})
// All operations are Nostr events to Lightning.Pub's pubkey
await relay.publish({
kind: 21002, // CLINK debit
content: encrypted_request,
tags: [['p', lightningPubPubkey]],
// Watch — must filter by payment_hash to avoid acting on unrelated settlements
const decoded = await lnbits.decodePayment(invoice)
const paymentHash = decoded.payment_hash
const subId = await lnbits.subscribePayments(walletId, {
payment_hash: paymentHash,
max_seconds: 600,
}, push => {
if (push.payment_hash !== paymentHash) return // belt-and-suspenders
if (push.status !== 'success') return
callback(push.preimage ?? 'payment-confirmed')
})
```
### Required Checks
- [ ] Using correct event kinds (21001, 21002, 21003)
- [ ] Proper NIP-44 encryption for requests
- [ ] Handling async responses via subscription
- [ ] Proper error handling for payment failures
Required checks:
## CLINK Protocol
- [ ] `subscribePayments` filter always specifies `payment_hash` for invoice watching (filter-less subscriptions are server-rejected; broader filters expose us to acting on unrelated settlements)
- [ ] `max_seconds` is clamped — LNbits clamps server-side to [1, 600], so values outside that range are silently changed
- [ ] The callback checks `push.payment_hash` matches the expected one (defense in depth)
- [ ] The callback checks `push.status === 'success'` before treating as paid
- [ ] On cleanup, `unsubscribe(walletId, subId)` is called
### Offer Flow (Kind 21001)
```
User scans noffer → Wallet sends 21002 → ATM responds with invoice → User pays
### 5. Cash-in flow (`--lnurlw`)
In `services/lightning.ts → generateLnurlWithdraw`:
```typescript
const link = await lnbits.createWithdrawLink(walletId, {
title: '...',
min_withdrawable: ctx.satsAmount,
max_withdrawable: ctx.satsAmount,
uses: 1,
wait_time: 1,
is_unique: false,
})
const callbackUrl = `${CONFIG.lnbitsHttpUrl.replace(/\/+$/, '')}/withdraw/api/v1/lnurl/${link.unique_hash}`
const lnurl = encodeLnurl(callbackUrl) // bech32 HRP="lnurl", uppercase
const subId = await lnbits.subscribePayments(walletId, {
tag: 'withdraw',
link_id: link.id,
max_seconds: 600,
}, push => { /* mark session claimed, dispense */ })
```
Validation:
- [ ] noffer encoding is valid
- [ ] Relays included in offer
- [ ] Price type correctly specified (fixed/variable/spontaneous)
- [ ] Amount bounds validated
Required checks:
### Debit Flow (Kind 21002)
Request structure:
```json
{
"method": "pay_invoice",
"params": {
"invoice": "lnbc...",
"amount_msat": 100000
}
}
```
- [ ] `uses: 1` — single-use prevents replay
- [ ] `min_withdrawable === max_withdrawable === ctx.satsAmount` — exact amount, no operator slippage
- [ ] The LNURL is composed from `VITE_LNBITS_HTTP_URL` + `link.unique_hash` (the transport `lnurlw_create_link` returns `link.lnurl === null` — those fields are filled by HTTP views, not the create RPC)
- [ ] bech32 encoding uses HRP `"lnurl"` and the result is uppercased per BOLT/LNURL convention
- [ ] Subscription filter is `tag: 'withdraw'` + `link_id: link.id` — this is what the withdraw extension stamps on settlement events
- [ ] On session abort, the cleanup closure both unsubscribes AND deletes the withdraw link (so a half-completed session doesn't leave a redeemable QR alive on LNbits)
Response structure:
```json
{
"result": {
"preimage": "...",
"fee_msat": 1000
}
}
```
### 6. Operator commands (`--clink`, kind-21003)
Validation:
- [ ] Invoice format valid (BOLT11)
- [ ] Amount matches invoice
- [ ] Preimage verified against payment hash
- [ ] Fee within acceptable bounds
- [ ] Timeout handling
The kind-21003 management surface is the one piece of CLINK still wired on `dev`. It's LP-independent (operators talk directly to the ATM over nostr) so it survived the migration.
### Manage Flow (Kind 21003)
For operator commands:
```json
{
"method": "get_balance",
"params": {}
}
```
Required checks:
Validation:
- [ ] Only authorized operators can send
- [ ] Commands are properly authenticated
- [ ] Responses handled securely
- [ ] `CLINKClient` is initialized with `operatorPubkey: CONFIG.operatorPubkeys` (no Lightning.Pub pubkey mixed in — that's LP-era)
- [ ] `clink.onManagement` handler routes through `handleManagementCommand` which dispatches by `request.command` (manual dispense, status query, etc.)
- [ ] Operator authentication is by pubkey allowlist — not a separate token
- [ ] Sensitive ops (`dispenseCash`) only fire when the state machine is idle (`isIdle.value` check)
## Invoice Validation
### BOLT11 Checks
- [ ] Valid bech32 encoding
- [ ] Expiry not passed
- [ ] Amount matches expected
- [ ] Description hash valid (if used)
- [ ] Payment hash extractable
### Security Checks
- [ ] Never pay same invoice twice
- [ ] Amount limits enforced
- [ ] Rate limiting on payments
- [ ] Proper logging (no sensitive data)
## Wallet Operations
### Balance Queries
- [ ] Cached appropriately (not every render)
- [ ] Error handling for offline
- [ ] Display in correct units (sats, not msat)
### Invoice Generation
- [ ] Unique payment hashes
- [ ] Reasonable expiry times
- [ ] Description for record-keeping
- [ ] Amount in correct units
## Output Format
## Output format
```markdown
## Lightning.Pub Conformity: [target]
## Lightning backend conformity: [target]
### CLINK Protocol
| Flow | Status | Notes |
|------|--------|-------|
| Offer (21001) | ✅ | |
| Debit (21002) | ⚠️ | Missing timeout |
| Manage (21003) | ✅ | |
### LNbits transport
| Concern | Status | Notes |
|---|---|---|
| Wire envelope | ✅ | |
| NIP-44 v2 encryption | ✅ | |
| Signature-only auth | ✅ | No admin tokens found |
| Subscription handling | ⚠️ | Missing payment_hash filter on watchInvoice cleanup |
### Invoice Handling
- [ ] `file:line` - Issue description
### Cash-out / Cash-in flows
- [ ] `file:line` — Issue description + fix
### Integration Issues
- [ ] Description with fix suggestion
### Operator (kind-21003)
- [ ] `file:line` — Issue description
### Recommendations
- [ ] Performance/UX improvements
```
## Example Usage
## Example
```
/lightning-check packages/lightning/src/ --clink
/lightning-check packages/lnbits/src/ --lnbits
```
Output:
```markdown
## Lightning.Pub Conformity: packages/lightning/src/
## Lightning backend conformity: packages/lnbits/src/
### CLINK Protocol
| Flow | Status | Notes |
|------|--------|-------|
| Offer (21001) | ✅ | Properly encoded |
| Debit (21002) | ⚠️ | No timeout handling |
### LNbits transport
| Concern | Status | Notes |
|---|---|---|
| Wire envelope | ✅ | request_id uniqueness OK, pending map cleaned up on timeout |
| NIP-44 v2 encryption | ✅ | encryptContentV2/decryptContentV2 used exclusively |
| Subscription handling | ✅ | subscriptionId resolved after ACK, push handlers race-safe |
| Cleanup | ⚠️ | unsubscribe error swallowed silently — log at warn level |
### Issues Found
- [ ] `client.ts:89` - Debit request has no timeout, could hang indefinitely
- [ ] `client.ts:112` - Payment hash not verified against preimage
### Recommendations
- Add 30-second timeout for CLINK debit requests
- Implement invoice deduplication to prevent double-pay
### Cash-out flow
- [ ] `packages/lnbits/src/client.ts:212` — watchInvoice resolves on first success push but doesn't unsubscribe until subIdRef is assigned; if push lands before subscribePayments resolves, leaks the subscription
```
## Related skills
- `/nostr-check` — validates NIP-01 / NIP-44 conformance at the nostr-client layer
- `/security` — Bitcoin/Lightning vuln audit
- `/test` — runs vitest + flow validation

View file

@ -22,7 +22,7 @@ Location: `*.test.ts` or `*.spec.ts` alongside source files
```bash
pnpm test # All tests
pnpm test --filter @lamassu/nostr-client # Specific package
pnpm test --filter @bitSpire/nostr-client # Specific package
```
### 2. Integration Tests