@_nazmiforreal/flutter-ota 0.1.19 → 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 (59) hide show
  1. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +4 -3
  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 +1422 -16
  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/installer.dart +1 -1
  19. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +113 -10
  20. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -6
  21. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
  22. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
  23. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
  24. package/dart-src/packages/core/CHANGELOG.md +6 -1
  25. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
  26. package/dart-src/packages/core/lib/src/app_update_info.dart +73 -1
  27. package/dart-src/packages/core/lib/src/bundle.dart +62 -17
  28. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
  29. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
  30. package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
  31. package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
  32. package/dart-src/packages/core/lib/src/platform.dart +8 -0
  33. package/dart-src/packages/core/lib/src/semver.dart +0 -3
  34. package/dart-src/packages/core/lib/src/status.dart +20 -1
  35. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  36. package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
  37. package/dart-src/packages/core/lib/src/uuid.dart +11 -0
  38. package/dart-src/plugins/aws/CHANGELOG.md +5 -1
  39. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
  40. package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
  41. package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
  42. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
  43. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
  44. package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
  45. package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
  46. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
  47. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
  48. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +617 -4
  49. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
  50. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
  51. package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
  52. package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
  53. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
  54. package/package.json +2 -2
  55. package/bin/flutter-ota-linux-x64 +0 -0
  56. package/bin/flutter-ota.js +0 -36
  57. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -458
  58. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
  59. package/dart-src/packages/cli-tools/pubspec.lock +0 -590
