@_nazmiforreal/flutter-ota 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -1
  2. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
  3. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
  4. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
  5. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
  6. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
  7. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
  8. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
  9. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
  13. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +96 -58
  14. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
  15. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
  16. package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
  17. package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
  18. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +10 -2
  19. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -1
  20. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
  21. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
  22. package/dart-src/packages/core/lib/src/app_update_info.dart +71 -1
  23. package/dart-src/packages/core/lib/src/bundle.dart +62 -17
  24. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
  25. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
  26. package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
  27. package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
  28. package/dart-src/packages/core/lib/src/platform.dart +8 -0
  29. package/dart-src/packages/core/lib/src/semver.dart +0 -3
  30. package/dart-src/packages/core/lib/src/status.dart +20 -1
  31. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  32. package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
  33. package/dart-src/packages/core/lib/src/uuid.dart +11 -0
  34. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
  35. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
  36. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
  37. package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
  38. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +3 -0
  39. package/package.json +1 -1
  40. package/bin/flutter-ota-linux-x64 +0 -0
  41. package/bin/flutter-ota.js +0 -36
  42. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -460
  43. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
  44. package/dart-src/packages/cli-tools/pubspec.lock +0 -590
@@ -232,8 +232,6 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
232
232
  String? url,
233
233
  String? serviceRoleKey,
234
234
  String? anonKey,
235
- String? bucket,
236
- String? basePath,
237
235
  SupabaseClientFactory? clientFactory,
238
236
  }) {
239
237
  final env = Platform.environment;
@@ -247,8 +245,8 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
247
245
 
248
246
  if (resolvedUrl == null || resolvedUrl.isEmpty) {
249
247
  throw StateError(
250
- 'Supabase URL is required. Set it via --url, SUPABASE_URL, or '
251
- '`flutter_ota_kit config set supabase.url <url>`.',
248
+ 'Supabase URL is required. Set it via SUPABASE_URL env var, '
249
+ 'or `flutter-ota config set supabase.url <url>`.',
252
250
  );
253
251
  }
254
252
 
@@ -275,8 +273,6 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
275
273
  url: url,
276
274
  serviceRoleKey: serviceRoleKey,
277
275
  anonKey: anonKey,
278
- bucket: bucket,
279
- basePath: basePath,
280
276
  clientFactory: clientFactory,
281
277
  );
