@_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.
Files changed (167) hide show
  1. package/bin/flutter-patcher.js +48 -39
  2. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  3. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  4. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  5. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  6. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  7. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  8. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  21. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  22. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  23. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  24. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  25. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  30. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  31. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  38. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  39. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  40. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  41. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  42. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  43. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  44. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  45. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  46. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  47. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  48. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  49. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  50. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  51. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  52. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  53. package/dart-src/packages/core/lib/src/status.dart +24 -0
  54. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  55. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  56. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  57. package/dart-src/packages/core/pubspec.yaml +9 -0
  58. package/dart-src/packages/core/tool/dbg.dart +6 -0
  59. package/dart-src/plugins/aws/.gitkeep +0 -0
  60. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  61. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  62. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  63. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  64. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  65. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  66. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  67. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  68. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  69. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  70. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  71. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  72. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  73. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  74. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  75. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  76. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  77. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  78. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  79. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  81. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  82. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  83. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  84. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  85. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  86. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  87. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  88. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  89. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  90. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  91. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  92. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  93. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  94. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  95. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  96. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  97. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  98. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  99. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  100. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  101. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  102. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  103. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  104. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  105. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  106. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  107. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  108. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  109. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  110. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  111. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  112. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  113. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  114. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  115. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  116. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  117. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  118. package/dart-src/plugins/postgres/.gitkeep +0 -0
  119. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  120. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  121. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  122. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  123. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  124. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  125. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  126. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  127. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  128. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  129. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  130. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  131. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  132. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  133. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  134. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  135. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  136. package/dart-src/plugins/standalone/.gitkeep +0 -0
  137. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  138. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  139. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  140. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  141. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  142. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  143. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  144. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  145. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  146. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  147. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  148. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  149. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  150. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  151. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  152. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  153. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  154. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  155. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  156. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  157. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  158. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  159. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  160. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  161. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  162. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  163. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  164. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  165. package/package.json +2 -1
  166. package/scripts/postinstall.js +53 -25
  167. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,207 @@
