diff --git a/quartz/plans/2026-06-09-clink.md b/quartz/plans/2026-06-09-clink.md index 93d65a7878..d7b91db15e 100644 --- a/quartz/plans/2026-06-09-clink.md +++ b/quartz/plans/2026-06-09-clink.md @@ -244,3 +244,55 @@ against a real CLINK service is not automated (needs a device or a mock/live service). Feasible later via quartz's in-process relay server (`nip01Core/relay/server/`) plus a mock CLINK responder; flagged but not built. + +--- + +## Interop review & spec-conformance pass (2026-06-10) + +Reviewed against the whole `shocknet/CLINK` ecosystem (Lightning.Pub, clink-sdk, +ShockWallet, Zeus, Stacker News, bridgelet, clinkme.dev) and re-audited every +spec file line-by-line against the code. **Verdict: the consume-only +payer/requestor role is conformant and interoperable; no real correctness bugs.** + +**Interop fixes shipped:** +1. Manage `details` single-object responses parse (Jackson + `ACCEPT_SINGLE_VALUE_AS_ARRAY`) — Lightning.Pub returns a bare object for + create/update/get, an array only for `list`. +2. `NOffer.encode()` always emits the price-type TLV (3); decode defaults an + absent/unknown type to SPONTANEOUS. The SDK + bridgelet decoders throw on a + missing TLV 3, so omitting it made our pointers undecodable by JS consumers. +3. `Nip05Parser.parseClinkOffer` accepts bridgelet's flat-string shape as well + as the spec's per-name map. +4. Offer receipts are a parseable primitive (`OfferEvent.createReceipt` / + `decryptReceipt`, `OfferClient.parseReceipt`, `OfferReceipt.isOk`). +5. `ClinkOfferPayer` signs offer requests with an ephemeral key (privacy parity + with the SDK/Zeus/Stacker News). Debits keep the persistent account key + (budgets need a stable app identity). + +**Conformance hardening shipped:** `NDebit.parse` rejects a non-32-byte k1; +`DebitClient.requestBudget` validates `frequency.unit ∈ {day,week,month}`; +`OfferClient` caps `description` at 100 chars; `DebitResponse.failureDetail()` +surfaces GFY `range`/`retry_after` in the debit zap error path. + +**New tests:** `ClinkWireShapeTest` (golden JSON payloads from the public-domain +specs), a clink-demo/SDK canonical `DEFAULT_NOFFER` vector, plus regressions for +each fix above. + +**Three "CRITICAL" review findings were verified FALSE — do NOT re-chase them:** +- *Offer price encoding for ≥2³¹ sats* is correct: `addInt` writes the low 32 + bits, which are bit-identical to the unsigned 4-byte BE (proven by + `offerLargePriceRoundTripIsUnsigned`, 3e9 sats). Only ≥2³² loses data, which + the spec's 4-byte field can't represent anyway. +- *Parser "wrongly accepts `nostr:`/`lightning:` wrappers"* is not a violation: + the MUST-NOT-wrap rule governs producing QR (our `encode()` is bare); lenient + decode is Postel-legal. +- *Manage "create shape deviation"* — the spec's inline-create example is the + outlier; Lightning.Pub/SDK/clink-demo all use nested `offer.fields` for create, + which we match. + +**Remaining non-bugs (intentional / out-of-role):** we don't reject responses +lacking `clink_version` (Lightning.Pub omits it — rejecting breaks interop); the +offer Payment Receipt UI subscription is unwired (primitive ships); `clink_debit` +discovery and the unrestricted-access debit request are creditor/app-side +features outside the consume-only role; noffer TLV 5 currency is dormant +(unimplemented by the SDK and Lightning.Pub too).