fetchAndApplyKbcPatch method

Future<KbcFetchResult> fetchAndApplyKbcPatch({
  1. required KbcLoaderFn loader,
  2. String? endpoint,
  3. String? apiKey,
  4. String? appVersion,
  5. String? engineVersion,
  6. String? distinctId,
  7. String? platform,
  8. String? currentLabel,
  9. bool persistToDisk = true,
  10. Duration timeout = const Duration(seconds: 30),
})

η v1: in-app fetch + apply for iOS Path C.

Replaces the manual xcrun devicectl device copy to workflow with a single in-app call. Hits the Sankofa server's /api/deploy/check endpoint, downloads the latest matching envelope, verifies sha-256, persists to <Documents>/sankofa-deploy/patches/active/patch.skdp, and applies via loader.

The canonical wiring from a host app:

import 'package:dynamic_modules/dynamic_modules.dart';

final result = await Sankofa.deploy!.fetchAndApplyKbcPatch(
  endpoint: 'http://172.20.10.8:8080',  // Mac's LAN IP in dev
  apiKey: '<test or live SDK key>',
  appVersion: '1.0.0',
  engineVersion: '3.41.9+sankofa-1',
  distinctId: await getOrCreateDistinctId(),
  loader: loadModuleFromBytes,
);
if (result.hasUpdate) {
  print('patched: ${result.applied?.returnValue}');
}

Throws KbcFetchException on any HTTP / sha-mismatch / apply failure. The exception message names the most likely cause.

Implementation

Future<KbcFetchResult> fetchAndApplyKbcPatch({
  required KbcLoaderFn loader,
  String? endpoint,
  String? apiKey,
  String? appVersion,
  String? engineVersion,
  String? distinctId,
  String? platform,
  String? currentLabel,
  bool persistToDisk = true,
  Duration timeout = const Duration(seconds: 30),
}) async {
  // Path C is pure Dart — see applyKbcPatchFromBytes above.
  // Prefix-call to avoid infinite recursion on the instance method.
  final banned = await getBannedKbcLabels();
  final resolvedAppVersion = _resolveAppVersion(appVersion);
  final resolvedPlatform = platform ?? _effectivePlatform;
  KbcFetchResult result;
  try {
    result = await kbc_fetch.fetchAndApplyKbcPatch(
      endpoint: _resolveEndpoint(endpoint),
      apiKey: _resolveApiKey(apiKey),
      appVersion: resolvedAppVersion,
      engineVersion: _resolveEngineVersion(engineVersion),
      distinctId: _resolveDistinctId(distinctId),
      loader: loader,
      platform: resolvedPlatform,
      currentLabel: currentLabel,
      persistToDisk: persistToDisk,
      timeout: timeout,
      signingPubkeysB64: _effectiveSigningPubkeys,
      bannedLabels: banned.isEmpty ? null : banned,
    );
  } catch (err, st) {
    // Telemetry fire-and-forget. Don't await — preserve the original
    // throw timing for the host's UI feedback.
    final stack = err is KbcApplyException
        ? _formatKbcStackTrace(err.causeStackTrace ?? st)
        : _formatKbcStackTrace(st);
    _reportKbcEvent(
      eventType: 'kbc_apply_failed',
      bundleLabel: currentLabel,
      appVersion: resolvedAppVersion,
      platform: resolvedPlatform,
      errorMessage: err.toString(),
      extra: {
        'phase': 'fetch_apply',
        'cause_class': err.runtimeType.toString(),
        if (err is KbcApplyException && err.cause != null)
          'inner_cause': err.cause.toString(),
        if (err is KbcApplyException && err.cause != null)
          'inner_cause_class': err.cause.runtimeType.toString(),
        if (stack != null) 'stack_trace': stack,
      },
    );
    rethrow;
  }
  if (result.hasUpdate && result.applied != null) {
    _reportKbcEvent(
      eventType: 'kbc_apply_success',
      releaseId: result.releaseId,
      bundleLabel: result.label,
      appVersion: resolvedAppVersion,
      platform: resolvedPlatform,
    );
  } else if (result.reason == 'label_banned_locally') {
    // Surface the skip so dashboards know "this device knows label X
    // is bad and is refusing to re-fetch it". Server-side telemetry
    // can then prompt the customer to disable the release globally.
    _reportKbcEvent(
      eventType: 'kbc_apply_skipped_label_banned',
      releaseId: result.releaseId,
      bundleLabel: result.label,
      appVersion: resolvedAppVersion,
      platform: resolvedPlatform,
    );
  }
  return result;
}