Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,8 +816,18 @@ design — the web compile graph never imports them.
- Deploys: any push to `main` touching `lib/`, `web/`, `site/` or
pubspec redeploys GitHub Pages (`.github/workflows/deploy-pages.yml`).
The artifact keeps `site/` byte-identical at the domain root (the
`/nft-claim` forwarder and `.well-known` app-link files are
load-bearing for native deep links) and serves the app under `/app/`.
`/nft-claim` and `/autopin-complete` forwarders and `.well-known`
app-link files are load-bearing for native deep links) and serves
the app under `/app/`.
- Blox pairing from the web: Settings → My Devices → Pair Blox hands
off to https://blox.fx.land (contract: `docs/AUTOPIN-HANDOFF.md`),
which returns to `files.fx.land/autopin-complete#secret=…` →
"Continue in web app". Known limitation: the web app cannot use the
Blox LAN gateway (`http://<lan-ip>` is mixed content and browsers
have no mDNS), so web downloads always come from the cloud; pairing
still makes the Blox auto-pin your files. The stored credentials are
local to that browser — pair native/desktop FxFiles separately
(desktop's manual dialog accepts the pairing secret).
- E2E harness: build with `--dart-define=E2E=true`, then drive
headless Chrome with `?e2e=create|signin|restore|signout|upload|`
`download|list|delete|share` (results print with an `[e2e]` prefix).
Expand Down
6 changes: 5 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="fxfiles"/>
</intent-filter>
<!-- HTTPS App Links for shareable NFT claim URLs -->
<!-- HTTPS App Links for shareable NFT claim URLs and the Blox
pairing return (files.fx.land/autopin-complete#secret=…;
the fragment is delivered with the intent data). Paths must
also be listed in site/.well-known (AASA for iOS). -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="files.fx.land" android:pathPrefix="/nft-claim"/>
<data android:scheme="https" android:host="files.fx.land" android:pathPrefix="/autopin-complete"/>
</intent-filter>
<!-- WalletConnect callback for signature responses -->
<intent-filter>
Expand Down
41 changes: 37 additions & 4 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,46 @@ FxFiles app

### Additional UX — Pairing

Contract: `docs/AUTOPIN-HANDOFF.md` (v1.1). URL builders + the return parser live in
`lib/core/services/blox_pairing_links.dart` (dart:io-free; unit-tested).

1. User goes to **Settings → My Devices → Pair Blox**
2. App opens deeplink to **FxBlox companion app**, passing JWT
3. FxBlox app calls blox's `AutoPinPair(token, endpoint)` via libp2p
2. FxFiles hands off to FxBlox with the SAME params on one of two carriers:
- **App** (mobile): `fxblox://autopin-pair?token=<JWT>&endpoint=<https api base>&returnUrl=<template>`
(query — the custom scheme is routed by the OS, never a server)
- **Web** (`https://blox.fx.land/autopin-pair#token=…&endpoint=…&returnUrl=…` — **fragment**, v1.1,
so the JWT never reaches the Pages server/CDN logs or a Referer; FxBlox-web reads `location.hash`
first and accepts the v1 `?token=…` query as a fallback): always from the web build
(`lib/web/screens/web_blox_pairing_screen.dart`, same-tab), from desktop's manual pairing dialog
("Pair in browser"), and as the mobile fallback when the FxBlox app is not installed
(`launchUrl` false/throws → "Pair in browser" dialog).
- `returnUrl` is a URL-encoded **template** with the literal placeholders `$secret`, `$hardwareId`,
`$bloxPeerId`, `$bloxName`: `https://files.fx.land/autopin-complete#secret=$secret&hardwareId=$hardwareId&bloxPeerId=$bloxPeerId&bloxName=$bloxName`
(fragment form — the bearer secret never reaches a server).
3. FxBlox calls the blox's `AutoPinPair(token, endpoint)` via libp2p
- go-fula stores `auto_pin_token`, `auto_pin_endpoint`, `auto_pin_pairing_secret` in `box_props.json`
- Returns pairing secret + hardware ID
4. FxBlox app returns deeplink: `fxfiles://autopin-complete?secret=...&hardwareId=...`
5. FxFiles stores pairing secret, hardware ID, peer ID in SecureStorage
4. FxBlox substitutes the placeholders and navigates (user click) to the result. Receivers:
- **Native app link** (`DeepLinkService._handleUniversalLink` → `_handleAutoPinComplete`): iOS
(AASA `/autopin-complete*`) and Android (manifest `pathPrefix="/autopin-complete"`) open FxFiles
directly with the full URL; the parser reads the fragment first, then the query. The legacy
`fxfiles://autopin-complete?secret=…` deep link is still accepted.
- **Static forwarder** `site/autopin-complete/index.html` (when the app link is not verified, the
app is missing, or on desktop): reads the fragment, on mobile auto-tries
`fxfiles://autopin-complete?…` (2.5 s `document.hidden` fallback), otherwise offers
"Open in FxFiles" and "Continue in web app" → `https://files.fx.land/app/#/autopin-complete?…`.
- **Web build** (`lib/main_web.dart` → `captureAutopinReturn()` BEFORE `runApp`): stashes the
params (memory + sessionStorage) and `history.replaceState`s the URL to `#/` so the secret leaves
the address bar and the logged-out router redirect cannot drop it; the web home's post-login init
`takePendingAutopinReturn()`s and navigates to `/blox-pairing` with the params as go_router
`extra`. `/autopin-complete` also exists as a router fallback.
5. FxFiles validates (non-empty secret, length caps, no control chars) and stores pairing secret,
hardware ID, peer ID, name in SecureStorage.

Web limitation: the web build cannot use the Blox LAN gateway (`http://<ip>:9000` is mixed content
from an https page and browsers have no mDNS), so web downloads always come from the cloud; pairing
from the web still makes the Blox auto-pin. The credentials are device-local (that browser); native /
desktop FxFiles pair separately (desktop's manual dialog accepts the pairing secret).

### Additional UX — Blox Discovery

Expand Down
57 changes: 57 additions & 0 deletions docs/AUTOPIN-HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Auto-pin pairing hand-off spec (v1.1)

Contract between **FxFiles** (mobile, desktop, files.fx.land) and **FxBlox** (mobile app or blox.fx.land).

## Outbound (FxFiles → FxBlox)

Mobile deep link (unchanged): `fxblox://autopin-pair?token=<t>&endpoint=<e>&returnUrl=<r>`
Web (v1.1, **fragment**): `https://blox.fx.land/autopin-pair#token=<t>&endpoint=<e>&returnUrl=<r>`

The web carrier puts the parameters in the URL **fragment** so the bearer `token` never reaches the
blox.fx.land server / CDN access logs, is not sent in a `Referer` header, and is not synced as part of
browser history. The custom-scheme link is routed by the OS (no server), so it keeps the query form.

**Receiver (FxBlox-web) rule:** read `location.hash` first; the v1 query form
(`https://blox.fx.land/autopin-pair?token=…`) remains **accepted as a fallback** for older senders.
FxBlox-web should `history.replaceState`-strip the fragment once the params are in memory and serve the
page with `<meta name="referrer" content="no-referrer">`.

| Param | Value | Validation on the FxBlox side |
|---|---|---|
| `token` | URL-encoded cloud.fx.land JWT | non-empty; ≤ 8 KiB |
| `endpoint` | URL-encoded pinning/IPFS API base, e.g. `https://api.cloud.fx.land` | `https:` URL |
| `returnUrl` | URL-encoded **template** containing the literal placeholders `$secret`, `$hardwareId`, `$bloxPeerId`, `$bloxName` | scheme is `https:` (files.fx.land) or `fxfiles:`; all four placeholders present |

The three values are `encodeURIComponent`-style encoded (`Uri.encodeComponent` on the FxFiles side) and
joined as `key=value&key=value`, identically for the query (native) and the fragment (web); the receiver
decodes them the same way (`URLSearchParams` on the hash string works).

## Return (FxBlox → FxFiles)

FxBlox substitutes the placeholders and navigates (user click) to the resulting URL. Recommended template (fragment
form so the bearer secret never reaches a server):

`https://files.fx.land/autopin-complete#secret=$secret&hardwareId=$hardwareId&bloxPeerId=$bloxPeerId&bloxName=$bloxName`

Substituted values MUST be `encodeURIComponent`-ed (a raw `+` would be decoded as a space by the receivers).

`files.fx.land/autopin-complete/` is a static forwarder: on mobile it tries `fxfiles://autopin-complete?…`, otherwise
offers "Continue in web app" → `https://files.fx.land/app/#/autopin-complete?…`.

Legacy template still accepted: `fxfiles://autopin-complete?secret=$secret&hardwareId=$hardwareId&bloxPeerId=$bloxPeerId&bloxName=$bloxName`.

## Versioning

- **v1** — web outbound as a query (`?token=…`). Still accepted by receivers as a fallback.
- **v1.1** (this document) — web outbound moved to the fragment (`#token=…`); return leg unchanged.

A future change adds `&v=2` to the outbound params; receivers must ignore unknown params.

## FxFiles implementation map (this repo)

- Builders + return parser: `lib/core/services/blox_pairing_links.dart` (`buildBloxWebPairUrl` → fragment,
`buildBloxNativePairUrl` → query, `kAutopinReturnTemplate`, `parseAutopinCompleteParams`).
- Native sender / receiver: `lib/features/settings/screens/blox_pairing_screen.dart`,
`lib/core/services/deep_link_service.dart`.
- Web receiver: `lib/web/services/web_autopin_return*.dart`, `lib/web/screens/web_blox_pairing_screen.dart`.
- Forwarder: `site/autopin-complete/index.html`.
Loading