@_nazmiforreal/flutter-ota 0.1.0 → 0.1.2
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/bin/flutter-patcher.js +48 -39
- package/dart-src/packages/cli-tools/.gitkeep +0 -0
- package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
- package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
- package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
- package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
- package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
- package/dart-src/packages/core/lib/src/bundle.dart +191 -0
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
- package/dart-src/packages/core/lib/src/metadata.dart +31 -0
- package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
- package/dart-src/packages/core/lib/src/platform.dart +15 -0
- package/dart-src/packages/core/lib/src/rollout.dart +209 -0
- package/dart-src/packages/core/lib/src/semver.dart +70 -0
- package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
- package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
- package/dart-src/packages/core/lib/src/status.dart +24 -0
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
- package/dart-src/packages/core/lib/src/uuid.dart +45 -0
- package/dart-src/packages/core/pubspec.yaml +9 -0
- package/dart-src/packages/core/tool/dbg.dart +6 -0
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
- package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
- package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
- package/dart-src/plugins/aws/pubspec.yaml +22 -0
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
- package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
- package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
- package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
- package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
- package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
- package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
- package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
- package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
- package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
- package/dart-src/plugins/postgres/pubspec.yaml +18 -0
- package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
- package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
- package/dart-src/plugins/standalone/pubspec.yaml +18 -0
- package/dart-src/plugins/supabase/lib/edge.dart +10 -0
- package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
- package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
- package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
- package/dart-src/plugins/supabase/pubspec.yaml +19 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +53 -25
- /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/// Minimum metadata needed to install a patch.
|
|
2
|
+
///
|
|
3
|
+
/// `PatchInfo` is intentionally backend-agnostic. Your update data can come
|
|
4
|
+
/// from HTTP JSON, gRPC, remote config, or hard-coded test data; as long as you
|
|
5
|
+
/// can provide these fields, the patch can be passed to
|
|
6
|
+
/// `FlutterPatcher.applyPatch`.
|
|
7
|
+
class PatchInfo {
|
|
8
|
+
/// Patch identifier, for example `1.0.1-h1`.
|
|
9
|
+
final String version;
|
|
10
|
+
|
|
11
|
+
/// Patch payload URL.
|
|
12
|
+
///
|
|
13
|
+
/// For a lib-only patch this points to `libapp.so`. For an asset patch this
|
|
14
|
+
/// points to `patch.zip`.
|
|
15
|
+
final String patchUrl;
|
|
16
|
+
|
|
17
|
+
/// MD5 of the patch payload, lower-case hex.
|
|
18
|
+
///
|
|
19
|
+
/// For a lib-only patch this is the MD5 of `libapp.so`. For an asset patch
|
|
20
|
+
/// this is the MD5 of `patch.zip`. An empty string skips payload MD5
|
|
21
|
+
/// verification and also skips signature verification.
|
|
22
|
+
final String md5;
|
|
23
|
+
|
|
24
|
+
/// Base64 Ed25519 signature over the [md5] hex string.
|
|
25
|
+
///
|
|
26
|
+
/// Empty disables signature verification. When [md5] is empty this field is
|
|
27
|
+
/// ignored because there is no signed message.
|
|
28
|
+
final String signature;
|
|
29
|
+
|
|
30
|
+
/// Host APK `versionCode` this patch targets.
|
|
31
|
+
///
|
|
32
|
+
/// On cold start, a patch whose target versionCode no longer matches the
|
|
33
|
+
/// installed APK is dropped automatically. If null, native code binds the
|
|
34
|
+
/// patch to the current app versionCode during `applyPatch`.
|
|
35
|
+
final int? targetVersionCode;
|
|
36
|
+
|
|
37
|
+
/// Original JSON fields preserved by [PatchInfo.fromJson].
|
|
38
|
+
///
|
|
39
|
+
/// Direct construction does not use this field.
|
|
40
|
+
final Map<String, dynamic> raw;
|
|
41
|
+
|
|
42
|
+
const PatchInfo({
|
|
43
|
+
required this.version,
|
|
44
|
+
required this.patchUrl,
|
|
45
|
+
this.md5 = '',
|
|
46
|
+
this.signature = '',
|
|
47
|
+
this.targetVersionCode,
|
|
48
|
+
this.raw = const {},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/// Parses the minimal built-in check-update protocol.
|
|
52
|
+
///
|
|
53
|
+
/// If your server response has a different shape, construct [PatchInfo]
|
|
54
|
+
/// directly. Compatible field names:
|
|
55
|
+
///
|
|
56
|
+
/// - `patchUrl` / `patch_url`
|
|
57
|
+
/// - `targetVersionCode` / `target_version_code`
|
|
58
|
+
factory PatchInfo.fromJson(Map<String, dynamic> json) {
|
|
59
|
+
final rawVc = json['targetVersionCode'] ?? json['target_version_code'];
|
|
60
|
+
final int? parsedVc = rawVc is num
|
|
61
|
+
? rawVc.toInt()
|
|
62
|
+
: (rawVc is String && rawVc.isNotEmpty ? int.tryParse(rawVc) : null);
|
|
63
|
+
return PatchInfo(
|
|
64
|
+
version: (json['version'] ?? '') as String,
|
|
65
|
+
patchUrl: (json['patchUrl'] ?? json['patch_url'] ?? '') as String,
|
|
66
|
+
md5: (json['md5'] ?? '') as String,
|
|
67
|
+
signature: (json['signature'] ?? '') as String,
|
|
68
|
+
targetVersionCode: parsedVc,
|
|
69
|
+
raw: Map<String, dynamic>.from(json),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Serializes this patch for the native MethodChannel call.
|
|
74
|
+
Map<String, dynamic> toJson() => {
|
|
75
|
+
'version': version,
|
|
76
|
+
'patchUrl': patchUrl,
|
|
77
|
+
if (md5.isNotEmpty) 'md5': md5,
|
|
78
|
+
'signature': signature,
|
|
79
|
+
if (targetVersionCode != null) 'targetVersionCode': targetVersionCode,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
String toString() => 'PatchInfo('
|
|
84
|
+
'version=$version, url=$patchUrl, '
|
|
85
|
+
'md5=${md5.isEmpty ? 'none' : md5}, '
|
|
86
|
+
'sig=${signature.isEmpty ? 'none' : '***'})';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Stages emitted while applying a patch.
|
|
90
|
+
enum PatchApplyPhase {
|
|
91
|
+
/// Payload download. [PatchApplyProgress.bytesReceived] and
|
|
92
|
+
/// [PatchApplyProgress.totalBytes] are meaningful in this phase.
|
|
93
|
+
downloading,
|
|
94
|
+
|
|
95
|
+
/// Payload MD5 / signature verification.
|
|
96
|
+
verifying,
|
|
97
|
+
|
|
98
|
+
/// Package parsing, asset installation, and transaction commit.
|
|
99
|
+
finalizing,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
PatchApplyPhase _parsePhase(String? s) {
|
|
103
|
+
switch (s) {
|
|
104
|
+
case 'downloading':
|
|
105
|
+
return PatchApplyPhase.downloading;
|
|
106
|
+
case 'verifying':
|
|
107
|
+
return PatchApplyPhase.verifying;
|
|
108
|
+
case 'finalizing':
|
|
109
|
+
return PatchApplyPhase.finalizing;
|
|
110
|
+
default:
|
|
111
|
+
return PatchApplyPhase.downloading;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Progress event emitted by `FlutterPatcher.applyProgress`.
|
|
116
|
+
class PatchApplyProgress {
|
|
117
|
+
final PatchApplyPhase phase;
|
|
118
|
+
|
|
119
|
+
/// Meaningful only when [phase] is [PatchApplyPhase.downloading].
|
|
120
|
+
final int bytesReceived;
|
|
121
|
+
|
|
122
|
+
/// Meaningful only when [phase] is [PatchApplyPhase.downloading].
|
|
123
|
+
///
|
|
124
|
+
/// `-1` means the server did not provide `Content-Length`.
|
|
125
|
+
final int totalBytes;
|
|
126
|
+
|
|
127
|
+
const PatchApplyProgress({
|
|
128
|
+
required this.phase,
|
|
129
|
+
this.bytesReceived = 0,
|
|
130
|
+
this.totalBytes = 0,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/// Download progress from 0.0 to 1.0, or null when unknown/not downloading.
|
|
134
|
+
double? get fraction {
|
|
135
|
+
if (phase != PatchApplyPhase.downloading) return null;
|
|
136
|
+
if (totalBytes <= 0) return null;
|
|
137
|
+
return (bytesReceived / totalBytes).clamp(0.0, 1.0).toDouble();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
factory PatchApplyProgress.fromNative(Object? native) {
|
|
141
|
+
if (native is! Map) {
|
|
142
|
+
return const PatchApplyProgress(phase: PatchApplyPhase.downloading);
|
|
143
|
+
}
|
|
144
|
+
final map = Map<String, dynamic>.from(native);
|
|
145
|
+
return PatchApplyProgress(
|
|
146
|
+
phase: _parsePhase(map['phase'] as String?),
|
|
147
|
+
bytesReceived: (map['received'] as num?)?.toInt() ?? 0,
|
|
148
|
+
totalBytes: (map['total'] as num?)?.toInt() ?? 0,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@override
|
|
153
|
+
String toString() {
|
|
154
|
+
if (phase != PatchApplyPhase.downloading) {
|
|
155
|
+
return 'PatchApplyProgress(${phase.name})';
|
|
156
|
+
}
|
|
157
|
+
final f = fraction;
|
|
158
|
+
final pct = f != null ? '${(f * 100).toStringAsFixed(1)}%' : '?';
|
|
159
|
+
return 'PatchApplyProgress(downloading, $bytesReceived/$totalBytes, $pct)';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Classified failure reason for `FlutterPatcher.applyPatch`.
|
|
164
|
+
enum PatchApplyError {
|
|
165
|
+
/// Missing version/URL, invalid MD5 format, unsupported mode, or target
|
|
166
|
+
/// version mismatch.
|
|
167
|
+
invalidArgs,
|
|
168
|
+
|
|
169
|
+
/// The same `(version, md5)` payload is in the local bad-patch blacklist.
|
|
170
|
+
blacklisted,
|
|
171
|
+
|
|
172
|
+
/// Download failed after retries.
|
|
173
|
+
network,
|
|
174
|
+
|
|
175
|
+
/// Payload, lib, or overlay asset MD5 did not match expected metadata.
|
|
176
|
+
md5Mismatch,
|
|
177
|
+
|
|
178
|
+
/// Ed25519 verification failed, or strict mode rejected a signed patch on an
|
|
179
|
+
/// Android version without Ed25519 support.
|
|
180
|
+
signatureInvalid,
|
|
181
|
+
|
|
182
|
+
/// The patch package has no `libapp.so` for the current device ABI.
|
|
183
|
+
unsupportedAbi,
|
|
184
|
+
|
|
185
|
+
/// Invalid asset package: bad zip/schema/manifest, unsafe path, missing asset
|
|
186
|
+
/// entry, or unsupported asset mode/op.
|
|
187
|
+
assetPackageInvalid,
|
|
188
|
+
|
|
189
|
+
/// Filesystem, disk space, copy, fsync, or rename failure.
|
|
190
|
+
ioError,
|
|
191
|
+
|
|
192
|
+
/// Unclassified native/channel error.
|
|
193
|
+
unknown,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
PatchApplyError _parseApplyError(String? code) {
|
|
197
|
+
switch (code) {
|
|
198
|
+
case 'INVALID_ARGS':
|
|
199
|
+
return PatchApplyError.invalidArgs;
|
|
200
|
+
case 'BLACKLISTED':
|
|
201
|
+
return PatchApplyError.blacklisted;
|
|
202
|
+
case 'NETWORK':
|
|
203
|
+
return PatchApplyError.network;
|
|
204
|
+
case 'MD5_MISMATCH':
|
|
205
|
+
return PatchApplyError.md5Mismatch;
|
|
206
|
+
case 'SIGNATURE_INVALID':
|
|
207
|
+
return PatchApplyError.signatureInvalid;
|
|
208
|
+
case 'UNSUPPORTED_ABI':
|
|
209
|
+
return PatchApplyError.unsupportedAbi;
|
|
210
|
+
case 'ASSET_PACKAGE_INVALID':
|
|
211
|
+
return PatchApplyError.assetPackageInvalid;
|
|
212
|
+
case 'IO_ERROR':
|
|
213
|
+
return PatchApplyError.ioError;
|
|
214
|
+
default:
|
|
215
|
+
return PatchApplyError.unknown;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Structured result returned by `FlutterPatcher.applyPatch`.
|
|
220
|
+
class PatchApplyResult {
|
|
221
|
+
/// Whether the patch was installed successfully.
|
|
222
|
+
///
|
|
223
|
+
/// Reapplying the same already-installed patch is also considered success.
|
|
224
|
+
final bool ok;
|
|
225
|
+
|
|
226
|
+
/// Failure category. Null when [ok] is true.
|
|
227
|
+
final PatchApplyError? error;
|
|
228
|
+
|
|
229
|
+
/// Developer-facing failure description. Do not show directly to users.
|
|
230
|
+
final String? message;
|
|
231
|
+
|
|
232
|
+
const PatchApplyResult._({required this.ok, this.error, this.message});
|
|
233
|
+
|
|
234
|
+
factory PatchApplyResult.success() => const PatchApplyResult._(ok: true);
|
|
235
|
+
|
|
236
|
+
factory PatchApplyResult.failure(PatchApplyError error, [String? message]) =>
|
|
237
|
+
PatchApplyResult._(ok: false, error: error, message: message);
|
|
238
|
+
|
|
239
|
+
/// Deserializes the native MethodChannel result.
|
|
240
|
+
factory PatchApplyResult.fromNative(Object? native) {
|
|
241
|
+
if (native is! Map) {
|
|
242
|
+
return PatchApplyResult.failure(
|
|
243
|
+
PatchApplyError.unknown,
|
|
244
|
+
'invalid native result: $native',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
final map = Map<String, dynamic>.from(native);
|
|
248
|
+
if (map['ok'] == true) return PatchApplyResult.success();
|
|
249
|
+
return PatchApplyResult.failure(
|
|
250
|
+
_parseApplyError(map['error'] as String?),
|
|
251
|
+
map['message'] as String?,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@override
|
|
256
|
+
String toString() => ok
|
|
257
|
+
? 'PatchApplyResult(ok)'
|
|
258
|
+
: 'PatchApplyResult(error=${error?.name}, message=$message)';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Result returned by `FlutterPatcher.checkUpdate`.
|
|
262
|
+
class PatchCheckResult {
|
|
263
|
+
/// Whether a new patch is available.
|
|
264
|
+
final bool hasUpdate;
|
|
265
|
+
|
|
266
|
+
/// Patch metadata. Null when [hasUpdate] is false.
|
|
267
|
+
final PatchInfo? patch;
|
|
268
|
+
|
|
269
|
+
const PatchCheckResult({required this.hasUpdate, this.patch});
|
|
270
|
+
|
|
271
|
+
factory PatchCheckResult.none() =>
|
|
272
|
+
const PatchCheckResult(hasUpdate: false, patch: null);
|
|
273
|
+
|
|
274
|
+
factory PatchCheckResult.fromJson(Map<String, dynamic> json) {
|
|
275
|
+
final hasUpdate = json['hasUpdate'] == true || json['has_update'] == true;
|
|
276
|
+
if (!hasUpdate) return PatchCheckResult.none();
|
|
277
|
+
final patchJson = json['patch'];
|
|
278
|
+
final patchMap = patchJson is Map ? patchJson : json;
|
|
279
|
+
return PatchCheckResult(
|
|
280
|
+
hasUpdate: true,
|
|
281
|
+
patch: PatchInfo.fromJson(Map<String, dynamic>.from(patchMap)),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// Platform enum — hot-updater keeps `ios | android`; we implement Android
|
|
2
|
+
/// first but keep the full union for wire compatibility.
|
|
3
|
+
library;
|
|
4
|
+
|
|
5
|
+
enum Platform {
|
|
6
|
+
ios('ios'),
|
|
7
|
+
android('android');
|
|
8
|
+
|
|
9
|
+
final String value;
|
|
10
|
+
const Platform(this.value);
|
|
11
|
+
|
|
12
|
+
static Platform fromValue(String v) =>
|
|
13
|
+
Platform.values.firstWhere((p) => p.value == v,
|
|
14
|
+
orElse: () => throw ArgumentError('unknown platform: $v'));
|
|
15
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/// Dart translation of hot-updater `packages/core/src/rollout.ts`.
|
|
2
|
+
///
|
|
3
|
+
/// Deterministic staged-rollout cohort math. Numeric cohorts are 1..1000;
|
|
4
|
+
/// each bundle gets a per-bundle deterministic shuffle so expanding a rollout
|
|
5
|
+
/// never drops devices that were already included.
|
|
6
|
+
library;
|
|
7
|
+
|
|
8
|
+
const int numericCohortSize = 1000;
|
|
9
|
+
const int defaultRolloutCohortCount = numericCohortSize;
|
|
10
|
+
const int maxCohortLength = 64;
|
|
11
|
+
const String invalidCohortErrorMessage =
|
|
12
|
+
'Invalid cohort. Use 1-1000 or a lowercase slug without spaces, '
|
|
13
|
+
'up to $maxCohortLength characters.';
|
|
14
|
+
|
|
15
|
+
final RegExp _digitsOnly = RegExp(r'^\d+$');
|
|
16
|
+
final RegExp _customCohortPattern = RegExp(r'^[a-z0-9-]+$');
|
|
17
|
+
|
|
18
|
+
int? _parseNumericCohortValue(String cohort) {
|
|
19
|
+
if (!_digitsOnly.hasMatch(cohort)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
final parsed = int.tryParse(cohort);
|
|
23
|
+
if (parsed == null || parsed < 1 || parsed > numericCohortSize) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
int _positiveMod(int value, int modulus) => ((value % modulus) + modulus) % modulus;
|
|
30
|
+
|
|
31
|
+
/// Java-style 32-bit signed rolling string hash (`(h<<5) - h + c`, truncated).
|
|
32
|
+
int _hashString(String value) {
|
|
33
|
+
var hash = 0;
|
|
34
|
+
for (var i = 0; i < value.length; i++) {
|
|
35
|
+
final char = value.codeUnitAt(i);
|
|
36
|
+
hash = _toSigned32((hash << 5) - hash + char);
|
|
37
|
+
}
|
|
38
|
+
return hash;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
int _toSigned32(int v) {
|
|
42
|
+
v &= 0xFFFFFFFF;
|
|
43
|
+
return v >= 0x80000000 ? v - 0x100000000 : v;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
int _gcd(int a, int b) {
|
|
47
|
+
var x = a.abs();
|
|
48
|
+
var y = b.abs();
|
|
49
|
+
while (y != 0) {
|
|
50
|
+
final next = x % y;
|
|
51
|
+
x = y;
|
|
52
|
+
y = next;
|
|
53
|
+
}
|
|
54
|
+
return x;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
int _modularInverse(int value, int modulus) {
|
|
58
|
+
var t = 0;
|
|
59
|
+
var newT = 1;
|
|
60
|
+
var r = modulus;
|
|
61
|
+
var newR = _positiveMod(value, modulus);
|
|
62
|
+
while (newR != 0) {
|
|
63
|
+
final quotient = r ~/ newR;
|
|
64
|
+
final tmpT = t - quotient * newT;
|
|
65
|
+
t = newT;
|
|
66
|
+
newT = tmpT;
|
|
67
|
+
final tmpR = r - quotient * newR;
|
|
68
|
+
r = newR;
|
|
69
|
+
newR = tmpR;
|
|
70
|
+
}
|
|
71
|
+
if (r > 1) {
|
|
72
|
+
throw ArgumentError('No modular inverse for $value mod $modulus');
|
|
73
|
+
}
|
|
74
|
+
return _positiveMod(t, modulus);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
({int multiplier, int offset, int inverseMultiplier}) _rolloutShuffleParameters(
|
|
78
|
+
String bundleId) {
|
|
79
|
+
var multiplier = _positiveMod(_hashString('$bundleId:multiplier'), 997);
|
|
80
|
+
if (multiplier == 0) {
|
|
81
|
+
multiplier = 1;
|
|
82
|
+
}
|
|
83
|
+
while (_gcd(multiplier, numericCohortSize) != 1) {
|
|
84
|
+
multiplier = _positiveMod(multiplier + 1, numericCohortSize);
|
|
85
|
+
if (multiplier == 0) {
|
|
86
|
+
multiplier = 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
final offset = _positiveMod(
|
|
90
|
+
_hashString('$bundleId:offset'),
|
|
91
|
+
numericCohortSize,
|
|
92
|
+
);
|
|
93
|
+
return (
|
|
94
|
+
multiplier: multiplier,
|
|
95
|
+
offset: offset,
|
|
96
|
+
inverseMultiplier: _modularInverse(multiplier, numericCohortSize),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Clamps a rollout percentage to 0..1000 (per-mille). Null means 100%.
|
|
101
|
+
int normalizeRolloutCohortCount(int? rolloutCohortCount) {
|
|
102
|
+
if (rolloutCohortCount == null) {
|
|
103
|
+
return defaultRolloutCohortCount;
|
|
104
|
+
}
|
|
105
|
+
if (rolloutCohortCount <= 0) {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
if (rolloutCohortCount >= numericCohortSize) {
|
|
109
|
+
return numericCohortSize;
|
|
110
|
+
}
|
|
111
|
+
return rolloutCohortCount.floor();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
String normalizeCohortValue(String cohort) {
|
|
115
|
+
final normalized = cohort.trim().toLowerCase();
|
|
116
|
+
final numeric = _parseNumericCohortValue(normalized);
|
|
117
|
+
if (numeric != null) {
|
|
118
|
+
return '$numeric';
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
int? getNumericCohortValue(String cohort) =>
|
|
124
|
+
_parseNumericCohortValue(normalizeCohortValue(cohort));
|
|
125
|
+
|
|
126
|
+
bool isNumericCohort(String cohort) => getNumericCohortValue(cohort) != null;
|
|
127
|
+
|
|
128
|
+
bool isCustomCohort(String cohort) {
|
|
129
|
+
final normalized = normalizeCohortValue(cohort);
|
|
130
|
+
return normalized.isNotEmpty &&
|
|
131
|
+
normalized.length <= maxCohortLength &&
|
|
132
|
+
!_digitsOnly.hasMatch(normalized) &&
|
|
133
|
+
_customCohortPattern.hasMatch(normalized);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
bool isValidCohort(String cohort) {
|
|
137
|
+
final normalized = normalizeCohortValue(cohort);
|
|
138
|
+
return isNumericCohort(normalized) || isCustomCohort(normalized);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Default cohort derived from a device identifier (ANDROID_ID etc.).
|
|
142
|
+
String getDefaultNumericCohort(String identifier) {
|
|
143
|
+
final cohortValue = _positiveMod(_hashString(identifier), numericCohortSize) + 1;
|
|
144
|
+
return '$cohortValue';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
int getNumericCohortRolloutPosition(String bundleId, int cohortValue) {
|
|
148
|
+
if (cohortValue < 1 || cohortValue > numericCohortSize) {
|
|
149
|
+
throw ArgumentError('Invalid numeric cohort: $cohortValue');
|
|
150
|
+
}
|
|
151
|
+
final params = _rolloutShuffleParameters(bundleId);
|
|
152
|
+
final zeroBasedCohort = cohortValue - 1;
|
|
153
|
+
return _positiveMod(
|
|
154
|
+
params.inverseMultiplier * (zeroBasedCohort - params.offset),
|
|
155
|
+
numericCohortSize,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
List<int> getRolledOutNumericCohorts(
|
|
160
|
+
String bundleId,
|
|
161
|
+
int? rolloutCohortCount,
|
|
162
|
+
) {
|
|
163
|
+
final normalized = normalizeRolloutCohortCount(rolloutCohortCount);
|
|
164
|
+
if (normalized <= 0) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return List.generate(numericCohortSize, (index) => index + 1)
|
|
168
|
+
.where((cohortValue) =>
|
|
169
|
+
normalized >= numericCohortSize ||
|
|
170
|
+
getNumericCohortRolloutPosition(bundleId, cohortValue) < normalized)
|
|
171
|
+
.toList();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Mirrors hot-updater semantics exactly:
|
|
175
|
+
/// - explicit target_cohorts match always wins (even at 100%/excluded cohorts)
|
|
176
|
+
/// - no cohort provided: eligible only at full rollout
|
|
177
|
+
/// - custom (non-numeric) cohorts are ineligible for percentage rollouts
|
|
178
|
+
bool isCohortEligibleForUpdate(
|
|
179
|
+
String bundleId,
|
|
180
|
+
String? cohort,
|
|
181
|
+
int? rolloutCohortCount,
|
|
182
|
+
List<String>? targetCohorts,
|
|
183
|
+
) {
|
|
184
|
+
final normalizedCohort = cohort == null ? null : normalizeCohortValue(cohort);
|
|
185
|
+
final normalizedTargets =
|
|
186
|
+
targetCohorts?.map(normalizeCohortValue).toList() ?? <String>[];
|
|
187
|
+
|
|
188
|
+
if (normalizedCohort != null && normalizedTargets.contains(normalizedCohort)) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
final normalizedRollout = normalizeRolloutCohortCount(rolloutCohortCount);
|
|
193
|
+
if (normalizedRollout <= 0) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (normalizedCohort == null) {
|
|
197
|
+
return normalizedRollout >= numericCohortSize;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
final numericCohort = getNumericCohortValue(normalizedCohort);
|
|
201
|
+
if (numericCohort == null) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (normalizedRollout >= numericCohortSize) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return getNumericCohortRolloutPosition(bundleId, numericCohort) <
|
|
208
|
+
normalizedRollout;
|
|
209
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import 'semver_range.dart' show satisfies;
|
|
2
|
+
import 'semver_version.dart';
|
|
3
|
+
|
|
4
|
+
export 'semver_range.dart' show satisfies;
|
|
5
|
+
export 'semver_version.dart' show SemVer;
|
|
6
|
+
|
|
7
|
+
/// hot-updater `semverSatisfies(targetAppVersion, currentVersion)`
|
|
8
|
+
/// (plugins/plugin-core/src/semverSatisfies.ts):
|
|
9
|
+
///
|
|
10
|
+
/// 1. `coerce(currentVersion)` — extract a version from loose input
|
|
11
|
+
/// ("v1.2", "version 2.3.4", "1a2b3c" → 1.0.0, junk → null).
|
|
12
|
+
/// 2. `satisfies(coerced, range)` against the raw target range.
|
|
13
|
+
bool semverSatisfies(String targetAppVersion, String currentVersion) {
|
|
14
|
+
final coerced = semverCoerce(currentVersion);
|
|
15
|
+
if (coerced == null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return satisfies(coerced, targetAppVersion);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// verkit `coerce` with default options: find the first plausible
|
|
22
|
+
/// major[.minor[.patch]] run; prerelease/build are dropped; anything after
|
|
23
|
+
/// the run (including stray dots/digits like "1.2.3.4") is ignored.
|
|
24
|
+
SemVer? semverCoerce(String value) {
|
|
25
|
+
var input = value.trim();
|
|
26
|
+
if (input.isEmpty) return null;
|
|
27
|
+
final m = RegExp(
|
|
28
|
+
r'(?<![0-9.])'
|
|
29
|
+
r'(0|[1-9]\d*)'
|
|
30
|
+
r'(?:\.(0|[1-9]\d*))?'
|
|
31
|
+
r'(?:\.(0|[1-9]\d*))?',
|
|
32
|
+
).firstMatch(input);
|
|
33
|
+
if (m == null) return null;
|
|
34
|
+
|
|
35
|
+
// Overflow guard mirroring verkit: a digit run too large for a safe
|
|
36
|
+
// integer is skipped and scanning resumes right after it, allowing a
|
|
37
|
+
// leading dot. e.g. "99999999999999999999.1.1" -> "1.1.0".
|
|
38
|
+
var majorStr = m.group(1)!;
|
|
39
|
+
if (majorStr.length > 15) {
|
|
40
|
+
return semverCoerceLoose(input.substring(m.start + majorStr.length));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return SemVer(
|
|
44
|
+
int.parse(majorStr),
|
|
45
|
+
int.tryParse(m.group(2) ?? '') ?? 0,
|
|
46
|
+
int.tryParse(m.group(3) ?? '') ?? 0,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Retry scan that allows a preceding dot (used after skipping an
|
|
51
|
+
/// oversized digit run).
|
|
52
|
+
SemVer? semverCoerceLoose(String input) {
|
|
53
|
+
var s = input.trim();
|
|
54
|
+
if (s.isEmpty) return null;
|
|
55
|
+
final m = RegExp(
|
|
56
|
+
r'(0|[1-9]\d*)'
|
|
57
|
+
r'(?:\.(0|[1-9]\d*))?'
|
|
58
|
+
r'(?:\.(0|[1-9]\d*))?',
|
|
59
|
+
).firstMatch(s);
|
|
60
|
+
if (m == null) return null;
|
|
61
|
+
var majorStr = m.group(1)!;
|
|
62
|
+
if (majorStr.length > 15) {
|
|
63
|
+
return semverCoerceLoose(s.substring(m.start + majorStr.length));
|
|
64
|
+
}
|
|
65
|
+
return SemVer(
|
|
66
|
+
int.parse(majorStr),
|
|
67
|
+
int.tryParse(m.group(2) ?? '') ?? 0,
|
|
68
|
+
int.tryParse(m.group(3) ?? '') ?? 0,
|
|
69
|
+
);
|
|
70
|
+
}
|