@@ -29,7 +29,7 @@ class _MockPocketBase {
29
29
 
30
30
  Future<void> _handle(HttpRequest req) async {
31
31
  final path = req.uri.path;
32
- if (path == '/api/admins/auth-with-password') {
32
+ if (path == '/api/collections/_superusers/auth-with-password') {
33
33
  authCalled = true;
34
34
  final body = jsonDecode(await utf8.decoder.bind(req).join()) as Map;
35
35
  lastAuthEmail = body['identity'] as String?;
@@ -63,6 +63,14 @@ class _MockPocketBase {
63
63
  }
64
64
  }
65
65
 
66
+ const _allCollections = [
67
+ 'bundles',
68
+ 'channels',
69
+ 'audit_log',
70
+ 'bundles_patches',
71
+ 'app_meta',
72
+ ];
73
+
66
74
  void main() {
67
75
  late _MockPocketBase mock;
68
76
  setUp(() async {
@@ -73,7 +81,7 @@ void main() {
73
81
  });
74
82
 
75
83
  test(
76
- 'schema installer creates the three collections on a fresh PB',
84
+ 'schema installer creates all five collections on a fresh PB',
77
85
  () async {
78
86
  final installer = PocketBaseSchemaInstaller(
79
87
  url: mock.baseUrl,
@@ -83,24 +91,21 @@ void main() {
83
91
  final result = await installer.install();
84
92
  expect(mock.authCalled, isTrue);
85
93
  expect(mock.lastAuthEmail, 'admin@x.com');
86
- expect(result.created, containsAll(['bundles', 'channels', 'audit_log']));
94
+ expect(result.created, containsAll(_allCollections));
87
95
  expect(result.skipped, isEmpty);
88
- expect(
89
- mock.createdCollections,
90
- containsAll(['bundles', 'channels', 'audit_log']),
91
- );
96
+ expect(mock.createdCollections, containsAll(_allCollections));
92
97
  },
93
98
  );
94
99
 
95
100
  test('schema installer skips collections that already exist', () async {
96
- mock.existingCollections = ['bundles', 'channels', 'audit_log'];
101
+ mock.existingCollections = _allCollections;
97
102
  final installer = PocketBaseSchemaInstaller(
98
103
  url: mock.baseUrl,
99
104
  adminEmail: 'admin@x.com',
100
105
  adminPassword: 'secret',
101
106
  );
102
107
  final result = await installer.install();
103
- expect(result.skipped, containsAll(['bundles', 'channels', 'audit_log']));
108
+ expect(result.skipped, containsAll(_allCollections));
104
109
  expect(result.created, isEmpty);
105
110
  expect(mock.createdCollections, isEmpty);
106
111
  });
@@ -1 +1,6 @@
1
- ## 0.1.4
1
+ ## 0.1.6
2
+
3
+ - Added `gitCommitHash` field to `UpdateInfo` for commit tracking.
4
+ - General maintenance.
5
+
6
+ ## 0.1.5
@@ -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,17 +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;
12
26
 
27
+ /// Git commit hash of the build, if available.
28
+ final String? gitCommitHash;
29
+
13
30
  /// Internal rollout metadata; never serialized to update-check clients.
14
31
  final int? rolloutCohortCount;
32
+
33
+ /// Cohort identifiers for targeted rollouts; never sent to devices.
15
34
  final List<String>? targetCohorts;
16
35
 
17
36
  const UpdateInfo({
@@ -21,6 +40,7 @@ class UpdateInfo {
21
40
  required this.status,
22
41
  required this.storageUri,
23
42
  required this.fileHash,
43
+ this.gitCommitHash,
24
44
  this.rolloutCohortCount,
25
45
  this.targetCohorts,
26
46
  });
@@ -28,20 +48,39 @@ class UpdateInfo {
28
48
 
29
49
  /// Device-facing payload for an available update —
30
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.
31
54
  class AppUpdateAvailableInfo extends AppUpdateInfo {
55
+ @override
32
56
  final String id;
57
+
58
+ @override
33
59
  final bool shouldForceUpdate;
60
+
61
+ @override
34
62
  final String? message;
63
+
64
+ @override
35
65
  final UpdateStatus status;
66
+
67
+ /// Signed download URL for the artifact.
36
68
  final String? fileUrl;
69
+
70
+ /// MD5 hex of the artifact for download verification.
37
71
  final String? fileHash;
38
72
 
39
73
  /// Ed25519 (base64) signature over the artifact MD5 hex string. Consumed by
40
74
  /// the device SDK to verify the patch (`PatchInfo.signature`).
41
75
  final String? signature;
42
76
 
77
+ /// Signed URL for the asset manifest (if asset-based patch).
43
78
  final String? manifestUrl;
79
+
80
+ /// MD5 hex of the manifest file for verification.
44
81
  final String? manifestFileHash;
82
+
83
+ /// Map of asset path to diff descriptor for incremental asset updates.
45
84
  final Map<String, ChangedAsset>? changedAssets;
46
85
 
47
86
  const AppUpdateAvailableInfo({
@@ -92,19 +131,52 @@ class AppUpdateAvailableInfo extends AppUpdateInfo {
92
131
  }
93
132
 
94
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.
95
137
  class AppUpToDateInfo extends AppUpdateInfo {
96
138
  const AppUpToDateInfo();
97
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
+
98
152
  @override
99
153
  Map<String, dynamic> toJson() => {'status': 'UP_TO_DATE'};
100
154
  }
101
155
 
102
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.
103
160
  sealed class AppUpdateInfo {
104
161
  const AppUpdateInfo();
105
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.
106
176
  Map<String, dynamic> toJson();
107
177
 
178
+ /// Deserialize from JSON. Returns [AppUpToDateInfo] if status is
179
+ /// `"UP_TO_DATE"`, otherwise [AppUpdateAvailableInfo].
108
180
  factory AppUpdateInfo.fromJson(Map<String, dynamic> j) {
109
181
  final raw = j['status'];
110
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
  }
@@ -2,12 +2,27 @@ import 'platform.dart';
2
2
 
3
3
  /// hot-updater `UpdateBundleParams` — device-side parameters sent with
4
4
  /// update requests.
5
+ ///
6
+ /// These parameters are collected by the device SDK and sent to the
7
+ /// backend's update-check endpoint to determine which bundle the
8
+ /// device should install.
5
9
  class UpdateBundleParams {
10
+ /// Target platform (`ios` or `android`).
6
11
  final Platform platform;
12
+
13
+ /// Currently installed bundle ID.
7
14
  final String bundleId;
15
+
16
+ /// Minimum bundle ID to consider (for rollback protection).
8
17
  final String minBundleId;
18
+
19
+ /// Release channel to query.
9
20
  final String channel;
21
+
22
+ /// Current app version (semver string).
10
23
  final String appVersion;
24
+
25
+ /// Optional device fingerprint hash for targeted rollouts.
11
26
  final String? fingerprintHash;
12
27
 
13
28
  const UpdateBundleParams({
@@ -6,18 +6,28 @@ library;
6
6
 
7
7
  import 'dart:math';
8
8
 
9
+ /// The zero UUID, used as a sentinel for "no bundle" or "latest".
9
10
  const String nilUuid = '00000000-0000-0000-0000-000000000000';
10
11
 
12
+ /// Regex pattern for validating UUIDv7 strings.
11
13
  final RegExp _uuidV7Pattern = RegExp(
12
14
  r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
13
15
  );
14
16
 
17
+ /// Secure random number generator for UUIDv7 variant bits.
15
18
  final Random _random = Random.secure();
16
19
 
20
+ /// Format an integer as a zero-padded hex string.
17
21
  String _hex(int value, int width) =>
18
22
  value.toRadixString(16).padLeft(width, '0');
19
23
 
20
24
  /// Generates a lowercase RFC 9562 UUIDv7 string (unix-ms timestamp based).
25
+ ///
26
+ /// The UUIDv7 format encodes a 48-bit millisecond timestamp in the first
27
+ /// 48 bits, making strings lexicographically sortable by creation time.
28
+ ///
29
+ /// Throws [ArgumentError] if the current timestamp is outside the valid
30
+ /// UUIDv7 range (0 to 2^48 - 1 milliseconds).
21
31
  String uuidV7({DateTime? now}) {
22
32
  final ts = (now ?? DateTime.now()).millisecondsSinceEpoch;
23
33
  if (ts < 0 || ts > 0xFFFFFFFFFFFF) {
@@ -38,6 +48,7 @@ String uuidV7({DateTime? now}) {
38
48
  '7$g3-$g4-$g5';
39
49
  }
40
50
 
51
+ /// Returns true if [value] is a valid UUIDv7 string.
41
52
  bool isUuidV7(String value) => _uuidV7Pattern.hasMatch(value);
42
53
 
43
54
  /// Lexicographic comparison used by hot-updater for bundle ordering.
@@ -1 +1,5 @@
1
- ## 0.1.5
1
+ ## 0.1.7
2
+
3
+ - General maintenance.
4
+
5
+ ## 0.1.6