diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e3a0a..77ab233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.1 + +- Support app_links 7.2.1; require Flutter 3.44 and Dart 3.12. +- Document wallet approval, transport, account adapters and request lifecycle APIs. +- Retain the CORE License, including its non-OSI status. + ## 0.1.0 Initial Connect v1 implementation, shared conformance vectors, security tests, examples and release automation. diff --git a/README.md b/README.md index be56d8c..adccb39 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Flutter wallet library for Connect v1. Handles full `connect://` links and QR te ```yaml dependencies: - flutter_connect: ^0.1.0 + flutter_connect: ^0.1.1 ``` ```dart @@ -76,3 +76,5 @@ node scripts/flutter-interop.mjs Shared vectors assert identical Dart/TypeScript messages. Dart Ed25519 signatures and an OpenSSL Ed448 host bridge also complete real Better Auth desktop sessions. The OpenSSL fixture requires OpenSSL 3 on the test host; it is not a mobile signing implementation. The standalone suite skips live-server tests unless the integration runner supplies its test endpoint. See [release setup](doc/RELEASE.md). Licensed under the [CORE License](LICENSE). + +This package is distributed under the [CORE License](LICENSE). It is not an OSI-approved license. diff --git a/doc/CHAINS.md b/doc/CHAINS.md index e440025..515a01d 100644 --- a/doc/CHAINS.md +++ b/doc/CHAINS.md @@ -28,7 +28,7 @@ Signing method availability depends on the host wallet. These are protocol adapt ## TypeScript wallet ```ts -import { chains, walletAccount } from 'connect.js'; +import { chains, walletAccount } from 'connect-protocol'; const account = walletAccount(chains.solana, address, async bytes => { const result = await wallet.signMessage(bytes); @@ -76,7 +76,7 @@ All profiles prove possession of the key associated with the selected address. T Cardano enforces protected EdDSA (`-8` inside legacy CIP-8 COSE, Ed25519 curve 6 and OKP key type 1), the exact protected address, the payload and a matching Blake2b-224 key credential. The Connect algorithm remains the fully specified Ed25519 identifier `-19`. Attached unhashed and Blake2b-224-hashed payloads are supported. Protected headers must be exactly algorithm and address; unprotected headers exactly `hashed`; COSE_Key exactly kty, alg, crv and x. Unknown/critical extensions, duplicate map keys, detached payloads, pointers, scripts and Byron addresses are rejected. -Monero verification lazily loads monero-project WASM through monero-ts, operates offline, validates the address checksum/network, and accepts only modern spend-key SigV2 proofs. Each verification closes its temporary in-memory wallet; it does not persist a wallet or contact a daemon. This is heavier than the other verifiers and requires WebAssembly. connect.js ships browser WASM chunks; better-connect owns the Node WASM loader and bundles its patched Monero dependency. The Bitcoin legacy signing library is used only in better-connect tests as an independent signing implementation; production recovery uses noble. Remaining low-severity elliptic audit notices are limited to that development dependency. +Monero verification lazily loads monero-project WASM through monero-ts, operates offline, validates the address checksum/network, and accepts only modern spend-key SigV2 proofs. Each verification closes its temporary in-memory wallet; it does not persist a wallet or contact a daemon. This is heavier than the other verifiers and requires WebAssembly. connect-protocol ships browser WASM chunks; better-connect owns the Node WASM loader and bundles its patched Monero dependency. The Bitcoin legacy signing library is used only in better-connect tests as an independent signing implementation; production recovery uses noble. Remaining low-severity elliptic audit notices are limited to that development dependency. ## Standards and references @@ -88,4 +88,4 @@ Monero verification lazily loads monero-project WASM through monero-ts, operates - [Litecoin signed messages](https://github.com/litecoin-project/litecoin/blob/master/src/util/message.cpp), [Zcash message magic](https://github.com/zcash/zcash/blob/master/src/main.cpp), [Zcash address versions and genesis](https://github.com/zcash/zcash/blob/master/src/chainparams.cpp). - [CAIP namespaces](https://namespaces.chainagnostic.org/), [Base chain ID](https://docs.base.org/base-chain/api-reference/ethereum-json-rpc-api/eth_chainId), [Polygon chain ID](https://docs.polygon.technology/wallets/wallet-operations), [BNB configuration](https://docs.bnbchain.org/bnb-smart-chain/developers/wallet-configuration/). -The main `connect.js` import provides browser-only challenge creation and verification. `connect.js/verification` exposes signature profiles, and `connect.js/wallet` exposes wallet discovery and adapters. better-connect owns its backend engine, HTTP endpoints and session storage. connect.js has no server or dapp entry point. +The main `connect-protocol` import provides browser-only challenge creation and verification. `connect-protocol/verification` exposes signature profiles, and `connect-protocol/wallet` exposes wallet discovery and adapters. better-connect owns its backend engine, HTTP endpoints and session storage. connect-protocol has no server or dapp entry point. diff --git a/doc/IMPLEMENTATION.md b/doc/IMPLEMENTATION.md index d683e63..4e7d07e 100644 --- a/doc/IMPLEMENTATION.md +++ b/doc/IMPLEMENTATION.md @@ -1,6 +1,6 @@ # Implementation plan -1. Define Connect v1, strict schemas, canonical messages and shared vectors in connect.js. +1. Define Connect v1, strict schemas, canonical messages and shared vectors in connect-protocol. 2. Build profile verification and atomic request transitions, then Better Auth endpoints and normal session cookies. 3. Port wallet behavior to Dart, including lifecycle, transport and explicit approval. 4. Exercise cryptographic substitution attacks, concurrency, cross-language bytes and real Better Auth sessions. diff --git a/doc/VALIDATION.md b/doc/VALIDATION.md index a70098b..d1476cb 100644 --- a/doc/VALIDATION.md +++ b/doc/VALIDATION.md @@ -1,12 +1,19 @@ # Implementation validation — 2026-09-07 -Implemented locally in flutter_connect, connect.js and better-connect. No commits, pushes, tags or registry publications were made in the target repositories. +## 0.1.1 validation + +Local pana 0.23.18 reports 150/160 points. Documentation scores 20/20 with 64 of 161 public API elements documented (39.8%); dependency support scores 40/40, including latest-version compatibility and lower-bound analysis. The remaining 10 points require an OSI-approved license; CORE License is intentionally retained unchanged. + +The package now requires Flutter 3.44 and Dart 3.12 to support app_links 7.2.1. Static analysis, Flutter tests, and Android/iOS simulator builds pass. Registry scores will update only after this version is published and analyzed by pub.dev. + + +Implemented locally in flutter_connect, connect-protocol and better-connect. No commits, pushes, tags or registry publications were made in the target repositories. ## Results | Check | Result | | --- | --- | -| connect.js browser API/unit/conformance/lifecycle suite | 39 passed | +| connect-protocol browser API/unit/conformance/lifecycle suite | 39 passed | | Chromium browser and installed-package verification | All 17 proof fixtures, fresh Core flow, lifecycle checks and static example passed | | better-connect cryptographic/security/SQLite/client suite | 54 passed | | Flutter standalone suite | 27 passed; the 17 live-server cases run separately below | @@ -30,7 +37,7 @@ Local toolchain: Node 24.2.0, Flutter 3.47.2 / Dart 3.13.2, OpenSSL 3.6.4, Bette ## Delivered architecture and support -connect.js owns the browser-only API, strict v1 schemas, canonicalization, wallet adapters and local signature verification. It has no server, HTTP dapp client or request store. better-connect owns the backend request engine, Node Monero loading, persistent adapter storage, HTTP endpoints and Better Auth session cookies. Flutter ports the wallet protocol to Dart and adds app_links lifecycle integration. Shared vectors verify canonical bytes across all three packages. +connect-protocol owns the browser-only API, strict v1 schemas, canonicalization, wallet adapters and local signature verification. It has no server, HTTP dapp client or request store. better-connect owns the backend request engine, Node Monero loading, persistent adapter storage, HTTP endpoints and Better Auth session cookies. Flutter ports the wallet protocol to Dart and adds app_links lifecycle integration. Shared vectors verify canonical bytes across all three packages. Named presets and built-in verification cover Core Blockchain, Ethereum, Polygon, Base, Bitcoin, Solana, BNB Smart Chain, TRON, Monero, Stellar, Litecoin, XRP, Zcash and Cardano, plus alternate Bitcoin legacy and XRP Ed25519 methods. Raw Ed25519 remains supported. [CHAINS.md](CHAINS.md) specifies address limits and encoding rules. Tests cover every chain through real signature verification, unrelated-key substitution, message-field changes, network/algorithm mismatches and one-time redemption. Additional checks use the official Core and Stellar vectors, independent Emurgo CIP-8 signing, Cardano protected-header and credential tampering, and rejection of Monero view-key authentication. diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fda9c3b..dbe0afa 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,9 +1,9 @@ name: connect_wallet_example description: Connect wallet reference application using an ephemeral Ed25519 test account. publish_to: none -version: 0.1.0+1 +version: 0.1.1+2 environment: - sdk: '>=3.9.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: flutter: sdk: flutter diff --git a/lib/flutter_connect.dart b/lib/flutter_connect.dart index 0536e4d..741ca73 100644 --- a/lib/flutter_connect.dart +++ b/lib/flutter_connect.dart @@ -1,3 +1,8 @@ +/// Wallet-side Connect authentication for dapps and optional Better Auth portals. +/// +/// Resolve a full connect:// handoff with [ConnectClient], show its origin and +/// accounts, then call [ConnectRequest.approve] only after user confirmation. +/// The portal must verify wallet ownership before establishing a session. library; export 'src/protocol.dart'; diff --git a/lib/src/chains.dart b/lib/src/chains.dart index 91f0ab6..ccb53ca 100644 --- a/lib/src/chains.dart +++ b/lib/src/chains.dart @@ -13,6 +13,8 @@ class ConnectChain { signatureEncoding; final String? publicKeyEncoding; final int? alg; + + /// Defines a chain preset without implementing or invoking a signer. const ConnectChain({ required this.name, required this.namespace, @@ -23,6 +25,8 @@ class ConnectChain { required this.signatureEncoding, this.publicKeyEncoding, }); + + /// Creates an Ethereum-compatible chain using EIP-191 personal_sign proofs. factory ConnectChain.evm(String name, int chainId) { if (chainId <= 0 || chainId > 999999999999999) fail('invalidChain'); return ConnectChain( @@ -35,8 +39,12 @@ class ConnectChain { signatureEncoding: 'hex0x', ); } + + /// Policy entry a portal can use to accept accounts from this chain. SigningRequirement get requirement => SigningRequirement(namespace, reference, profile, alg); + + /// Binds an address claim to this chain; the portal must verify ownership. SigningSelection account(String address) => SigningSelection( account: WalletIdentity( namespace: namespace, @@ -48,6 +56,7 @@ class ConnectChain { ); } +/// Built-in chain presets defining identities, signing methods and proof encodings. abstract final class ConnectChains { static const core = ConnectChain( name: 'Core Blockchain', @@ -217,6 +226,7 @@ abstract final class ConnectChains { ]; } +/// Returns immutable, deduplicated requirements for the selected chains. List requirementsFor(Iterable chains) { final result = {}; for (final chain in chains) { @@ -228,15 +238,19 @@ List requirementsFor(Iterable chains) { /// Connect adapter around an existing wallet's message-signing operation. class ChainWalletAccount implements WalletAccount { + /// Chain and address offered to compatible Connect requests. @override final SigningSelection selection; final Future Function(Uint8List) _sign; + + /// Adapts a host signer; the callback must preserve the canonical bytes. ChainWalletAccount({ required ConnectChain chain, required String address, - required Future Function(Uint8List) sign, - }) : selection = chain.account(address), - _sign = sign; + required this._sign, + }) : selection = chain.account(address); + + /// Delegates canonical bytes to the host wallet without exporting its key. @override Future sign(Uint8List canonicalPayload) => _sign(canonicalPayload); diff --git a/lib/src/links.dart b/lib/src/links.dart index b7ad75b..804e740 100644 --- a/lib/src/links.dart +++ b/lib/src/links.dart @@ -5,11 +5,16 @@ import 'wallet.dart'; /// Create early after WidgetsFlutterBinding.ensureInitialized(). /// Host applications own navigation and display errors through [onError]. class ConnectLinks { + /// Client that receives raw links for validation and user approval. final ConnectClient client; + + /// Receives link delivery or request validation errors for safe UI handling. final void Function(Object error) onError; final AppLinks _links; StreamSubscription? _subscription; final Set _delivered = {}; + + /// Creates a lifecycle bridge; inject AppLinks when testing OS delivery. ConnectLinks({required this.client, required this.onError, AppLinks? links}) : _links = links ?? AppLinks(); Future _handle(String uri) async { @@ -21,6 +26,7 @@ class ConnectLinks { } } + /// Subscribes to warm links and handles the initial cold-start link once. Future start() async { if (_subscription != null) return; // String APIs preserve raw syntax; Uri.toString could normalize hostile input. @@ -32,6 +38,7 @@ class ConnectLinks { if (initial != null) await _handle(initial); } + /// Stops OS link delivery without disposing the underlying Connect client. Future dispose() async { await _subscription?.cancel(); _subscription = null; diff --git a/lib/src/wallet.dart b/lib/src/wallet.dart index 776c0f6..35dabfe 100644 --- a/lib/src/wallet.dart +++ b/lib/src/wallet.dart @@ -5,29 +5,51 @@ import 'package:http/http.dart' as http; import 'protocol.dart'; import 'handoff.dart'; +/// Signature data returned by a host wallet; never contains private keys. class WalletSignature { + /// Signature encoded as required by the selected signing profile. final String signature; + + /// Optional encoded public key required by profiles that cannot recover it. final String? publicKey; + + /// Creates signature data using the selected profile's wire encodings. const WalletSignature(this.signature, {this.publicKey}); } +/// A wallet account capable of signing canonical Connect messages. abstract interface class WalletAccount { + /// The account identity, proof profile and algorithm offered by this signer. SigningSelection get selection; + + /// Signs the exact canonical bytes after explicit user approval. Future sign(Uint8List canonicalPayload); } +/// Supplies accounts available to the host wallet without signing automatically. abstract interface class WalletAccountProvider { + /// Returns available signers; the client filters them against request requirements. Future> getAccounts(); } +/// Optional challenge lookup and proof submission for short request URIs. +/// Full offline handoffs do not require a network adapter. abstract interface class ConnectNetwork { + /// Retrieves a challenge matching the target origin and request identifier. Future challenge(ConnectTarget target); + + /// Submits a signed proof and returns the transport's approval result. Future approve(ConnectTarget target, Map proof); } +/// Optional Better Auth HTTP transport with bounded responses and no redirects. class HttpsNetwork implements ConnectNetwork { final http.Client _client; + + /// Creates a transport, optionally using a caller-provided HTTP client. HttpsNetwork({http.Client? client}) : _client = client ?? http.Client(); + + /// Closes the HTTP client; call when this transport is no longer needed. void close() => _client.close(); Future _request( ConnectTarget target, @@ -78,44 +100,74 @@ class HttpsNetwork implements ConnectNetwork { } @override + /// Retrieves a challenge matching the target origin and request identifier. Future challenge(ConnectTarget target) => _request(target, 'challenge'); @override + /// Submits a signed proof and returns the transport's approval result. Future approve(ConnectTarget target, Map proof) => _request(target, 'approve', proof); } +/// Wallet-side request progress; portal authentication is a separate operation. enum ConnectState { + /// Waiting for the user to select an account and approve signing. awaitingUserApproval, + + /// The host wallet is signing the canonical message. signing, + + /// The proof is being encrypted for return or submitted through the transport. submitting, + + /// The configured remote transport acknowledged approval. approved, + + /// An encrypted offline response is ready; the portal has not yet verified it. responseReady, + + /// The user rejected the request locally without signing. rejected, + + /// The request was cancelled before proof submission. cancelled, + + /// The request expired before signing or submission completed. expired, + + /// Signing, validation or transport failed. failed, } +/// An account offered by the wallet that satisfies this request's policy. class CompatibleAccount { + /// The identity and signing profile displayed for user selection. final SigningSelection selection; final WalletAccount _signer; CompatibleAccount._(this.selection, this._signer); } +/// A single request with explicit approval and immutable compatible accounts. class ConnectRequest { + /// Validated challenge whose canonical fields must be shown before signing. final ConnectChallenge challenge; + + /// Immutable accounts accepted by the challenge's signing requirements. final List compatibleAccounts; final ConnectNetwork _network; String? _response; /// Encrypted response to return by BLE, response QR, or another host channel. String? get response => _response; + + /// Pairing ticket for an offline request, or null for a network lookup. ConnectHandoff? get handoff => _network is _HandoffNetwork ? _network.ticket : null; final DateTime Function() _now; final void Function(ConnectRequest) _changed; ConnectState _state = ConnectState.awaitingUserApproval; + + /// Current wallet-side lifecycle state. ConnectState get state => _state; ConnectRequest._( this.challenge, @@ -137,6 +189,7 @@ class ConnectRequest { if (_state != ConnectState.awaitingUserApproval) fail('invalidState'); } + /// Cancels a pending request or discards a signature still being produced. void cancel() { if (_state != ConnectState.awaitingUserApproval && _state != ConnectState.signing) { @@ -151,6 +204,8 @@ class ConnectRequest { _set(ConnectState.rejected); } + /// Signs once using an offered account after explicit user confirmation. + /// Offline success exposes [response]; the portal must still verify the proof. Future approve(CompatibleAccount account) async { _pending(); if (!compatibleAccounts.contains(account)) fail('noCompatibleAccount'); @@ -196,14 +251,26 @@ class ConnectRequest { } } +/// Resolves Connect links, filters accounts and emits requests for trusted UI. class ConnectClient { + /// Host account source; signing remains inside its wallet implementations. final WalletAccountProvider accountProvider; + + /// Optional network adapter for short links without an embedded challenge. final ConnectNetwork? network; + + /// Parser for short request links; full handoffs use their strict parser. final ConnectTransport transport; + + /// Clock used for challenge validation; injectable for deterministic tests. final DateTime Function() now; final Set _handled = {}; final _requests = StreamController.broadcast(); + + /// Broadcasts resolved requests and subsequent lifecycle transitions. Stream get requests => _requests.stream; + + /// Creates a client; no network is required for full offline handoffs. ConnectClient({ required this.accountProvider, this.network, @@ -211,7 +278,12 @@ class ConnectClient { DateTime Function()? now, }) : transport = transport ?? UriTransport(), now = now ?? DateTime.now; + + /// Handles a raw OS link or scanned QR string through [resolve]. Future handleUri(String input) => resolve(input); + + /// Validates a full handoff or looks up a short URI using [network]. + /// Never signs automatically; duplicate resolved requests are rejected. Future resolve(String input) { if (input.contains('#')) { final handoff = ConnectHandoff.parse(input, now: now); @@ -220,6 +292,7 @@ class ConnectClient { return resolveTarget(transport.parse(input)); } + /// Looks up an already parsed target using the configured network adapter. Future resolveTarget(ConnectTarget target) { final adapter = network; if (adapter == null) fail('transportUnavailable'); @@ -258,6 +331,7 @@ class ConnectClient { } } + /// Closes the request event stream and prevents further resolution. Future dispose() => _requests.close(); } @@ -265,9 +339,11 @@ class _HandoffNetwork implements ConnectNetwork { final ConnectHandoff ticket; _HandoffNetwork(this.ticket); @override + /// Retrieves a challenge matching the target origin and request identifier. Future challenge(ConnectTarget target) async => ticket.challenge.toJson(); @override + /// Submits a signed proof and returns the transport's approval result. Future approve( ConnectTarget target, Map proof, diff --git a/pubspec.yaml b/pubspec.yaml index 4e57061..5a4785a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,17 @@ name: flutter_connect description: Wallet-side Connect protocol with strict transport validation, explicit approval, and multi-chain signing adapters. -version: 0.1.0 +version: 0.1.1 homepage: https://github.com/bchainhub/flutter_connect repository: https://github.com/bchainhub/flutter_connect issue_tracker: https://github.com/bchainhub/flutter_connect/issues environment: - sdk: '>=3.9.0 <4.0.0' - flutter: '>=3.35.0' + sdk: '>=3.12.0 <4.0.0' + flutter: '>=3.44.0' dependencies: flutter: sdk: flutter http: ^1.5.0 - app_links: ^6.4.1 + app_links: ^7.2.1 bluetooth_low_energy: ^6.2.1 cryptography: ^2.9.0 dev_dependencies: