dynamic_sdk
A thin, per-platform host around the generated Dart SDK sources. The generated code is platform- agnostic business logic (auth flows, WaaS orchestration, session signatures); this package supplies the six injected platform services the core needs — HTTP, secure storage, device/session signing, OAuth, and the WaaS engine — with a native implementation per platform, from one Dart codebase.
WaaS runs Dynamic's waas-v1 MPC page (the private key never exists whole:
2-of-2 MPC runs inside that page). We do not re-implement it — each
platform just re-hosts it with a thin transport and speaks its message protocol.
Verified end-to-end (login → create embedded wallet → sign): Web (Chrome)
and macOS (desktop). Android/iOS reuse the hand-written native/ layer.
Big picture
packages/spec (the DSL — single source of truth for ALL languages)
│ pnpm generate
▼
packages/dynamic_sdk (package:dynamic_sdk; generated API,
platform services, and native plugin)
▼
examples/flutter-app (idiomatic-Flutter demo; stores the
client it gets from DynamicSdk.createDynamicClient)
The core declares interfaces (HttpService, SecureStorage, DeviceSigner,
SessionSigner, OAuthBrowser, WaasEngine). This package constructs a
DynamicClient wired with the right implementation for the current platform and
returns it — the app owns the client (idiomatic Dart; the demo threads it
through constructors, no globals/singletons).
Files (lib/)
| File | Role |
|---|---|
dynamic_sdk.dart |
Public entry point: DynamicSdk.createDynamicClient(...). |
src/waas_bridge.dart |
Transport-agnostic WaasBridge implements WaasEngine — pure Dart, the full host↔page protocol (envelope, iframe-ready handshake, request → __ack → __resolve/__reject, reverse handlers). Depends on injected WaasTransport + WaasKeyShareStore abstractions. Shared by every platform. |
src/waas_web.dart |
Web transport: real hidden <iframe> + native window.postMessage/message (structured-clone objects). WebKeyShareStore = IndexedDB. |
src/waas_desktop.dart |
Desktop transport: headless flutter_inappwebview webview + injected postMessage shim (JSON-string wire). |
src/services_shared.dart |
Shared web+desktop: HttpxService (package:http) + EcdsaSigner (P-256 via PointyCastle, pure Dart). |
src/signer_web.dart |
Web signer: non-extractable P-256 CryptoKey in IndexedDB via WebCrypto (private key never leaves the browser — mirrors the JS SDK keychain). |
src/p256.dart |
Shared pure-Dart low-S canonicalization (canonicalLowS) used by both the web and desktop signers. |
src/platform_web.dart |
Web composition: WebSecureStorage (localStorage), WebOAuthBrowser (popup), buildClient(...). |
src/platform_desktop.dart |
Desktop composition: DesktopSecureStorage/DesktopKeyShareStore (flutter_secure_storage), DesktopOAuthBrowser, buildClient(...). |
src/platform_io.dart |
Mobile composition: Native* services over a MethodChannel to the native/ layer; delegates to platform_desktop when not Android/iOS. |
Native plugin (android/, ios/, macos/): the mobile method-channel handlers
live in android/ + ios/ (source-included from native/android-core-shared,
native/ios-core-shared, and their -waas-shared counterparts).
macos/ is a method-channel-free plugin whose only job is a keyboard fix
(see macOS notes).
The platform split — one line
// dynamic_sdk.dart
import 'src/platform_io.dart'
if (dart.library.js_interop) 'src/platform_web.dart' as platform;
- Web (
dart.library.js_interopavailable) →platform_web.dart. - Everything else →
platform_io.dart, which itself picks mobile native (Android/iOS) or delegates toplatform_desktop(macOS/Windows/Linux).
WaaS is its own opt-in package (dynamic_sdk_waas, same split as
kotlin-waas/swift-waas/Dynamic.Sdk.MAUI.Waas) — createDemoClient calls
client.useWaas() explicitly, then adds EvmExtension.
Injected services, per platform
| Service | Web | Desktop (macOS/Win/Linux) | Mobile (Android/iOS) |
|---|---|---|---|
| HTTP | HttpxService (package:http) |
HttpxService (package:http) |
Native* MethodChannel → native/ URLSession/HttpURLConnection |
| Secure storage | WebSecureStorage (localStorage) |
flutter_secure_storage → Keychain / DPAPI / libsecret |
native/ DataStore+Tink (Android) / Keychain (iOS) |
| Device / session signer | WebCrypto non-extractable key (IndexedDB) | PointyCastle P-256, key in secure storage | Hardware: AndroidKeyStore / Secure Enclave |
| OAuth browser | popup + poll | flutter_inappwebview in-app browser |
Custom Tabs / ASWebAuthenticationSession |
WaaS transport/key-share store moved to dynamic_sdk_waas (opt-in) — see that
package for its own per-platform breakdown (iframe/IndexedDB on web, headless
flutter_inappwebview on desktop, native WebView via method channel on mobile).
Same WaasBridge protocol drives all transports; only the transport (how bytes
cross to the MPC page) differs.
Signing — why this is subtle
Privileged WaaS calls present a signedSessionId proof:
sessionSignature/nonceSignature/nonce, each an ECDSA P-256/SHA-256 signature by
the session key. Three rules every signer here obeys — encoded once in the
DSL service contract (packages/spec/src/services.ts) and satisfied per
implementation:
- Compressed SEC1 public key hex, IEEE-P1363
r||ssignature hex — the wire formats the API expects. - Canonical low-S (
s <= n/2). WebCrypto and PointyCastle (and raw Keystore/Enclave DER→P1363) do not guarantee low-S; a high-S signature is rejected by the key-share relay asinvalid_nonce_signature. All signers flips -> n - s(src/p256.dartfor Dart;BigIntegerin Kotlin/C#, byte math in Swift). - One stable key per session. Key load/generate is memoized so a racing
publicKeyHex()/sign()can't register one pubkey in the JWT then sign with a different persisted key (alsoinvalid_nonce_signature).
Session-change rebuild (the waas-v1 page binds its iframe to one user) is
generic orchestration, so it lives in the DSL flows, not here: the generated
WaasClient tracks a private waasBoundSessionId and calls engine.destroy()
before re-initialize() when the session changes.
Running it
Web
cd examples/flutter-app
flutter run -d chrome --web-port 8099
- The environment must allow the origin (CORS). Add it with the Dynamic console
CLI:
dyn origins create --origin http://localhost:8099. - For a clean WaaS test use an incognito window (empty IndexedDB/localStorage) so login mints a fresh session bound to the current signer key.
macOS
open examples/flutter-app/macos/Runner.xcworkspace # then Run (scheme: Runner)
# or: cd examples/flutter-app && flutter run -d macos
- Entitlements (
macos/Runner/*.entitlements) grantnetwork.client(HTTPS). flutter_secure_storageuses the data-protection Keychain, which needs thekeychain-access-groupsentitlement + a development signing team. Run from Xcode with your account so automatic signing generates the provisioning profile. (For an unsigned CLI run you can instead setMacOsOptions(useDataProtectionKeyChain: false)to use the file Keychain.)- Keyboard fix ships in the SDK:
macos/Classes/DynamicSdkFlutterPlugin.swiftreclaims the Flutter view's first responder after registration — otherwiseflutter_inappwebview's native NSViews steal the key-event chain and TextFields focus/click but won't type. Consumers get this automatically; noMainFlutterWindow.swiftedits.
Android / iOS
cd examples/flutter-app && flutter run # with an emulator/device selected
Uses the hand-written native/android-core-shared + native/ios-core-shared layer over a method
channel (hardware-backed signing + secure storage); WaaS (if you call
dynamic_sdk_waas's client.useWaas()) rides the same channel.
Windows / Linux
Same desktop code path (platform_desktop) — PointyCastle signer +
flutter_secure_storage (DPAPI/libsecret). Windows is build-compatible; on
Linux, dynamic_sdk_waas has no flutter_inappwebview backend, so a WaaS
transport must be injected via client.useWaas(waasTransportFactory: ...).
Dependencies of note
pointycastleis pinned to^3.9.1(not 4.0.0):web3dart(the EVM extension) caps it at^3.x, and 3.9.1 has the same P-256 API we use.package:cryptographyis not used for ECDSA — it ships no pure-Dart ECDSA (throwsUnimplementedError), so it can't sign on desktop; PointyCastle can.