1
+ import 'dart:convert' show utf8;
2
+ import 'dart:typed_data' show Uint8List;
3
+
4
+ import 'package:crypto/crypto.dart' show Hmac, sha256;
5
+ import 'package:http/http.dart' show Client, Request, Response;
6
+
7
+ const String _cloudfrontService = 'cloudfront';
8
+ const String _cloudfrontRegion = 'us-east-1';
9
+ const String _cloudfrontApiBase = 'https://cloudfront.amazonaws.com';
10
+
11
+ /// Configuration for the CloudFront invalidation client.
12
+ class AwsCloudFrontConfig {
13
+ const AwsCloudFrontConfig({
14
+ required this.accessKeyId,
15
+ required this.secretAccessKey,
16
+ this.sessionToken,
17
+ });
18
+
19
+ final String accessKeyId;
20
+ final String secretAccessKey;
21
+ final String? sessionToken;
22
+ }
23
+
24
+ /// CloudFront cache-invalidation client (mirrors the subset of
25
+ /// `@aws-sdk/client-cloudfront` used by hot-updater's `s3Database.ts`).
26
+ abstract class AwsCloudFrontClientLike {
27
+ const AwsCloudFrontClientLike();
28
+
29
+ Future<void> createInvalidation(
30
+ String distributionId,
31
+ List<String> paths, {
32
+ bool shouldWait = false,
33
+ });
34
+ }
35
+
36
+ /// Real CloudFront client backed by `package:http` + AWS SigV4 signing
37
+ /// (service `cloudfront`, global region `us-east-1`).
38
+ class AwsCloudFrontClient implements AwsCloudFrontClientLike {
39
+ AwsCloudFrontClient(this.config, {Client? http}) : _http = http ?? Client();
40
+
41
+ final AwsCloudFrontConfig config;
42
+ final Client _http;
43
+
44
+ static const Duration _pollInterval = Duration(seconds: 2);
45
+ static const Duration _timeout = Duration(minutes: 5);
46
+
47
+ @override
48
+ Future<void> createInvalidation(
49
+ String distributionId,
50
+ List<String> paths, {
51
+ bool shouldWait = false,
52
+ }) async {
53
+ if (paths.isEmpty) return;
54
+
55
+ final timestamp = DateTime.now();
56
+ final callerReference = 'invalidation-${timestamp.millisecondsSinceEpoch}';
57
+ final body = _buildInvalidationXml(callerReference, paths);
58
+ final uri = Uri.parse(
59
+ '$_cloudfrontApiBase/2020-05-31/distribution/$distributionId/invalidation',
60
+ );
61
+
62
+ try {
63
+ final response = await _send('POST', uri, utf8.encode(body));
64
+ _assertStatus(response, [200, 201], 'CreateInvalidation');
65
+
66
+ if (!shouldWait) return;
67
+
68
+ final invalidationId = _extractInvalidationId(response.body);
69
+ if (invalidationId == null) {
70
+ throw StateError('CloudFront invalidation response missing Id');
71
+ }
72
+ if (response.body.contains('<Status>Completed</Status>')) return;
73
+
74
+ final deadline = DateTime.now().add(_timeout);
75
+ while (DateTime.now().isBefore(deadline)) {
76
+ await Future.delayed(_pollInterval);
77
+ final statusResp = await _send(
78
+ 'GET',
79
+ Uri.parse(
80
+ '$_cloudfrontApiBase/2020-05-31/distribution/$distributionId/'
81
+ 'invalidation/$invalidationId',
82
+ ),
83
+ null,
84
+ );
85
+ if (statusResp.body.contains('<Status>Completed</Status>')) return;
86
+ }
87
+ throw StateError(
88
+ 'Timed out waiting for CloudFront invalidation $invalidationId',
89
+ );
90
+ } catch (error) {
91
+ if (shouldWait) rethrow;
92
+ // When not waiting, surface invalidation failures as warnings and
93
+ // continue (mirrors hot-updater's s3Database behaviour).
94
+ // ignore: avoid_print
95
+ print(
96
+ '[flutter_patcher/aws] CloudFront invalidation failed for '
97
+ '$distributionId; continuing without cache invalidation: $error',
98
+ );
99
+ }
100
+ }
101
+
102
+ String _buildInvalidationXml(String callerReference, List<String> paths) {
103
+ final items = paths
104
+ .map((p) => ' <Path>${_xmlEscape(p)}</Path>')
105
+ .join('\n');
106
+ return '<?xml version="1.0" encoding="UTF-8"?>\n'
107
+ '<InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">\n'
108
+ ' <CallerReference>$callerReference</CallerReference>\n'
109
+ ' <Paths>\n'
110
+ ' <Quantity>${paths.length}</Quantity>\n'
111
+ ' <Items>\n$items\n </Items>\n'
112
+ ' </Paths>\n'
113
+ '</InvalidationBatch>';
114
+ }
115
+
116
+ String? _extractInvalidationId(String xml) {
117
+ final m = RegExp(r'<Id>([^<]+)</Id>').firstMatch(xml);
118
+ return m?.group(1);
119
+ }
120
+
121
+ Map<String, String> _authHeaders(
122
+ String method,
123
+ Uri uri,
124
+ List<int>? body,
125
+ ) {
126
+ final now = DateTime.now().toUtc();
127
+ final amzDate = _amzDate(now);
128
+ final dateStamp = amzDate.substring(0, 8);
129
+ final scope =
130
+ '$dateStamp/$_cloudfrontRegion/$_cloudfrontService/aws4_request';
131
+ final payloadHash = body == null ? _hashHex(const []) : _hashHex(body);
132
+
133
+ final canonicalHeaders = 'host:${uri.host}\n'
134
+ 'x-amz-content-sha256:$payloadHash\n'
135
+ 'x-amz-date:$amzDate\n'
136
+ '${config.sessionToken != null ? 'x-amz-security-token:${config.sessionToken}\n' : ''}';
137
+ final signedHeaders = config.sessionToken != null
138
+ ? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
139
+ : 'host;x-amz-content-sha256;x-amz-date';
140
+
141
+ final canonicalRequest = '$method\n${uri.path}\n'
142
+ '\n$canonicalHeaders$signedHeaders\n$payloadHash';
143
+ final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
144
+ final signingKey = _signingKey(dateStamp);
145
+ final signature = _toHex(_hmac(signingKey, stringToSign));
146
+
147
+ final headers = {
148
+ 'x-amz-date': amzDate,
149
+ 'x-amz-content-sha256': payloadHash,
150
+ 'Authorization':
151
+ 'AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/$scope, '
152
+ 'SignedHeaders=$signedHeaders, Signature=$signature',
153
+ };
154
+ if (config.sessionToken != null) {
155
+ headers['x-amz-security-token'] = config.sessionToken!;
156
+ }
157
+ return headers;
158
+ }
159
+
160
+ Future<Response> _send(String method, Uri uri, List<int>? body) async {
161
+ final request = Request(method, uri);
162
+ request.headers.addAll(_authHeaders(method, uri, body));
163
+ request.headers['Content-Type'] = 'application/xml';
164
+ if (body != null) request.bodyBytes = body;
165
+ final streamed = await _http.send(request);
166
+ return await Response.fromStream(streamed);
167
+ }
168
+
169
+ List<int> _signingKey(String dateStamp) {
170
+ var key = utf8.encode('AWS4${config.secretAccessKey}');
171
+ key = _hmac(key, dateStamp);
172
+ key = _hmac(key, _cloudfrontRegion);
173
+ key = _hmac(key, _cloudfrontService);
174
+ key = _hmac(key, 'aws4_request');
175
+ return key;
176
+ }
177
+
178
+ String _stringToSign(String amzDate, String scope, String canonicalRequest) =>
179
+ 'AWS4-HMAC-SHA256\n$amzDate\n$scope\n${_hashHex(utf8.encode(canonicalRequest))}';
180
+
181
+ static String _amzDate(DateTime utc) =>
182
+ '${utc.toIso8601String().replaceAll('-', '').replaceAll(':', '').split('.').first}Z';
183
+
184
+ static String _xmlEscape(String value) =>
185
+ value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
186
+
187
+ static Uint8List _hmac(List<int> key, String data) =>
188
+ Hmac(sha256, key).convert(utf8.encode(data)).bytes as Uint8List;
189
+
190
+ static String _hashHex(List<int> data) =>
191
+ _toHex(sha256.convert(data).bytes);
192
+
193
+ static String _toHex(List<int> bytes) =>
194
+ bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
195
+
196
+ static void _assertStatus(
197
+ Response response,
198
+ List<int> ok,
199
+ String operation,
200
+ ) {
201
+ if (!ok.contains(response.statusCode)) {
202
+ throw Exception(
203
+ 'CloudFront $operation failed (${response.statusCode}): ${response.body}',
204
+ );
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,130 @@
1
+ import 'dart:convert' show base64Decode, base64Encode, jsonEncode, utf8;
2
+ import 'dart:typed_data' show Uint8List;
3
+
4
+ import 'package:asn1lib/asn1lib.dart';
5
+ import 'package:pointycastle/export.dart'
6
+ show
7
+ PrivateKeyParameter,
8
+ PublicKeyParameter,
9
+ RSAPrivateKey,
10
+ RSAPublicKey,
11
+ RSASignature,
12
+ RSASigner,
13
+ SHA1Digest;
14
+
15
+ /// ASN.1 DigestInfo prefix for SHA1, used by pointycastle's PKCS#1 v1.5 signer.
16
+ const String _sha1DigestIdentifierHex = '3021300906052b0e03021a05000414';
17
+
18
+ String _base64UrlNoPad(List<int> bytes) =>
19
+ base64Encode(bytes).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
20
+
21
+ /// Parse a PEM-encoded RSA private key (PKCS#1 `RSA PRIVATE KEY` or PKCS#8
22
+ /// `PRIVATE KEY`) into a [RSAPrivateKey].
23
+ RSAPrivateKey parseRsaPrivateKeyPem(String pem) {
24
+ final lines = pem
25
+ .split('\n')
26
+ .where((l) => l.isNotEmpty && !l.startsWith('-----'))
27
+ .join('');
28
+ final der = base64DecodeToBytes(lines);
29
+ final seq = ASN1Parser(der).nextObject() as ASN1Sequence;
30
+
31
+ late final ASN1Sequence keySeq;
32
+ if (seq.elements.length == 3) {
33
+ // PKCS#8: SEQUENCE { version, AlgorithmIdentifier, BIT STRING }
34
+ final bitString = seq.elements[2] as ASN1BitString;
35
+ keySeq = ASN1Parser(bitString.contentBytes()).nextObject() as ASN1Sequence;
36
+ } else {
37
+ // PKCS#1: RSAPrivateKey ::= SEQUENCE { version, n, e, d, p, q, dP, dQ, qInv }
38
+ keySeq = seq;
39
+ }
40
+
41
+ final modulus = (keySeq.elements[1] as ASN1Integer).valueAsBigInteger;
42
+ final privateExponent = (keySeq.elements[3] as ASN1Integer).valueAsBigInteger;
43
+ final p = (keySeq.elements[4] as ASN1Integer).valueAsBigInteger;
44
+ final q = (keySeq.elements[5] as ASN1Integer).valueAsBigInteger;
45
+ return RSAPrivateKey(modulus, privateExponent, p, q);
46
+ }
47
+
48
+ Uint8List base64DecodeToBytes(String s) => _base64Decode(s);
49
+
50
+ Uint8List _base64Decode(String s) {
51
+ final normalized = s.replaceAll('-', '+').replaceAll('_', '/');
52
+ final padded = normalized.padRight(
53
+ normalized.length + (4 - normalized.length % 4) % 4,
54
+ '=',
55
+ );
56
+ return base64Decode(padded);
57
+ }
58
+
59
+ /// Build a CloudFront signed URL (custom policy, RSA-SHA1) for [url].
60
+ ///
61
+ /// Faithful port of `@aws-sdk/cloudfront-signer`'s `getSignedUrl`.
62
+ String cloudfrontSignedUrl({
63
+ required String url,
64
+ required String keyPairId,
65
+ required String privateKeyPem,
66
+ required DateTime dateLessThan,
67
+ }) {
68
+ final privateKey = parseRsaPrivateKeyPem(privateKeyPem);
69
+
70
+ final policy = jsonEncode({
71
+ 'Statement': [
72
+ {
73
+ 'Resource': url,
74
+ 'Condition': {
75
+ 'DateLessThan': {
76
+ 'AWS:EpochTime': dateLessThan.millisecondsSinceEpoch ~/ 1000,
77
+ },
78
+ },
79
+ },
80
+ ],
81
+ });
82
+ final policyBytes = utf8.encode(policy);
83
+ final policyB64 = _base64UrlNoPad(policyBytes);
84
+
85
+ final signer = RSASigner(SHA1Digest(), _sha1DigestIdentifierHex);
86
+ signer.init(true, PrivateKeyParameter<RSAPrivateKey>(privateKey));
87
+ final signature = signer.generateSignature(Uint8List.fromList(policyBytes)).bytes;
88
+ final signatureB64 = _base64UrlNoPad(signature);
89
+
90
+ return '$url?Policy=$policyB64&Signature=$signatureB64'
91
+ '&Key-Pair-Id=$keyPairId';
92
+ }
93
+
94
+ /// Verify a CloudFront signed URL's signature against [publicKeyPem] (test seam).
95
+ bool verifyCloudfrontSignedUrl({
96
+ required String signedUrl,
97
+ required String publicKeyPem,
98
+ }) {
99
+ final uri = Uri.parse(signedUrl);
100
+ final policy = uri.queryParameters['Policy'];
101
+ final signature = uri.queryParameters['Signature'];
102
+ if (policy == null || signature == null) return false;
103
+
104
+ final policyBytes = _base64Decode(policy);
105
+ final sigBytes = _base64Decode(signature);
106
+
107
+ final pub = parseRsaPublicKeyPem(publicKeyPem);
108
+ final signer = RSASigner(SHA1Digest(), _sha1DigestIdentifierHex);
109
+ signer.init(false, PublicKeyParameter<RSAPublicKey>(pub));
110
+ return signer.verifySignature(
111
+ Uint8List.fromList(policyBytes),
112
+ RSASignature(Uint8List.fromList(sigBytes)),
113
+ );
114
+ }
115
+
116
+ /// Parse a PEM-encoded RSA public key into [RSAPublicKey].
117
+ RSAPublicKey parseRsaPublicKeyPem(String pem) {
118
+ final lines = pem
119
+ .split('\n')
120
+ .where((l) => l.isNotEmpty && !l.startsWith('-----'))
121
+ .join('');
122
+ final der = base64DecodeToBytes(lines);
123
+ final seq = ASN1Parser(der).nextObject() as ASN1Sequence;
124
+ // SubjectPublicKeyInfo: SEQUENCE { AlgorithmIdentifier, BIT STRING }
125
+ final bitString = seq.elements[1] as ASN1BitString;
126
+ final key = ASN1Parser(bitString.contentBytes()).nextObject() as ASN1Sequence;
127
+ final modulus = (key.elements[0] as ASN1Integer).valueAsBigInteger;
128
+ final exponent = (key.elements[1] as ASN1Integer).valueAsBigInteger;
129
+ return RSAPublicKey(modulus, exponent);
130
+ }
@@ -0,0 +1,59 @@
1
+ import 'dart:io' show Platform;
2
+
3
+ import 'aws_s3_client.dart' show AwsS3Client, AwsS3ClientLike;
4
+
5
+ /// Configuration for the AWS S3 storage plugin.
6
+ ///
7
+ /// Faithful port of hot-updater `S3StorageConfig` (extends the AWS SDK
8
+ /// `S3ClientConfig` with `bucketName` and `basePath`).
9
+ class AwsS3StorageConfig {
10
+ AwsS3StorageConfig({
11
+ required this.bucketName,
12
+ required this.region,
13
+ required this.accessKeyId,
14
+ required this.secretAccessKey,
15
+ this.basePath,
16
+ this.endpoint,
17
+ this.sessionToken,
18
+ this.clientFactory = defaultAwsS3ClientFactory,
19
+ });
20
+
21
+ final String bucketName;
22
+ final String region;
23
+ final String accessKeyId;
24
+ final String secretAccessKey;
25
+ final String? basePath;
26
+ final String? endpoint;
27
+ final String? sessionToken;
28
+
29
+ /// Test seam: override the concrete S3 client.
30
+ final AwsS3ClientLike Function(AwsS3StorageConfig config) clientFactory;
31
+ }
32
+
33
+ /// Default factory producing a real [AwsS3Client] with runtime region applied.
34
+ AwsS3ClientLike defaultAwsS3ClientFactory(AwsS3StorageConfig config) =>
35
+ AwsS3Client(applyS3RuntimeAwsConfig(config));
36
+
37
+ /// Fill in a default region from the environment when one is not supplied
38
+ /// (mirrors hot-updater's `applyS3RuntimeAwsConfig`).
39
+ AwsS3StorageConfig applyS3RuntimeAwsConfig(AwsS3StorageConfig config) {
40
+ if (config.region.isNotEmpty) return config;
41
+ final envRegion =
42
+ Platform.environment['AWS_REGION'] ??
43
+ Platform.environment['AWS_DEFAULT_REGION'];
44
+ if (envRegion == null || envRegion.isEmpty) return config;
45
+ return AwsS3StorageConfig(
46
+ bucketName: config.bucketName,
47
+ region: envRegion,
48
+ accessKeyId: config.accessKeyId,
49
+ secretAccessKey: config.secretAccessKey,
50
+ basePath: config.basePath,
51
+ endpoint: config.endpoint,
52
+ sessionToken: config.sessionToken,
53
+ clientFactory: config.clientFactory,
54
+ );
55
+ }
56
+
57
+ /// Resolve the concrete S3 client from config (seam for tests).
58
+ AwsS3ClientLike resolveAwsS3Client(AwsS3StorageConfig config) =>
59
+ config.clientFactory(config);
@@ -0,0 +1,198 @@
1
+ import 'dart:convert' show jsonDecode, jsonEncode, utf8;
2
+
3
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
4
+ show BlobOperations, createBlobDatabasePlugin;
5
+
6
+ import 'aws_cloudfront_client.dart'
7
+ show AwsCloudFrontClient, AwsCloudFrontClientLike, AwsCloudFrontConfig;
8
+ import 'aws_config.dart' show AwsS3StorageConfig, applyS3RuntimeAwsConfig;
9
+ import 'aws_s3_client.dart' show AwsS3Client, AwsS3ClientLike;
10
+
11
+ /// Thrown when an S3 object is archived (Glacier) and cannot be read.
12
+ class S3ArchivedObjectError implements Exception {
13
+ S3ArchivedObjectError({required this.key, required this.cause});
14
+
15
+ final String key;
16
+ final Object cause;
17
+
18
+ @override
19
+ String toString() =>
20
+ 'S3ArchivedObjectError: object "$key" is archived and cannot be read.';
21
+ }
22
+
23
+ /// Build `toStorageKey` / `fromStorageKey` transformers for a base path.
24
+ /// Faithful port of hot-updater `createDatabaseKeyBuilder.ts`.
25
+ (String Function(String), String Function(String)) createDatabaseKeyBuilder(
26
+ String? basePath,
27
+ ) {
28
+ final normalized = basePath?.replaceAll(RegExp(r'^/+|/+$'), '') ?? '';
29
+ String toStorageKey(String key) =>
30
+ [normalized, key].where((s) => s.isNotEmpty).join('/');
31
+ String fromStorageKey(String key) {
32
+ if (normalized.isEmpty) return key;
33
+ final prefix = '$normalized/';
34
+ return key.startsWith(prefix) ? key.substring(prefix.length) : key;
35
+ }
36
+
37
+ return (toStorageKey, fromStorageKey);
38
+ }
39
+
40
+ /// Configuration for the AWS S3 blob database plugin.
41
+ class S3DatabaseConfig {
42
+ S3DatabaseConfig({
43
+ required this.bucketName,
44
+ required this.region,
45
+ required this.accessKeyId,
46
+ required this.secretAccessKey,
47
+ this.basePath,
48
+ this.cloudfrontDistributionId,
49
+ this.shouldWaitForInvalidation = false,
50
+ this.apiBasePath = '/api/check-update',
51
+ this.endpoint,
52
+ this.sessionToken,
53
+ this.clientFactory = defaultAwsS3DatabaseClientFactory,
54
+ this.cloudfrontClientFactory = defaultAwsCloudFrontClientFactory,
55
+ });
56
+
57
+ final String bucketName;
58
+ final String region;
59
+ final String accessKeyId;
60
+ final String secretAccessKey;
61
+ final String? basePath;
62
+ final String? cloudfrontDistributionId;
63
+ final bool shouldWaitForInvalidation;
64
+ final String apiBasePath;
65
+ final String? endpoint;
66
+ final String? sessionToken;
67
+
68
+ /// Test seam: override the concrete S3 client.
69
+ final AwsS3ClientLike Function(S3DatabaseConfig config) clientFactory;
70
+
71
+ /// Test seam: override the concrete CloudFront client.
72
+ final AwsCloudFrontClientLike Function(S3DatabaseConfig config)
73
+ cloudfrontClientFactory;
74
+ }
75
+
76
+ /// Default factory producing a real [AwsS3Client] with runtime region applied.
77
+ AwsS3ClientLike defaultAwsS3DatabaseClientFactory(S3DatabaseConfig config) =>
78
+ AwsS3Client(
79
+ applyS3RuntimeAwsConfig(
80
+ AwsS3StorageConfig(
81
+ bucketName: config.bucketName,
82
+ region: config.region,
83
+ accessKeyId: config.accessKeyId,
84
+ secretAccessKey: config.secretAccessKey,
85
+ basePath: config.basePath,
86
+ endpoint: config.endpoint,
87
+ sessionToken: config.sessionToken,
88
+ ),
89
+ ),
90
+ );
91
+
92
+ /// Default factory producing a real [AwsCloudFrontClient].
93
+ AwsCloudFrontClientLike defaultAwsCloudFrontClientFactory(
94
+ S3DatabaseConfig config,
95
+ ) =>
96
+ AwsCloudFrontClient(
97
+ AwsCloudFrontConfig(
98
+ accessKeyId: config.accessKeyId,
99
+ secretAccessKey: config.secretAccessKey,
100
+ sessionToken: config.sessionToken,
101
+ ),
102
+ );
103
+
104
+ bool _isUpdateJsonKey(String key) =>
105
+ RegExp(r'^[^/]+/(?:ios|android)/[^/]+/update\.json$').hasMatch(key);
106
+
107
+ /// S3-backed [BlobOperations] for the AWS `s3Database` plugin.
108
+ class S3BlobOperations implements BlobOperations {
109
+ S3BlobOperations(this.config)
110
+ : _client = config.clientFactory(config),
111
+ _cloudfront = config.cloudfrontDistributionId != null
112
+ ? config.cloudfrontClientFactory(config)
113
+ : null {
114
+ final keyBuilder = createDatabaseKeyBuilder(config.basePath);
115
+ _toStorageKey = keyBuilder.$1;
116
+ _fromStorageKey = keyBuilder.$2;
117
+ }
118
+
119
+ final S3DatabaseConfig config;
120
+ final AwsS3ClientLike _client;
121
+ final AwsCloudFrontClientLike? _cloudfront;
122
+ late final String Function(String) _toStorageKey;
123
+ late final String Function(String) _fromStorageKey;
124
+
125
+ @override
126
+ String get apiBasePath => config.apiBasePath;
127
+
128
+ @override
129
+ Future<List<String>> listObjects(String prefix) async {
130
+ final objects = await _client.listObjects(_toStorageKey(prefix));
131
+ return objects
132
+ .map((o) => _fromStorageKey(o.key))
133
+ .where(_isUpdateJsonKey)
134
+ .toList();
135
+ }
136
+
137
+ @override
138
+ Future<T?> loadObject<T>(String key) async {
139
+ final fullKey = _toStorageKey(key);
140
+ try {
141
+ final body = await _client.getObjectAsString(fullKey);
142
+ return jsonDecode(body) as T?;
143
+ } on Exception catch (e) {
144
+ if (e.toString().contains('NoSuchKey')) return null;
145
+ if (e.toString().contains('InvalidObjectState') ||
146
+ e.toString().contains('Archived')) {
147
+ throw S3ArchivedObjectError(key: fullKey, cause: e);
148
+ }
149
+ rethrow;
150
+ }
151
+ }
152
+
153
+ @override
154
+ Future<void> uploadObject<T>(String key, T data) async {
155
+ await _client.putObject(
156
+ _toStorageKey(key),
157
+ utf8.encode(jsonEncode(data)),
158
+ 'application/json',
159
+ );
160
+ }
161
+
162
+ @override
163
+ Future<void> deleteObject(String key) async {
164
+ await _client.deleteObject(_toStorageKey(key));
165
+ }
166
+
167
+ @override
168
+ bool shouldSkipLoadObjectError(Object error, String key) =>
169
+ error is S3ArchivedObjectError;
170
+
171
+ @override
172
+ void validateChannel(String channel) {
173
+ // hot-updater's s3Database leaves channel validation optional (no-op).
174
+ }
175
+
176
+ @override
177
+ Future<void> invalidatePaths(List<String> paths) async {
178
+ final distributionId = config.cloudfrontDistributionId;
179
+ if (_cloudfront == null || distributionId == null || paths.isEmpty) {
180
+ return;
181
+ }
182
+ await _cloudfront.createInvalidation(
183
+ distributionId,
184
+ paths,
185
+ shouldWait: config.shouldWaitForInvalidation,
186
+ );
187
+ }
188
+ }
189
+
190
+ /// AWS S3 blob database plugin (faithful port of hot-updater `s3Database.ts`).
191
+ ///
192
+ /// Stores bundles as JSON (`update.json`) under
193
+ /// `channel/platform/targetAppVersion/` paths and serves them via
194
+ /// `createBlobDatabasePlugin`, invalidating CloudFront paths on writes.
195
+ final s3Database = createBlobDatabasePlugin<S3DatabaseConfig>(
196
+ name: 's3Database',
197
+ blobFactory: (config) => S3BlobOperations(config),
198
+ );
@@ -0,0 +1,72 @@
1
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
2
+ show StoragePlugin, StoragePluginHooks;
3
+
4
+ import 'aws_config.dart' show AwsS3StorageConfig;
5
+ import 'aws_storage.dart' show s3Storage;
6
+ import 'with_cloudfront_signed_url.dart'
7
+ show WithCloudFrontSignedUrlOptions, withCloudFrontSignedUrl;
8
+
9
+ /// Configuration for the AWS Lambda@Edge S3 storage plugin.
10
+ ///
11
+ /// Combines `S3StorageConfig` (credentials/bucket) with the CloudFront signed
12
+ /// URL options. Faithful port of hot-updater `AwsLambdaEdgeStorageConfig`.
13
+ class AwsLambdaEdgeStorageConfig extends AwsS3StorageConfig {
14
+ AwsLambdaEdgeStorageConfig({
15
+ required super.bucketName,
16
+ required super.region,
17
+ required super.accessKeyId,
18
+ required super.secretAccessKey,
19
+ super.basePath,
20
+ super.endpoint,
21
+ super.sessionToken,
22
+ super.clientFactory,
23
+ required this.keyPairId,
24
+ this.publicBaseUrl,
25
+ this.publicBaseUrlResolver,
26
+ this.getPrivateKey,
27
+ this.ssmParameterName,
28
+ this.ssmRegion,
29
+ this.expiresSeconds,
30
+ });
31
+
32
+ final String keyPairId;
33
+ final String? publicBaseUrl;
34
+ final Future<String> Function()? publicBaseUrlResolver;
35
+ final Future<String> Function()? getPrivateKey;
36
+ final String? ssmParameterName;
37
+ final String? ssmRegion;
38
+ final int? expiresSeconds;
39
+
40
+ WithCloudFrontSignedUrlOptions toSignedUrlOptions() =>
41
+ WithCloudFrontSignedUrlOptions(
42
+ keyPairId: keyPairId,
43
+ publicBaseUrl: publicBaseUrl,
44
+ publicBaseUrlResolver: publicBaseUrlResolver,
45
+ getPrivateKey: getPrivateKey,
46
+ ssmParameterName: ssmParameterName,
47
+ ssmRegion: ssmRegion,
48
+ expiresSeconds: expiresSeconds,
49
+ accessKeyId: accessKeyId,
50
+ secretAccessKey: secretAccessKey,
51
+ sessionToken: sessionToken,
52
+ );
53
+ }
54
+
55
+ /// AWS Lambda@Edge S3 storage plugin: S3 storage whose `s3://` download URLs
56
+ /// are rewritten as CloudFront signed URLs. Faithful port of
57
+ /// `s3LambdaEdgeStorage.ts` / `awsLambdaEdgeStorage.ts`.
58
+ StoragePlugin s3LambdaEdgeStorage(
59
+ AwsLambdaEdgeStorageConfig config, [
60
+ StoragePluginHooks? hooks,
61
+ ]) =>
62
+ withCloudFrontSignedUrl(
63
+ () => s3Storage(config, hooks),
64
+ config.toSignedUrlOptions(),
65
+ )();
66
+
67
+ /// Alias matching hot-updater's `awsLambdaEdgeStorage` export.
68
+ StoragePlugin awsLambdaEdgeStorage(
69
+ AwsLambdaEdgeStorageConfig config, [
70
+ StoragePluginHooks? hooks,
71
+ ]) =>
72
+ s3LambdaEdgeStorage(config, hooks);