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_interop available) → platform_web.dart.
  • Everything else → platform_io.dart, which itself picks mobile native (Android/iOS) or delegates to platform_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:

  1. Compressed SEC1 public key hex, IEEE-P1363 r||s signature hex — the wire formats the API expects.
  2. 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 as invalid_nonce_signature. All signers flip s -> n - s (src/p256.dart for Dart; BigInteger in Kotlin/C#, byte math in Swift).
  3. 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 (also invalid_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) grant network.client (HTTPS).
  • flutter_secure_storage uses the data-protection Keychain, which needs the keychain-access-groups entitlement + 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 set MacOsOptions(useDataProtectionKeyChain: false) to use the file Keychain.)
  • Keyboard fix ships in the SDK: macos/Classes/DynamicSdkFlutterPlugin.swift reclaims the Flutter view's first responder after registration — otherwise flutter_inappwebview's native NSViews steal the key-event chain and TextFields focus/click but won't type. Consumers get this automatically; no MainFlutterWindow.swift edits.

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

  • pointycastle is 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:cryptography is not used for ECDSA — it ships no pure-Dart ECDSA (throws UnimplementedError), so it can't sign on desktop; PointyCastle can.