From 4e8f4df73f9bb84983695e07d8d6289c6fe5a597 Mon Sep 17 00:00:00 2001 From: mftee Date: Mon, 27 Jul 2026 11:49:04 +0000 Subject: [PATCH] docs(product-audit): add knowledge-base entries for #253, #259, #275 Adds 3 documentation files under bridgelet-product-audit/. Documentation-only; no changes to frontend/ or mobile/. --- .../glossary/custodial-model-in-practice.md | 50 +++++++++++++++++++ .../glossary/docs-mdx-vs-pdf-pairs.md | 50 +++++++++++++++++++ .../claim-url-security-properties.md | 50 +++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 bridgelet-product-audit/glossary/custodial-model-in-practice.md create mode 100644 bridgelet-product-audit/glossary/docs-mdx-vs-pdf-pairs.md create mode 100644 bridgelet-product-audit/integration-notes/claim-url-security-properties.md diff --git a/bridgelet-product-audit/glossary/custodial-model-in-practice.md b/bridgelet-product-audit/glossary/custodial-model-in-practice.md new file mode 100644 index 0000000..3138b1f --- /dev/null +++ b/bridgelet-product-audit/glossary/custodial-model-in-practice.md @@ -0,0 +1,50 @@ +# Custodial Model in Practice + +## The starting definition + +`docs/GLOSSARY.md` defines the **Custodial Model** as a system where a service +temporarily controls assets on behalf of users. + +That is abstract by design. This entry grounds it in the code paths that actually +implement it — and separates it from a nearby concept it is easy to confuse with. + +## Where custody actually happens + +The custodial mechanism lives in **bridgelet-sdk**, not in this repo. Per +bridgelet-sdk-audit's `two-phase-account-creation.md`, a backend-held **funding +keypair** creates and funds the ephemeral account, and the backend later signs the +sweep that moves funds to the recipient's destination. + +Two words in the glossary definition do real work: + +- **"temporarily"** — custody spans the window between the sender's payment and + the recipient's claim. The ephemeral account exists to be emptied. +- **"on behalf of users"** — during that window the recipient cannot move the + funds themselves. They hold a claim token, not a key. Bridgelet's backend holds + the only key that can sweep the account. + +This is what makes the product work: the recipient doesn't need a wallet at claim +time precisely *because* someone else is holding the asset for them. +`docs/security-model.mdx` reflects this — sweeps are gated on a valid claim token +plus a **server-side signature with the ephemeral private key**, with no +client-supplied authorization credential. + +## What this is *not*: the `generated` wallet type + +Do not conflate the custodial funding key with the `generated` `WalletType`. + +| | Custodial funding key | `generated` wallet | +|---|---|---| +| Created | Backend, per ephemeral account | Browser, via `Keypair.random()` | +| Held by | Bridgelet's backend | The user | +| Purpose | Fund and sweep the ephemeral account | Give a user their own wallet | +| Repo | bridgelet-sdk | This repo (`frontend/lib/wallet.ts`) | + +The `generated` path is the **opposite** of custodial: `generateNewWallet()` +returns the secret key to the user's browser, and Bridgelet never sees it — a +user-held key that happens to be created client-side. + +Both can appear in one claim: a recipient may generate a wallet (non-custodial) to +receive funds swept from an ephemeral account (custodial). Conflating them +misstates who is trusted with what, in a system whose value proposition is that +boundary. See [`generated-wallet-key-custody.md`](../integration-notes/generated-wallet-key-custody.md). diff --git a/bridgelet-product-audit/glossary/docs-mdx-vs-pdf-pairs.md b/bridgelet-product-audit/glossary/docs-mdx-vs-pdf-pairs.md new file mode 100644 index 0000000..d0c50e0 --- /dev/null +++ b/bridgelet-product-audit/glossary/docs-mdx-vs-pdf-pairs.md @@ -0,0 +1,50 @@ +# `docs/` — `.mdx` Sources and `.pdf` Exports + +Some documents in `docs/` exist as an editable `.mdx` source with a matching +`.pdf`; others exist **only** as a PDF. The distinction matters the moment +something needs updating. + +## Paired — `.mdx` source with a `.pdf` export + +| Source | Export | +|---|---| +| `architecture.mdx` | `architecture.pdf` | +| `security-model.mdx` | `security-model.pdf` | +| `integration-guide.mdx` | `integration-guide.pdf` | + +For these, the `.mdx` is authoritative. Edit it, then regenerate the PDF. + +## PDF-only — no `.mdx` source + +- `getting-started.pdf` +- `mvp-specification.pdf` +- `use-cases.pdf` + +These have no source file in the repository. + +## Practical implication + +**A PDF without a source can only be manually re-created.** There is nothing to +edit and re-render — changing `use-cases.pdf` means reproducing the whole document +in whatever tool made it, matching its styling by eye, and hoping the result +doesn't look like a different document. + +Three further consequences worth naming: + +- **They are opaque to review.** A PDF in a diff shows as a binary blob. A + reviewer cannot see what changed, so content changes arrive effectively + unreviewed. +- **They are not greppable.** Searching the repo for a term will find it in the + three `.mdx` sources and miss it in the PDF-only trio entirely — so these + documents are easy to forget when updating terminology. +- **Drift is silent even for paired files.** Nothing enforces that a `.pdf` was + regenerated after its `.mdx` changed. A stale export can sit alongside an + updated source indefinitely, and readers who open the PDF get the old answer + with no indication it is out of date. + +Note that `security-model.pdf` is among the paired files — the security +documentation specifically is exposed to this drift risk. + +**Related:** +[`../runbooks/rebuild-docs-pdf-from-mdx.md`](../runbooks/rebuild-docs-pdf-from-mdx.md) +— the regeneration procedure — and the postmortem entry on regeneration risk. diff --git a/bridgelet-product-audit/integration-notes/claim-url-security-properties.md b/bridgelet-product-audit/integration-notes/claim-url-security-properties.md new file mode 100644 index 0000000..c76eb1a --- /dev/null +++ b/bridgelet-product-audit/integration-notes/claim-url-security-properties.md @@ -0,0 +1,50 @@ +# Claim URL — Security Properties + +What makes a claim URL safe (or unsafe) to share, from the frontend's view. + +## Stated guarantees + +`docs/GLOSSARY.md` defines a **Claim Token** as the token letting a recipient +securely claim funds, and a **Claim URL** as the link carrying it. +`docs/security-model.mdx` is more specific: + +- Claim tokens are **JWTs signed by the backend**, using a secret the frontend + never sees — integrity-protected. +- **Single-use** — consumed on redemption, cannot be replayed. +- **Entropy:** derived from a UUID v4 `accountId` (122 bits). Brute-force + enumeration is not feasible, and verify calls are rate-limited (1,000/hour/IP). +- **Path segment, not query string** (`/claim/[token]`) — deliberately, to avoid + leaking the token in `Referer` headers. +- **Recipients are unauthenticated.** The token "is the sole proof of + entitlement. No login, no session." + +## The bearer problem + +That last point is the defining property: **whoever holds the link can attempt the +claim.** There is no identity check binding the token to an intended recipient, so +a forwarded, screenshotted, or intercepted link is as good as the original. + +`security-model.mdx` logs this as **T-01 (claim token theft)** and rates it +mitigated — but read the mitigation precisely: single-use means an attacker cannot +replay a *spent* token, and "first valid redemption wins." That protects against +reuse, **not against a thief redeeming first.** If an attacker gets the link +before the recipient acts, they claim the funds and the legitimate recipient finds +a spent token. The document's own recommendation — share via end-to-end encrypted +channels — is the real control. + +**The payload is not encrypted.** Signed JWTs are integrity-protected, not +confidential: `accountId`, `amount` and `assetCode` are readable by anyone holding +the token — including link-preview services and analytics pipelines. JWE is future work. + +## Practical consequences + +- Treat a claim URL as **cash in an envelope**, not an addressed letter; don't + paste them into shared tickets, logs or chat channels. +- Expiry is not a loss: it lets the sender reclaim via a backend return sweep. + +## Double claims + +A second claim is caught on-chain by the already-swept guard (bridgelet-audit's +`account-status-state-machine.md`); whether that reason reaches the user is traced +in [`three-repo-error-surface-consistency.md`](./three-repo-error-surface-consistency.md). +**T-13** (sweep authorization) is also an MVP stub there.