@_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.
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +4 -3
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1422 -16
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
- package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
- package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +113 -10
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -6
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
- package/dart-src/packages/core/CHANGELOG.md +6 -1
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +73 -1
- package/dart-src/packages/core/lib/src/bundle.dart +62 -17
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
- package/dart-src/packages/core/lib/src/platform.dart +8 -0
- package/dart-src/packages/core/lib/src/semver.dart +0 -3
- package/dart-src/packages/core/lib/src/status.dart +20 -1
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
- package/dart-src/packages/core/lib/src/uuid.dart +11 -0
- package/dart-src/plugins/aws/CHANGELOG.md +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
- package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
- package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +617 -4
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
- package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
- package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
- package/package.json +2 -2
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +0 -36
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -458
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
- 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/
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
108
|
+
expect(result.skipped, containsAll(_allCollections));
|
|
104
109
|
expect(result.created, isEmpty);
|
|
105
110
|
expect(mock.createdCollections, isEmpty);
|
|
106
111
|
});
|
|
@@ -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
|
-
///
|
|
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
|
-
'
|
|
179
|
-
'
|
|
180
|
-
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
'
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
'
|
|
189
|
-
'
|
|
190
|
-
|
|
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 &&
|
|
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 =>
|
|
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
|
-
///
|
|
12
|
-
///
|
|
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;
|
|
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
|
-
///
|
|
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
|
-
///
|
|
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
|
-
///
|
|
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.
|