@_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.
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -1
- 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 +96 -58
- 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/process_manager.dart +10 -2
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -1
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +71 -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/lib/src/aws_cloudfront_client.dart +2 -2
- 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/lib/src/pocketbase_client.dart +3 -0
- package/package.json +1 -1
- 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 -460
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
- 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
|
|
251
|
-
|
|
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:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
391
|
-
|
|
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
|
|
769
|
-
'`
|
|
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
|
|
775
|
-
'
|
|
776
|
-
'`
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
-
|
|
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
|
-
///
|
|
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
|
-
'
|
|
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
|
}
|