Skip to content

feat(utils): findCashuPayload to locate tokens and payment requests in text - #967

Merged
robwoodgate merged 6 commits into
cashubtc:mainfrom
KvngMikey:feat/find-cashu-payload
Aug 20, 2026
Merged

feat(utils): findCashuPayload to locate tokens and payment requests in text#967
robwoodgate merged 6 commits into
cashubtc:mainfrom
KvngMikey:feat/find-cashu-payload

Conversation

@KvngMikey

@KvngMikey KvngMikey commented Aug 14, 2026

Copy link
Copy Markdown
Member

TL;DR

Adds findCashuPayload(text), which locates the first cashu token or payment request inside arbitrary text and returns the encoded payload verbatim. Wallets can drop their hand-rolled URL-prefix tables and regexes.

Fixes #849

Why

Wallets receive payloads buried in whatever the user pasted: chat messages, clipboard blobs, bitcoin: URI parameters, wallet-site URLs. getDecodedToken already handles the clean cases (it strips cashu:, cashu://, web+cashu://), but nothing in the library locates a payload inside text that contains other content. Every wallet ends up maintaining its own curated table of known wallet URL prefixes plus a regex, and those tables rot as the ecosystem churns. Scanning for the payload itself subsumes every wrapper form, so there is no per-wallet knowledge to maintain.

Changes

Two-stage, exactly as specced in #849:

  1. Candidate scan: one scanner per prefix (cashuA/cashuB, creqA, creqb1), each a literal prefix plus a single character-class quantifier. No alternation, no nested quantifiers, so no catastrophic backtracking. creqb1 (NUT-26) is matched case-insensitively because QR alphanumeric mode emits the uppercase form.
  2. Decode to validate: handleTokens for tokens, PaymentRequest.fromEncodedRequest for requests. A regex alone false-positives; the decoder is the authority. First candidate that decodes, by position, wins.

Notes for review:

  • The charset is base64url only ([A-Za-z0-9=_-]), not base64. This is the one place the implementation departs from the issue text, which says "base64/base64url charset". Including + and / is actively wrong: a token in a URL path (example.com/cashuB…/more) has the trailing segment swallowed into the candidate, and base64 decoding ignores what follows the padding while CBOR ignores trailing bytes, so the candidate decodes successfully and the payload comes back as cashuB…/more. The decoders validate the prefix, not the boundary. The cashu specs are base64url throughout (NUT-00 tokens, NUT-18 creqA), so nothing spec-compliant is lost. Regression test pins all three payload types. Happy to revisit if you want the wider charset back.
  • Re-entry on failed decode resumes at matchIndex + 1, not match end, so a payload swallowed inside a failed greedy span is still found, the cashuB-inside-cashuA example is a test case.
  • Decode-attempt cap: MAX_PAYLOAD_DECODE_ATTEMPTS = 16 in utils/limits.ts, following the house pattern there. Bounds the adversarial prefix-stuffed case to linear total work. The number is a suggestion and is a one-integer change if needed.
  • Token validation uses the module-private handleTokens via removePrefix: keyset-free, and avoids getDecodedToken's 5.0 keysetIds requirement, which the finder has no business needing.
  • CashuPayloadKind is exported as a named alias for 'token' | 'paymentRequest' so consumers can type against it. Structurally identical to the inline signature in the issue.
  • Non-goals honored: no wallet URL table, no multi-match variant, no clipboard/DOM integration, and the returned payload is never normalized (uppercase CREQB1… comes back uppercase — test case).

Docs: new section on the Helpers page; the usage-index Helpers row previously named mint URL normalization as the page's only content, so it is broadened.

Impact

Purely additive, one new function and one new exported type, no existing behavior touched. The API report diff is those two entries and nothing else.

Copilot AI lite review requested due to automatic review settings August 14, 2026 15:34
@github-project-automation github-project-automation Bot moved this to Backlog in cashu-ts Aug 14, 2026
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.97%. Comparing base (59d36b1) to head (c4b50dd).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #967      +/-   ##
==========================================
+ Coverage   95.92%   95.97%   +0.04%     
==========================================
  Files          56       56              
  Lines        5961     5984      +23     
  Branches     1516     1521       +5     
==========================================
+ Hits         5718     5743      +25     
+ Misses        103      102       -1     
+ Partials      140      139       -1     
Flag Coverage Δ
integration 37.29% <13.04%> (-0.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new public helper findCashuPayload(text) to locate the first embedded Cashu token (cashuA/cashuB) or payment request (creqA/creqb1/CREQB1) inside arbitrary text, validate it by decoding, and return the encoded payload verbatim for wallets to process.

Changes:

  • Implement findCashuPayload + exported CashuPayloadKind in src/utils/core.ts with a capped number of decode attempts.
  • Add MAX_PAYLOAD_DECODE_ATTEMPTS limit in src/utils/limits.ts.
  • Add tests and documentation for token/payment-request detection in pasted text and URLs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/utils/core.test.ts Adds test coverage for findCashuPayload behavior (wrappers, punctuation, ordering, cap behavior, and URL-path regression).
src/utils/limits.ts Introduces MAX_PAYLOAD_DECODE_ATTEMPTS to cap decode validations during scanning.
src/utils/core.ts Implements CashuPayloadKind, scanner regexes, and the findCashuPayload decode-to-validate search loop.
etc/cashu-ts.api.md Updates API report to include the new exported type and function.
docs-src/usage/usage_index.md Updates Helpers page description to include token detection.
docs-src/usage/helpers.md Documents findCashuPayload and shows a basic usage example.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/utils/core.ts Outdated
Comment thread src/utils/core.ts
Comment thread src/utils/core.ts Outdated
Comment thread docs-src/usage/helpers.md

@robwoodgate robwoodgate left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial pass with executed probes; details inline (this supersedes my earlier summary comment, now removed). Sequencing note: #986 is queued ahead of this PR and makes junk-suffixed candidates fail uniformly on both platforms. Previously they were returned with the junk included (v4/creqA), missed entirely (v3), or platform-dependent (Node Buffer vs browser atob). Please rebase once it merges.

Comment thread src/utils/core.ts Outdated
Comment thread src/utils/core.ts
Comment thread src/utils/core.ts Outdated
Comment thread src/utils/limits.ts Outdated
Comment thread test/utils/core.test.ts
Comment thread test/utils/core.test.ts Outdated
Comment thread docs-src/usage/helpers.md
Comment thread docs-src/usage/usage_index.md Outdated
Comment thread src/utils/core.ts Outdated
robwoodgate added a commit that referenced this pull request Aug 19, 2026
…latforms (#986)

Base64 and CBOR payload decoding now gives the same verdict on Node and
in browsers: well-formed input decodes, malformed input throws
`CTSError`.

## Why

The two platform decoders (`Buffer`, `atob`) disagree on some edge
cases, so the same string could decode on one platform and fail on the
other. #967 validates scanned text candidates by decoding them, so
verdicts must be uniform across platforms; this PR is sequenced ahead of
it.

## Changes

- `Bytes.fromBase64` normalizes (trim, whitespace strip, base64url
mapping, padding) and validates in one place, then decodes the validated
string, so both backends see identical input.
- `decodeCBOR` requires a payload to consume the whole buffer.
- `encodeBase64ToJson` delegates to `Bytes.fromBase64` directly; the
redundant private pre-mapping helper is removed and the base64url output
mapping has a single implementation.
- Tests run a shared accept/reject corpus under both decoder backends.

## Impact

`getDecodedToken`, `getDecodedTokenBinary` and
`PaymentRequest.fromEncodedRequest` throw `CTSError` for malformed
payloads on every platform. Line-wrapped, unpadded, and edge-whitespace
input still decodes. Full suite and prtasks pass.
…ts in text

Two-stage: prefix-anchored charset candidates, then decode-to-validate via
handleTokens / PaymentRequest.fromEncodedRequest, first valid match by
position wins. Each pattern is a literal prefix plus one character class and
a failed candidate re-enters at matchIndex + 1 under a decode-attempt cap,
so the scan stays linear on attacker-influenced input.

Token and creqA charsets are base64url only. Including `+` and `/` let a
token in a URL path run on into the following segment, and the decoders
accept the trailing junk rather than reject it, so the payload came back
corrupted; a regression test pins this.
Adds the section to the Helpers page and broadens the Helpers row in the
usage index, which previously named mint URL normalization as the page's
only content. The example uses the idioms the other pages already document
(getDecodedToken with keyset IDs, decodePaymentRequest).
… docs

The decode-attempt cap bounds how many candidates are tried but not the cost
of each, so a hostile paste of millions of charset characters produced one
enormous match for the base64/CBOR decoders. Scan quantifiers are now bounded
by MAX_PAYLOAD_LENGTH (256 KiB, ~90x the largest token in the test tree).

Also corrects two doc comments that drifted from the implementation: the
scanner class is base64url only, not both alphabets, and the NUT-26 prefix is
creqb1/CREQB1 rather than creqB.
Bound a single candidate at MAX_PAYLOAD_LENGTH, lowered to 1 MiB: V8 matches
a bounded quantifier recursively, so a match of a few million characters
overflows the stack inside exec, which sits outside the decode try/catch. The
previous cap test passed with the bound removed; it now pads a v3 token either
side of the limit so only length decides the outcome.

Canonicalise case-insensitive matches to lowercase, so a mixed-case creqb1
paste no longer hands callers spec-invalid bech32m. Guard the argument at the
public boundary, since exec would otherwise coerce a non-string and scan it.

Docs: justify the base64url class from NUT-00 and the delimiters it stops at
rather than from decoder leniency, record the other two causes of a null
return, show getTokenMetadata alongside getDecodedToken since short keyset IDs
only resolve against keysets the caller holds, and name payment requests in
the usage index row. Hoist the duplicated token fixtures in the test file.
Now that base64 and CBOR decoding validate strictly, a candidate that carries
a base64url suffix fails rather than decoding its valid prefix and returning
the junk attached. Covers v4, v3 and creqA, since the three previously failed
in three different ways.
@KvngMikey
KvngMikey force-pushed the feat/find-cashu-payload branch from 4e9291d to c4b50dd Compare August 19, 2026 21:14

@robwoodgate robwoodgate left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome job, great utility, thanks @KvngMikey .

This will be v5 only, as it may change decoding experience on some platforms, throwing cases that previously passed etc.

@robwoodgate
robwoodgate merged commit d094499 into cashubtc:main Aug 20, 2026
26 of 28 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in cashu-ts Aug 20, 2026
@KvngMikey

Copy link
Copy Markdown
Member Author

Awesome job, great utility, thanks @KvngMikey .

This will be v5 only, as it may change decoding experience on some platforms, throwing cases that previously passed etc.

no problem, as soon as V5 lands, i can make the downstream PRs to capture this.

robwoodgate added a commit that referenced this pull request Aug 28, 2026
[5.0.0-rc.8](v5.0.0-rc.7...v5.0.0-rc.8)
(2026-08-27)

### Features

* **model:** expose NUT-06 urls, time and tos_url on MintInfo
([#1003](#1003))
([832a71c](832a71c))
* **utils:** findCashuPayload to locate tokens and payment requests in
text ([#967](#967))
([d094499](d094499))
* **wallet:** add LockBuilder and asLocked as forward-compatible names
([#995](#995))
([4d69fe7](4d69fe7))
* **wallet:** add swap preview serialize helpers and persistence docs
([#972](#972))
([02f0bfe](02f0bfe))


### Bug Fixes

* **transport:** split request option precedence by option class
([#968](#968))
([603553d](603553d))
* **utils:** make base64 and CBOR payload decoding consistent across
platforms ([#986](#986))
([59d36b1](59d36b1))
* **wallet:** keep large proofs in the close-match selection pool
([#1001](#1001))
([a9ade37](a9ade37))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

feat(utils): findCashuPayload to locate tokens and payment requests in text

3 participants