282
278
  return SupabaseStorageConfig(
@@ -284,12 +280,10 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
284
280
  supabaseServiceRoleKey: db.supabaseServiceRoleKey,
285
281
  supabaseAnonKey: db.supabaseAnonKey,
286
282
  clientFactory: db.clientFactory,
287
- bucketName: db.supabaseServiceRoleKey != null || db.supabaseAnonKey != null
288
- ? (bucket ??
289
- Platform.environment['SUPABASE_BUCKET'] ??
290
- config.supabase.bucket ??
291
- 'bundles')
292
- : (bucket ?? config.supabase.bucket ?? 'bundles'),
283
+ bucketName: bucket ??
284
+ Platform.environment['SUPABASE_BUCKET'] ??
285
+ config.supabase.bucket ??
286
+ 'bundles',
293
287
  basePath:
294
288
  basePath ??
295
289
  Platform.environment['SUPABASE_BASE_PATH'] ??
@@ -387,8 +381,8 @@ PostgresConfig resolvePostgresDatabaseConfig(
387
381
 
388
382
  if (resolvedHost == null || resolvedHost.isEmpty) {
389
383
  throw StateError(
390
- 'Postgres host is required. Set --pg-host, POSTGRES_HOST, or '
391
- '`flutter_ota_kit config set postgres.host <host>`.',
384
+ 'Postgres host is required. Set POSTGRES_HOST env var, '
385
+ 'or `flutter-ota config set postgres.host <host>`.',
392
386
  );
393
387
  }
394
388
 
@@ -765,15 +759,16 @@ PocketBaseDatabaseConfig resolvePocketBaseDatabaseConfig(
765
759
 
766
760
  if (resolvedUrl == null || resolvedUrl.isEmpty) {
767
761
  throw StateError(
768
- 'PocketBase URL is required. Set --pb-url, POCKETBASE_URL, or '
769
- '`flutter_ota_kit config set pocketbase.url <url>`.',
762
+ 'PocketBase URL is required. Set POCKETBASE_URL env var, '
763
+ 'or `flutter-ota config set pocketbase.url <url>`.',
770
764
  );
771
765
  }
772
766
  if (resolvedEmail == null || resolvedPassword == null) {
773
767
  throw StateError(
774
- 'PocketBase admin credentials are required. Set --pb-email, '
775
- '--pb-password, POCKETBASE_ADMIN_EMAIL/PASSWORD, or '
776
- '`flutter_ota_kit config set pocketbase.*`.',
768
+ 'PocketBase admin credentials are required. Set '
769
+ 'POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD env vars, or '
770
+ '`flutter-ota config set pocketbase.adminEmail/<email> '
771
+ 'pocketbase.adminPassword/<password>`.',
777
772
  );
778
773
  }
779
774
 
@@ -62,7 +62,7 @@ class ListOptions {
62
62
  Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
63
63
  if (opts.targetAppVersion != null && opts.fingerprintHash != null) {
64
64
  throw StateError(
65
- 'Use only one of target-app-version / fingerprint-hash (DB CHECK).',
65
+ 'Use only one of target-app-version / fingerprint-hash.',
66
66
  );
67
67
  }
68
68
  if (opts.targetAppVersion == null && opts.fingerprintHash == null) {
@@ -16,13 +16,17 @@ import 'dart:io';
16
16
  import 'package:path/path.dart' as p;
17
17
 
18
18
  class PocketBaseProcess {
19
- PocketBaseProcess._(this.process, this.dataDir, this.binaryPath);
19
+ PocketBaseProcess._(this.process, this.dataDir, this.binaryPath)
20
+ : _exitCodeCompleted = false {
21
+ process.exitCode.then((_) => _exitCodeCompleted = true);
22
+ }
20
23
 
21
24
  final Process process;
22
25
  final Directory dataDir;
23
26
  final File binaryPath;
27
+ bool _exitCodeCompleted;
24
28
 
25
- bool get isRunning => true;
29
+ bool get isRunning => !_exitCodeCompleted;
26
30
 
27
31
  int get pid => process.pid;
28
32
 
@@ -214,5 +218,9 @@ class PocketBaseProcessManager {
214
218
  .transform(utf8.decoder)
215
219
  .transform(const LineSplitter())
216
220
  .map((line) => '[stdout] $line');
221
+ yield* proc.errors
222
+ .transform(utf8.decoder)
223
+ .transform(const LineSplitter())
224
+ .map((line) => '[stderr] $line');
217
225
  }
218
226
  }
@@ -221,7 +221,6 @@ class PocketBaseSchemaInstaller {
221
221
  required this.url,
222
222
  required this.adminEmail,
223
223
  required this.adminPassword,
224
- http.Client? httpClient,
225
224
  this.bundlesCollection = 'bundles',
226
225
  this.channelsCollection = 'channels',
227
226
  this.auditLogCollection = 'audit_log',
@@ -292,7 +292,7 @@ class Steps {
292
292
  _echo(' ${dim('·')} $label');
293
293
  }
294
294
 
295
- /// Print boxed summary: "N applied · N skipped · N errors · TIME"
295
+ /// Print summary. Always inline no box for a single line of text.
296
296
  void summary() {
297
297
  _sw.stop();
298
298
  final ms = _sw.elapsedMilliseconds;
@@ -307,7 +307,7 @@ class Steps {
307
307
  }
308
308
  if (parts.isEmpty) parts.add('no steps');
309
309
  parts.add(time);
310
- box('summary', [parts.join(' · ')]);
310
+ _echo(' ${dim(parts.join(' · '))}');
311
311
  }
312
312
 
313
313
  /// Run a step that shows a spinner while working, then marks it
@@ -21,6 +21,8 @@ export 'src/update_bundle_params.dart';
21
21
  // Algorithms.
22
22
  export 'src/rollout.dart';
23
23
  export 'src/semver.dart';
24
+ export 'src/semver_range.dart' show satisfies;
25
+ export 'src/semver_version.dart' show SemVer;
24
26
  export 'src/uuid.dart';
25
27
 
26
28
  // Bundle artifact helpers.
@@ -1,18 +1,36 @@
1
1
  import 'changed_asset.dart' show ChangedAsset;
2
2
  import 'status.dart';
3
3
 
4
- /// hot-updater `UpdateInfo`database-layer update decision.
4
+ /// Database-layer update decision hot-updater `UpdateInfo`.
5
+ ///
6
+ /// This is the raw result from the database query before the storage
7
+ /// resolver converts URIs to download URLs. Not directly sent to devices.
5
8
  class UpdateInfo {
9
+ /// Bundle UUID.
6
10
  final String id;
11
+
12
+ /// Whether the device must install this update before proceeding.
7
13
  final bool shouldForceUpdate;
14
+
15
+ /// Optional message to display to the user during the update.
8
16
  final String? message;
17
+
18
+ /// Deployment status (`update` or `rollback`).
9
19
  final UpdateStatus status;
20
+
21
+ /// Protocol URI for the artifact in storage (e.g. `s3://bucket/key`).
10
22
  final String? storageUri;
23
+
24
+ /// MD5 hex of the artifact for verification.
11
25
  final String? fileHash;
26
+
27
+ /// Git commit hash of the build, if available.
12
28
  final String? gitCommitHash;
13
29
 
14
30
  /// Internal rollout metadata; never serialized to update-check clients.
15
31
  final int? rolloutCohortCount;
32
+
33
+ /// Cohort identifiers for targeted rollouts; never sent to devices.
16
34
  final List<String>? targetCohorts;
17
35
 
18
36
  const UpdateInfo({
@@ -30,20 +48,39 @@ class UpdateInfo {
30
48
 
31
49
  /// Device-facing payload for an available update —
32
50
  /// hot-updater `AppUpdateAvailableInfo`.
51
+ ///
52
+ /// This is the resolved update info sent to the device SDK after the
53
+ /// storage resolver converts URIs to signed download URLs.
33
54
  class AppUpdateAvailableInfo extends AppUpdateInfo {
55
+ @override
34
56
  final String id;
57
+
58
+ @override
35
59
  final bool shouldForceUpdate;
60
+
61
+ @override
36
62
  final String? message;
63
+
64
+ @override
37
65
  final UpdateStatus status;
66
+
67
+ /// Signed download URL for the artifact.
38
68
  final String? fileUrl;
69
+
70
+ /// MD5 hex of the artifact for download verification.
39
71
  final String? fileHash;
40
72
 
41
73
  /// Ed25519 (base64) signature over the artifact MD5 hex string. Consumed by
42
74
  /// the device SDK to verify the patch (`PatchInfo.signature`).
43
75
  final String? signature;
44
76
 
77
+ /// Signed URL for the asset manifest (if asset-based patch).
45
78
  final String? manifestUrl;
79
+
80
+ /// MD5 hex of the manifest file for verification.
46
81
  final String? manifestFileHash;
82
+
83
+ /// Map of asset path to diff descriptor for incremental asset updates.
47
84
  final Map<String, ChangedAsset>? changedAssets;
48
85
 
49
86
  const AppUpdateAvailableInfo({
@@ -94,19 +131,52 @@ class AppUpdateAvailableInfo extends AppUpdateInfo {
94
131
  }
95
132
 
96
133
  /// Device-facing "nothing to do" — hot-updater `AppUpToDateInfo`.
134
+ ///
135
+ /// Returned when the device is already on the latest bundle for its
136
+ /// channel/platform. All fields are empty/null defaults.
97
137
  class AppUpToDateInfo extends AppUpdateInfo {
98
138
  const AppUpToDateInfo();
99
139
 
140
+ @override
141
+ String get id => '';
142
+
143
+ @override
144
+ bool get shouldForceUpdate => false;
145
+
146
+ @override
147
+ String? get message => null;
148
+
149
+ @override
150
+ UpdateStatus get status => UpdateStatus.update;
151
+
100
152
  @override
101
153
  Map<String, dynamic> toJson() => {'status': 'UP_TO_DATE'};
102
154
  }
103
155
 
104
156
  /// Sealed union: hot-updater `AppUpdateInfo`.
157
+ ///
158
+ /// Pattern-match on [AppUpdateAvailableInfo] or [AppUpToDateInfo] to
159
+ /// handle the two possible outcomes of an update check.
105
160
  sealed class AppUpdateInfo {
106
161
  const AppUpdateInfo();
107
162
 
163
+ /// Bundle UUID (empty string for up-to-date).
164
+ String get id;
165
+
166
+ /// Whether the device must install this update.
167
+ bool get shouldForceUpdate;
168
+
169
+ /// Optional message to display during the update.
170
+ String? get message;
171
+
172
+ /// Deployment status.
173
+ UpdateStatus get status;
174
+
175
+ /// Serialize to JSON for device SDK consumption.
108
176
  Map<String, dynamic> toJson();
109
177
 
178
+ /// Deserialize from JSON. Returns [AppUpToDateInfo] if status is
179
+ /// `"UP_TO_DATE"`, otherwise [AppUpdateAvailableInfo].
110
180
  factory AppUpdateInfo.fromJson(Map<String, dynamic> j) {
111
181
  final raw = j['status'];
112
182
  if (raw == 'UP_TO_DATE') return const AppUpToDateInfo();
@@ -172,28 +172,73 @@ class Bundle {
172
172
  'enabled': enabled,
173
173
  'file_hash': fileHash,
174
174
  'storage_uri': storageUri,
175
- 'git_commit_hash': gitCommitHash,
176
- 'message': message,
177
175
  'channel': channel,
178
- 'target_app_version': targetAppVersion,
179
- 'fingerprint_hash': fingerprintHash,
180
- 'metadata': metadata?.toJson() ?? <String, dynamic>{},
181
- 'manifest_storage_uri': manifestStorageUri,
182
- 'manifest_file_hash': manifestFileHash,
183
- 'asset_base_storage_uri': assetBaseStorageUri,
184
- 'patches': patches?.map((p) => p.toJson()).toList(),
185
- 'patch_base_bundle_id': patchBaseBundleId,
186
- 'patch_base_file_hash': patchBaseFileHash,
187
- 'patch_file_hash': patchFileHashLegacy,
188
- 'patch_storage_uri': patchStorageUri,
189
- 'rollout_cohort_count': rolloutCohortCount,
190
- 'target_cohorts': targetCohorts,
176
+ if (gitCommitHash != null) 'git_commit_hash': gitCommitHash,
177
+ if (message != null) 'message': message,
178
+ if (targetAppVersion != null) 'target_app_version': targetAppVersion,
179
+ if (fingerprintHash != null) 'fingerprint_hash': fingerprintHash,
180
+ if (metadata != null) 'metadata': metadata!.toJson(),
181
+ if (manifestStorageUri != null)
182
+ 'manifest_storage_uri': manifestStorageUri,
183
+ if (manifestFileHash != null) 'manifest_file_hash': manifestFileHash,
184
+ if (assetBaseStorageUri != null)
185
+ 'asset_base_storage_uri': assetBaseStorageUri,
186
+ if (patches != null) 'patches': patches!.map((p) => p.toJson()).toList(),
187
+ if (patchBaseBundleId != null) 'patch_base_bundle_id': patchBaseBundleId,
188
+ if (patchBaseFileHash != null)
189
+ 'patch_base_file_hash': patchBaseFileHash,
190
+ if (patchFileHashLegacy != null) 'patch_file_hash': patchFileHashLegacy,
191
+ if (patchStorageUri != null) 'patch_storage_uri': patchStorageUri,
192
+ if (rolloutCohortCount != null)
193
+ 'rollout_cohort_count': rolloutCohortCount,
194
+ if (targetCohorts != null) 'target_cohorts': targetCohorts,
191
195
  };
192
196
 
193
197
  @override
194
198
  bool operator ==(Object o) =>
195
- o is Bundle && o.toJson().toString() == toJson().toString();
199
+ o is Bundle &&
200
+ o.id == id &&
201
+ o.platform == platform &&
202
+ o.shouldForceUpdate == shouldForceUpdate &&
203
+ o.enabled == enabled &&
204
+ o.fileHash == fileHash &&
205
+ o.storageUri == storageUri &&
206
+ o.channel == channel &&
207
+ o.gitCommitHash == gitCommitHash &&
208
+ o.message == message &&
209
+ o.targetAppVersion == targetAppVersion &&
210
+ o.fingerprintHash == fingerprintHash &&
211
+ o.metadata == metadata &&
212
+ o.manifestStorageUri == manifestStorageUri &&
213
+ o.manifestFileHash == manifestFileHash &&
214
+ o.assetBaseStorageUri == assetBaseStorageUri &&
215
+ o.patchBaseBundleId == patchBaseBundleId &&
216
+ o.patchBaseFileHash == patchBaseFileHash &&
217
+ o.patchFileHashLegacy == patchFileHashLegacy &&
218
+ o.patchStorageUri == patchStorageUri &&
219
+ o.rolloutCohortCount == rolloutCohortCount;
196
220
 
197
221
  @override
198
- int get hashCode => toJson().toString().hashCode;
222
+ int get hashCode => Object.hash(
223
+ id,
224
+ platform,
225
+ shouldForceUpdate,
226
+ enabled,
227
+ fileHash,
228
+ storageUri,
229
+ channel,
230
+ gitCommitHash,
231
+ message,
232
+ targetAppVersion,
233
+ fingerprintHash,
234
+ metadata,
235
+ manifestStorageUri,
236
+ manifestFileHash,
237
+ assetBaseStorageUri,
238
+ patchBaseBundleId,
239
+ patchBaseFileHash,
240
+ patchFileHashLegacy,
241
+ patchStorageUri,
242
+ rolloutCohortCount,
243
+ );
199
244
  }
@@ -7,9 +7,11 @@ library;
7
7
  import 'bundle.dart';
8
8
  import 'bundle_patch_artifact.dart';
9
9
 
10
- /// Identity — returns the metadata as-is.
11
- /// (TS version strips artifact-specific keys; Dart uses strong types so
12
- /// there is nothing to strip.)
10
+ /// Identity function — returns the metadata as-is.
11
+ ///
12
+ /// In the TypeScript version this strips artifact-specific keys from the
13
+ /// metadata map. The Dart version uses strong types so there is nothing
14
+ /// to strip; this function is kept for API compatibility.
13
15
  Map<String, Object?>? stripBundleArtifactMetadata(
14
16
  Map<String, Object?>? metadata,
15
17
  ) => metadata;
@@ -25,6 +27,8 @@ String? getAssetBaseStorageUri(Bundle bundle) => bundle.assetBaseStorageUri;
25
27
 
26
28
  /// Deduplicate patches by [BundlePatchArtifact.baseBundleId], keeping the
27
29
  /// first occurrence (by insertion order).
30
+ ///
31
+ /// Returns an empty list if the bundle has no patches.
28
32
  List<BundlePatchArtifact> getBundlePatches(Bundle bundle) {
29
33
  final patches = bundle.patches;
30
34
  if (patches == null || patches.isEmpty) return const [];
@@ -1,10 +1,26 @@
1
1
  /// Binary patch artifact — hot-updater `BundlePatchArtifact`.
2
+ ///
3
+ /// Represents a single binary diff between two bundles. The device SDK uses
4
+ /// [patchStorageUri] to download the patch, verifies it against [patchFileHash],
5
+ /// then applies it on top of the base bundle identified by [baseBundleId].
2
6
  library;
3
7
 
8
+ /// A binary diff artifact that patches an older bundle into a newer one.
9
+ ///
10
+ /// Patch artifacts are stored in [Bundle.patches] as an ordered list. The
11
+ /// device SDK tries each patch in order and applies the first one whose base
12
+ /// matches the installed bundle.
4
13
  class BundlePatchArtifact {
14
+ /// UUID of the bundle this patch transforms from.
5
15
  final String baseBundleId;
16
+
17
+ /// MD5 hex of the base bundle's artifact (for verification).
6
18
  final String baseFileHash;
19
+
20
+ /// MD5 hex of the patch artifact (for download verification).
7
21
  final String patchFileHash;
22
+
23
+ /// Protocol URI pointing to the patch artifact in storage.
8
24
  final String patchStorageUri;
9
25
 
10
26
  const BundlePatchArtifact({
@@ -1,9 +1,16 @@
1
1
  /// Per-asset diff descriptors — hot-updater `ChangedAsset*`.
2
+ ///
3
+ /// Used by the CLI's asset diffing pipeline to describe how individual
4
+ /// assets changed between two bundles. The device SDK uses these descriptors
5
+ /// to apply incremental asset updates.
2
6
  library;
3
7
 
8
+ /// A single asset file that changed between bundles.
4
9
  class ChangedAssetFile {
5
- /// "br" or null.
10
+ /// Compression format (e.g. `"br"` for Brotli), or null if uncompressed.
6
11
  final String? compression;
12
+
13
+ /// Download URL for the full asset file.
7
14
  final String url;
8
15
 
9
16
  const ChangedAssetFile({required this.url, this.compression});
@@ -16,13 +23,27 @@ class ChangedAssetFile {
16
23
  Map<String, dynamic> toJson() => {'url': url, 'compression': compression};
17
24
  }
18
25
 
26
+ /// A binary patch for a changed asset.
27
+ ///
28
+ /// When an asset is modified between bundles, a bsdiff patch is generated
29
+ /// to avoid re-downloading the full file. The device SDK downloads [patchUrl],
30
+ /// verifies it against [patchFileHash], then applies the diff on top of
31
+ /// the base asset.
19
32
  class ChangedAssetPatch {
33
+ /// UUID of the bundle containing the original (unpatched) asset.
20
34
  final String baseBundleId;
35
+
36
+ /// MD5 hex of the original asset file (for verification).
21
37
  final String baseFileHash;
38
+
39
+ /// MD5 hex of the patch file (for verification).
22
40
  final String patchFileHash;
41
+
42
+ /// Download URL for the binary patch file.
23
43
  final String patchUrl;
24
44
 
25
- /// Always "bsdiff" in hot-updater; kept for wire compatibility.
45
+ /// Diff algorithm identifier. Always `"bsdiff"` in hot-updater;
46
+ /// kept for wire compatibility with future algorithms.
26
47
  final String algorithm;
27
48
 
28
49
  const ChangedAssetPatch({
@@ -51,10 +72,19 @@ class ChangedAssetPatch {
51
72
  };
52
73
  }
53
74
 
75
+ /// Describes a single asset that changed between two bundles.
76
+ ///
77
+ /// Exactly one of [file] or [patch] is non-null:
78
+ /// - [file] is set when the asset is new or replaced entirely.
79
+ /// - [patch] is set when the asset was modified and a binary diff is available.
54
80
  class ChangedAsset {
55
- /// SHA256 hex of the asset file.
81
+ /// SHA-256 hex of the asset file content.
56
82
  final String fileHash;
83
+
84
+ /// Full replacement file descriptor. Null if a patch is available instead.
57
85
  final ChangedAssetFile? file;
86
+
87
+ /// Binary diff patch. Null if the asset is new (use [file] instead).
58
88
  final ChangedAssetPatch? patch;
59
89
 
60
90
  const ChangedAsset({required this.fileHash, this.file, this.patch});
@@ -6,14 +6,26 @@ import 'uuid.dart' show nilUuid;
6
6
  ///
7
7
  /// Two variants keyed by [UpdateStrategy]; shared fields defaulted the same
8
8
  /// way (minBundleId = NIL_UUID, channel = "production").
9
+ ///
10
+ /// Use [AppVersionGetBundlesArgs] for semver-based targeting or
11
+ /// [FingerprintGetBundlesArgs] for device-specific targeting.
9
12
  sealed class GetBundlesArgs {
13
+ /// The update strategy this request uses.
10
14
  final UpdateStrategy _updateStrategy;
15
+
16
+ /// Target platform (`ios` or `android`).
11
17
  final Platform platform;
18
+
19
+ /// Currently installed bundle ID.
12
20
  final String bundleId;
21
+
22
+ /// Minimum bundle ID to consider (for rollback protection).
13
23
  final String minBundleId;
24
+
25
+ /// Release channel to query (default: `"production"`).
14
26
  final String channel;
15
27
 
16
- /// Cohort identifier for server-side rollout decisions.
28
+ /// Optional cohort identifier for server-side rollout decisions.
17
29
  final String? cohort;
18
30
 
19
31
  const GetBundlesArgs({
@@ -38,7 +50,13 @@ sealed class GetBundlesArgs {
38
50
  };
39
51
  }
40
52
 
53
+ /// Update-check args using device fingerprint for targeting.
54
+ ///
55
+ /// The server matches [fingerprintHash] against bundles that have a
56
+ /// `targetCohorts` list, enabling device-specific rollouts without
57
+ /// exposing device IDs.
41
58
  class FingerprintGetBundlesArgs extends GetBundlesArgs {
59
+ /// SHA-256 hex of the device fingerprint.
42
60
  final String fingerprintHash;
43
61
 
44
62
  const FingerprintGetBundlesArgs({
@@ -57,8 +75,12 @@ class FingerprintGetBundlesArgs extends GetBundlesArgs {
57
75
  };
58
76
  }
59
77
 
78
+ /// Update-check args using semver range for targeting.
79
+ ///
80
+ /// The server matches [appVersion] against bundles' `targetAppVersion`
81
+ /// semver ranges to determine eligibility.
60
82
  class AppVersionGetBundlesArgs extends GetBundlesArgs {
61
- /// Current app version (semver).
83
+ /// Current app version (semver string, e.g. `"1.2.3"`).
62
84
  final String appVersion;
63
85
 
64
86
  const AppVersionGetBundlesArgs({
@@ -2,13 +2,21 @@
2
2
  /// first but keep the full union for wire compatibility.
3
3
  library;
4
4
 
5
+ /// Target platform for bundle deployment and update checks.
5
6
  enum Platform {
7
+ /// Apple iOS platform.
6
8
  ios('ios'),
9
+
10
+ /// Google Android platform.
7
11
  android('android');
8
12
 
13
+ /// String value used in database rows and API payloads.
9
14
  final String value;
15
+
10
16
  const Platform(this.value);
11
17
 
18
+ /// Parse a platform string. Throws [ArgumentError] if [v] is not a
19
+ /// recognized platform value.
12
20
  static Platform fromValue(String v) => Platform.values.firstWhere(
13
21
  (p) => p.value == v,
14
22
  orElse: () => throw ArgumentError('unknown platform: $v'),
@@ -1,9 +1,6 @@
1
1
  import 'semver_range.dart' show satisfies;
2
2
  import 'semver_version.dart';
3
3
 
4
- export 'semver_range.dart' show satisfies;
5
- export 'semver_version.dart' show SemVer;
6
-
7
4
  /// hot-updater `semverSatisfies(targetAppVersion, currentVersion)`
8
5
  /// (plugins/plugin-core/src/semverSatisfies.ts):
9
6
  ///
@@ -1,25 +1,44 @@
1
1
  /// Update status enums — hot-updater `UpdateStatus` / `AppUpdateStatus`.
2
+ ///
3
+ /// [UpdateStatus] is used in database rows to mark bundles as deployed or
4
+ /// rolled back. [AppUpdateStatus] is derived by the client SDK to indicate
5
+ /// the result of an update check.
2
6
  library;
3
7
 
8
+ /// Server-side deployment status for a bundle.
4
9
  enum UpdateStatus {
10
+ /// Bundle has been rolled back (no longer served to devices).
5
11
  rollback('ROLLBACK'),
12
+
13
+ /// Bundle is active and served to eligible devices.
6
14
  update('UPDATE');
7
15
 
16
+ /// String value stored in database rows.
8
17
  final String value;
18
+
9
19
  const UpdateStatus(this.value);
10
20
 
21
+ /// Parse an update status string. Throws [ArgumentError] if [v] is not
22
+ /// a recognized status value.
11
23
  static UpdateStatus fromValue(String v) => UpdateStatus.values.firstWhere(
12
24
  (s) => s.value == v,
13
25
  orElse: () => throw ArgumentError('unknown status: $v'),
14
26
  );
15
27
  }
16
28
 
17
- /// What the client derives: UP_TO_DATE | ROLLBACK | UPDATE.
29
+ /// Client-side update check result.
18
30
  enum AppUpdateStatus {
31
+ /// Device is already on the latest bundle for its channel/platform.
19
32
  upToDate('UP_TO_DATE'),
33
+
34
+ /// Device should roll back to a previous bundle.
20
35
  rollback('ROLLBACK'),
36
+
37
+ /// Device should download and install a new bundle.
21
38
  update('UPDATE');
22
39
 
40
+ /// String value returned in update-check API responses.
23
41
  final String value;
42
+
24
43
  const AppUpdateStatus(this.value);
25
44
  }
@@ -1,10 +1,20 @@
1
1
  /// Update targeting strategy — hot-updater `UpdateStrategy`.
2
+ ///
3
+ /// Determines how the server matches bundles to devices during update checks.
2
4
  library;
3
5
 
6
+ /// Strategy for targeting bundles to devices.
4
7
  enum UpdateStrategy {
8
+ /// Match by device fingerprint hash. The server compares the hash against
9
+ /// bundles' `targetCohorts` list.
5
10
  fingerprint('fingerprint'),
11
+
12
+ /// Match by app version semver range. The server compares the version
13
+ /// against bundles' `targetAppVersion` range.
6
14
  appVersion('appVersion');
7
15
 
16
+ /// String value used in API payloads.
8
17
  final String value;
18
+
9
19
  const UpdateStrategy(this.value);
10
20
  }