@_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,312 @@
1
+ import 'dart:convert' show utf8;
2
+ import 'dart:io' show File;
3
+ import 'dart:typed_data' show Uint8List;
4
+
5
+ import 'package:crypto/crypto.dart' show Hmac, sha256;
6
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
7
+ show StorageObject;
8
+ import 'package:http/http.dart' show Client, Request, Response;
9
+
10
+ import 'aws_config.dart' show AwsS3StorageConfig;
11
+
12
+ /// A thin S3 client for AWS S3.
13
+ ///
14
+ /// Mirrors the subset of `@aws-sdk/client-s3` used by hot-updater's
15
+ /// `s3Storage.ts`: delete / put / head / get objects, listing, multi-delete,
16
+ /// and presigned GET URLs. Requests are signed with AWS Signature V4
17
+ /// (service `s3`, virtual-hosted addressing).
18
+ abstract class AwsS3ClientLike {
19
+ const AwsS3ClientLike();
20
+
21
+ Future<void> deleteObject(String key);
22
+
23
+ Future<void> deleteObjects(List<String> keys);
24
+
25
+ Future<void> putObject(String key, List<int> body, String contentType);
26
+
27
+ Future<bool> headObject(String key);
28
+
29
+ Future<List<int>> getObjectAsBytes(String key);
30
+
31
+ Future<String> getObjectAsString(String key);
32
+
33
+ Future<String> getPresignedUrl(String key, {int expiresIn = 3600});
34
+
35
+ Future<List<StorageObject>> listObjects([String? prefix]);
36
+ }
37
+
38
+ /// Real AWS S3 client backed by `package:http` + AWS SigV4 signing.
39
+ class AwsS3Client implements AwsS3ClientLike {
40
+ AwsS3Client(this.config, {Client? http}) : _http = http ?? Client();
41
+
42
+ final AwsS3StorageConfig config;
43
+ final Client _http;
44
+
45
+ static const String _service = 's3';
46
+
47
+ Uri get _baseUri {
48
+ final host =
49
+ config.endpoint ??
50
+ 'https://${config.bucketName}.s3.${config.region}.amazonaws.com';
51
+ return Uri.parse(host);
52
+ }
53
+
54
+ Uri _objectUri(String key) => _baseUri.replace(path: '/$key');
55
+
56
+ @override
57
+ Future<void> deleteObject(String key) async {
58
+ final response = await _send('DELETE', _objectUri(key), body: null);
59
+ _assertStatus(response, [200, 204], 'deleteObject');
60
+ }
61
+
62
+ @override
63
+ Future<void> deleteObjects(List<String> keys) async {
64
+ for (var offset = 0; offset < keys.length; offset += 1000) {
65
+ final batch = keys.skip(offset).take(1000).toList();
66
+ if (batch.isEmpty) continue;
67
+
68
+ final xml = StringBuffer()
69
+ ..write('<?xml version="1.0" encoding="UTF-8"?>')
70
+ ..write('<Delete><Quiet>true</Quiet>');
71
+ for (final key in batch) {
72
+ xml.write('<Object><Key>${_xmlEscape(key)}</Key></Object>');
73
+ }
74
+ xml.write('</Delete>');
75
+
76
+ final response = await _send(
77
+ 'POST',
78
+ _baseUri.replace(path: '/', queryParameters: {'delete': ''}),
79
+ body: utf8.encode(xml.toString()),
80
+ extraHeaders: {'Content-Type': 'application/xml'},
81
+ );
82
+ _assertStatus(response, [200], 'deleteObjects');
83
+ }
84
+ }
85
+
86
+ @override
87
+ Future<void> putObject(
88
+ String key,
89
+ List<int> body,
90
+ String contentType,
91
+ ) async {
92
+ final response = await _send(
93
+ 'PUT',
94
+ _objectUri(key),
95
+ body: body,
96
+ extraHeaders: {
97
+ 'Content-Type': contentType,
98
+ 'Cache-Control': 'max-age=31536000',
99
+ },
100
+ );
101
+ _assertStatus(response, [200], 'putObject');
102
+ }
103
+
104
+ @override
105
+ Future<bool> headObject(String key) async {
106
+ final uri = _objectUri(key);
107
+ final request =
108
+ Request('HEAD', uri)..headers.addAll(_authHeaders('HEAD', uri, null));
109
+ final response = await _http.send(request);
110
+ if (response.statusCode == 404) return false;
111
+ if (response.statusCode < 200 || response.statusCode >= 300) {
112
+ throw Exception('S3 HeadObject failed (${response.statusCode})');
113
+ }
114
+ return true;
115
+ }
116
+
117
+ @override
118
+ Future<List<int>> getObjectAsBytes(String key) async {
119
+ final response = await _send('GET', _objectUri(key), body: null);
120
+ _assertStatus(response, [200], 'getObjectAsBytes');
121
+ return response.bodyBytes;
122
+ }
123
+
124
+ @override
125
+ Future<String> getObjectAsString(String key) async {
126
+ final bytes = await getObjectAsBytes(key);
127
+ return utf8.decode(bytes);
128
+ }
129
+
130
+ @override
131
+ Future<String> getPresignedUrl(String key, {int expiresIn = 3600}) async {
132
+ final uri = _objectUri(key);
133
+ final now = DateTime.now().toUtc();
134
+ final amzDate = _amzDate(now);
135
+ final dateStamp = amzDate.substring(0, 8);
136
+ final scope = '$dateStamp/${config.region}/$_service/aws4_request';
137
+
138
+ final tokenParam = config.sessionToken != null
139
+ ? {'X-Amz-Security-Token': _uriEncode(config.sessionToken!)}
140
+ : <String, String>{};
141
+
142
+ final params = {
143
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
144
+ 'X-Amz-Credential': '${config.accessKeyId}/$scope',
145
+ 'X-Amz-Date': amzDate,
146
+ 'X-Amz-Expires': '$expiresIn',
147
+ 'X-Amz-SignedHeaders': 'host',
148
+
149
+ ...tokenParam,
150
+ };
151
+ final canonicalQuery = params.entries
152
+ .map((e) => '${_uriEncode(e.key)}=${_uriEncode(e.value)}')
153
+ .toList()
154
+ ..sort();
155
+ final queryString = canonicalQuery.join('&');
156
+
157
+ const payloadHash = 'UNSIGNED-PAYLOAD';
158
+ final canonicalHeaders = 'host:${uri.host}\n';
159
+ final canonicalRequest =
160
+ 'GET\n${uri.path}\n$queryString\n$canonicalHeaders\nhost\n$payloadHash';
161
+ final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
162
+ final signingKey = _signingKey(dateStamp, config.region);
163
+ final signature = _toHex(_hmac(signingKey, stringToSign));
164
+ return '${uri.scheme}://${uri.host}${uri.path}?$queryString'
165
+ '&X-Amz-Signature=$signature';
166
+ }
167
+
168
+ @override
169
+ Future<List<StorageObject>> listObjects([String? prefix]) async {
170
+ final listUri = _baseUri.replace(
171
+ path: '/',
172
+ queryParameters: {
173
+ 'list-type': '2',
174
+ if (prefix != null && prefix.isNotEmpty) 'prefix': prefix,
175
+ },
176
+ );
177
+ final response = await _send('GET', listUri, body: null);
178
+ _assertStatus(response, [200], 'listObjects');
179
+ return _parseListObjects(response.body);
180
+ }
181
+
182
+ // --- signing helpers -------------------------------------------------------
183
+
184
+ Map<String, String> _authHeaders(String method, Uri uri, List<int>? body) {
185
+ final now = DateTime.now().toUtc();
186
+ final amzDate = _amzDate(now);
187
+ final dateStamp = amzDate.substring(0, 8);
188
+ final scope = '$dateStamp/${config.region}/$_service/aws4_request';
189
+ final payloadHash = body == null ? _hashHex(const []) : _hashHex(body);
190
+
191
+ final canonicalHeaders = 'host:${uri.host}\n'
192
+ 'x-amz-content-sha256:$payloadHash\n'
193
+ 'x-amz-date:$amzDate\n'
194
+ '${config.sessionToken != null ? 'x-amz-security-token:${config.sessionToken}\n' : ''}';
195
+ final signedHeaders = config.sessionToken != null
196
+ ? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
197
+ : 'host;x-amz-content-sha256;x-amz-date';
198
+
199
+ final canonicalRequest = '$method\n${uri.path}\n'
200
+ '\n$canonicalHeaders$signedHeaders\n$payloadHash';
201
+ final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
202
+ final signingKey = _signingKey(dateStamp, config.region);
203
+ final signature = _toHex(_hmac(signingKey, stringToSign));
204
+
205
+ final headers = {
206
+ 'x-amz-date': amzDate,
207
+ 'x-amz-content-sha256': payloadHash,
208
+ 'Authorization':
209
+ 'AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/$scope, '
210
+ 'SignedHeaders=$signedHeaders, Signature=$signature',
211
+ };
212
+ if (config.sessionToken != null) {
213
+ headers['x-amz-security-token'] = config.sessionToken!;
214
+ }
215
+ return headers;
216
+ }
217
+
218
+ Future<Response> _send(
219
+ String method,
220
+ Uri uri, {
221
+ required List<int>? body,
222
+ Map<String, String>? extraHeaders,
223
+ }) async {
224
+ final request = Request(method, uri);
225
+ request.headers.addAll(_authHeaders(method, uri, body));
226
+ if (extraHeaders != null) request.headers.addAll(extraHeaders);
227
+ if (body != null) request.bodyBytes = body;
228
+ final streamed = await _http.send(request);
229
+ return await Response.fromStream(streamed);
230
+ }
231
+
232
+ List<StorageObject> _parseListObjects(String xml) {
233
+ final objects = <StorageObject>[];
234
+ final contents =
235
+ _allMatches(xml, RegExp(r'<Contents>([\s\S]*?)</Contents>'));
236
+ for (final block in contents) {
237
+ final key = _tag(block, 'Key');
238
+ final size = int.tryParse(_tag(block, 'Size') ?? '');
239
+ final lastModified = _tag(block, 'LastModified');
240
+ if (key == null) continue;
241
+ objects.add(
242
+ StorageObject(
243
+ key: key,
244
+ storageUri: 's3://${config.bucketName}/$key',
245
+ size: size ?? 0,
246
+ lastModifiedAt: lastModified == null
247
+ ? null
248
+ : DateTime.tryParse(lastModified),
249
+ ),
250
+ );
251
+ }
252
+ return objects;
253
+ }
254
+
255
+ // --- low-level sigv4 primitives --------------------------------------------
256
+
257
+ List<int> _signingKey(String dateStamp, String region) {
258
+ var key = utf8.encode('AWS4${config.secretAccessKey}');
259
+ key = _hmac(key, dateStamp);
260
+ key = _hmac(key, region);
261
+ key = _hmac(key, _service);
262
+ key = _hmac(key, 'aws4_request');
263
+ return key;
264
+ }
265
+
266
+ String _stringToSign(String amzDate, String scope, String canonicalRequest) =>
267
+ 'AWS4-HMAC-SHA256\n$amzDate\n$scope\n${_hashHex(utf8.encode(canonicalRequest))}';
268
+
269
+ // --- static utilities ------------------------------------------------------
270
+
271
+ static String _amzDate(DateTime utc) =>
272
+ '${utc.toIso8601String().replaceAll('-', '').replaceAll(':', '').split('.').first}Z';
273
+
274
+ static String _uriEncode(String value) => Uri.encodeQueryComponent(value);
275
+
276
+ static String _xmlEscape(String value) =>
277
+ value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
278
+
279
+ static Uint8List _hmac(List<int> key, String data) =>
280
+ Hmac(sha256, key).convert(utf8.encode(data)).bytes as Uint8List;
281
+
282
+ static String _hashHex(List<int> data) => _toHex(sha256.convert(data).bytes);
283
+
284
+ static String _toHex(List<int> bytes) =>
285
+ bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
286
+
287
+ static List<String> _allMatches(String input, RegExp regex) =>
288
+ regex.allMatches(input).map((m) => m.group(1)!).toList();
289
+
290
+ static String? _tag(String block, String name) {
291
+ final m = RegExp('<$name>([\\s\\S]*?)</$name>').firstMatch(block);
292
+ return m?.group(1);
293
+ }
294
+
295
+ static void _assertStatus(Response response, List<int> ok, String operation) {
296
+ if (!ok.contains(response.statusCode)) {
297
+ throw Exception(
298
+ 'S3 $operation failed (${response.statusCode}): ${response.body}',
299
+ );
300
+ }
301
+ }
302
+ }
303
+
304
+ /// Read a local file's bytes (node profile helper).
305
+ Future<List<int>> readFileBytes(String filePath) => File(filePath).readAsBytes();
306
+
307
+ /// Write bytes to a local file, creating parent directories.
308
+ Future<void> writeFileBytes(String filePath, List<int> bytes) async {
309
+ final file = File(filePath);
310
+ await file.parent.create(recursive: true);
311
+ await file.writeAsBytes(bytes);
312
+ }
@@ -0,0 +1,125 @@
1
+ import 'dart:convert' show jsonDecode, jsonEncode, 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
+ /// Minimal AWS SSM client used to fetch the CloudFront private key from a
8
+ /// `SecureString` parameter (faithful to hot-updater's `applySsmRuntimeAwsConfig`
9
+ /// + `SSM.getParameter`).
10
+ class AwsSsmClient {
11
+ AwsSsmClient({
12
+ required this.region,
13
+ required this.accessKeyId,
14
+ required this.secretAccessKey,
15
+ this.sessionToken,
16
+ Client? http,
17
+ }) : _http = http ?? Client();
18
+
19
+ final String region;
20
+ final String accessKeyId;
21
+ final String secretAccessKey;
22
+ final String? sessionToken;
23
+ final Client _http;
24
+
25
+ static const String _service = 'ssm';
26
+
27
+ Future<String> getParameter(String name) async {
28
+ final uri = Uri.parse('https://ssm.$region.amazonaws.com/');
29
+ final body = utf8.encode(
30
+ jsonEncode({'Name': name, 'WithDecryption': true}),
31
+ );
32
+ final response = await _send('POST', uri, body);
33
+ _assertStatus(response, [200], 'GetParameter');
34
+
35
+ final json = jsonDecode(response.body) as Map<String, dynamic>;
36
+ final parameter = json['Parameter'] as Map<String, dynamic>?;
37
+ final value = parameter?['Value'] as String?;
38
+ if (value == null || value.isEmpty) {
39
+ throw StateError('SSM parameter "$name" returned no value');
40
+ }
41
+
42
+ // The stored value is a JSON document containing the private key.
43
+ final parsed = jsonDecode(value) as Map<String, dynamic>;
44
+ final privateKey = parsed['privateKey'] as String?;
45
+ if (privateKey == null || privateKey.isEmpty) {
46
+ throw StateError('SSM parameter "$name" missing "privateKey"');
47
+ }
48
+ return privateKey;
49
+ }
50
+
51
+ Map<String, String> _authHeaders(String method, Uri uri, List<int> body) {
52
+ final now = DateTime.now().toUtc();
53
+ final amzDate = _amzDate(now);
54
+ final dateStamp = amzDate.substring(0, 8);
55
+ final scope = '$dateStamp/$region/$_service/aws4_request';
56
+ final payloadHash = _hashHex(body);
57
+
58
+ final canonicalHeaders = 'host:${uri.host}\n'
59
+ 'x-amz-content-sha256:$payloadHash\n'
60
+ 'x-amz-date:$amzDate\n'
61
+ '${sessionToken != null ? 'x-amz-security-token:$sessionToken\n' : ''}'
62
+ 'x-amz-target:AmazonSSM.GetParameter\n';
63
+ final signedHeaders = sessionToken != null
64
+ ? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
65
+ : 'host;x-amz-content-sha256;x-amz-date';
66
+
67
+ final canonicalRequest = '$method\n${uri.path}\n'
68
+ '\n$canonicalHeaders$signedHeaders\n$payloadHash';
69
+ final stringToSign = _stringToSign(amzDate, scope, canonicalRequest);
70
+ final signingKey = _signingKey(dateStamp);
71
+ final signature = _toHex(_hmac(signingKey, stringToSign));
72
+
73
+ final headers = {
74
+ 'x-amz-date': amzDate,
75
+ 'x-amz-content-sha256': payloadHash,
76
+ 'x-amz-target': 'AmazonSSM.GetParameter',
77
+ 'Content-Type': 'application/x-amz-json-1.1',
78
+ 'Authorization':
79
+ 'AWS4-HMAC-SHA256 Credential=$accessKeyId/$scope, '
80
+ 'SignedHeaders=$signedHeaders, Signature=$signature',
81
+ };
82
+ if (sessionToken != null) headers['x-amz-security-token'] = sessionToken!;
83
+ return headers;
84
+ }
85
+
86
+ Future<Response> _send(String method, Uri uri, List<int> body) async {
87
+ final request = Request(method, uri);
88
+ request.headers.addAll(_authHeaders(method, uri, body));
89
+ request.bodyBytes = body;
90
+ final streamed = await _http.send(request);
91
+ return await Response.fromStream(streamed);
92
+ }
93
+
94
+ List<int> _signingKey(String dateStamp) {
95
+ var key = utf8.encode('AWS4$secretAccessKey');
96
+ key = _hmac(key, dateStamp);
97
+ key = _hmac(key, region);
98
+ key = _hmac(key, _service);
99
+ key = _hmac(key, 'aws4_request');
100
+ return key;
101
+ }
102
+
103
+ String _stringToSign(String amzDate, String scope, String canonicalRequest) =>
104
+ 'AWS4-HMAC-SHA256\n$amzDate\n$scope\n${_hashHex(utf8.encode(canonicalRequest))}';
105
+
106
+ static String _amzDate(DateTime utc) =>
107
+ '${utc.toIso8601String().replaceAll('-', '').replaceAll(':', '').split('.').first}Z';
108
+
109
+ static Uint8List _hmac(List<int> key, String data) =>
110
+ Hmac(sha256, key).convert(utf8.encode(data)).bytes as Uint8List;
111
+
112
+ static String _hashHex(List<int> data) =>
113
+ _toHex(sha256.convert(data).bytes);
114
+
115
+ static String _toHex(List<int> bytes) =>
116
+ bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
117
+
118
+ static void _assertStatus(Response response, List<int> ok, String operation) {
119
+ if (!ok.contains(response.statusCode)) {
120
+ throw Exception(
121
+ 'SSM $operation failed (${response.statusCode}): ${response.body}',
122
+ );
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,20 @@
1
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
2
+ show createUniversalStoragePlugin;
3
+
4
+ import 'aws_config.dart' show AwsS3StorageConfig;
5
+ import 'aws_storage_profile.dart' show
6
+ createS3RuntimeStorageProfile,
7
+ createS3StorageProfile;
8
+
9
+ /// AWS S3 storage plugin (faithful port of hot-updater `s3Storage.ts`).
10
+ ///
11
+ /// Stores bundles as objects in an S3 bucket and serves presigned GET URLs at
12
+ /// runtime. Uses AWS Signature V4 (virtual-hosted addressing).
13
+ final s3Storage = createUniversalStoragePlugin<AwsS3StorageConfig>(
14
+ name: 's3Storage',
15
+ supportedProtocol: 's3',
16
+ factory: (config) => (
17
+ node: createS3StorageProfile(config),
18
+ runtime: createS3RuntimeStorageProfile(config),
19
+ ),
20
+ );
@@ -0,0 +1,123 @@
1
+ import 'package:path/path.dart' show basename;
2
+
3
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
4
+ show
5
+ NodeStorageProfile,
6
+ RuntimeStorageProfile,
7
+ StorageObject,
8
+ createStorageKeyBuilder,
9
+ getContentType,
10
+ parseStorageUri;
11
+
12
+ import 'aws_config.dart' show AwsS3StorageConfig, resolveAwsS3Client;
13
+ import 'aws_s3_client.dart' show AwsS3ClientLike, readFileBytes, writeFileBytes;
14
+
15
+ void _ensureExpectedBucket(String bucket, String bucketName) {
16
+ if (bucket != bucketName) {
17
+ throw ArgumentError(
18
+ 'Bucket name mismatch: expected "$bucketName", but found "$bucket".',
19
+ );
20
+ }
21
+ }
22
+
23
+ /// Build the node (deploy/CLI) storage profile for AWS S3.
24
+ NodeStorageProfile createS3StorageProfile(AwsS3StorageConfig config) {
25
+ final AwsS3ClientLike client = resolveAwsS3Client(config);
26
+ final getStorageKey = createStorageKeyBuilder(config.basePath);
27
+
28
+ return _S3NodeProfile(client, config, getStorageKey);
29
+ }
30
+
31
+ /// Build the runtime (update-check) storage profile for AWS S3.
32
+ RuntimeStorageProfile createS3RuntimeStorageProfile(AwsS3StorageConfig config) {
33
+ final AwsS3ClientLike client = resolveAwsS3Client(config);
34
+ return _S3RuntimeProfile(client, config);
35
+ }
36
+
37
+ class _S3NodeProfile implements NodeStorageProfile {
38
+ _S3NodeProfile(this._client, this._config, this._getStorageKey);
39
+
40
+ final AwsS3ClientLike _client;
41
+ final AwsS3StorageConfig _config;
42
+ final String Function(String, [String, String, String]) _getStorageKey;
43
+
44
+ String _getListPrefix(String prefix) {
45
+ final normalizedPrefix = prefix.replaceAll(RegExp(r'^/+|/+$'), '');
46
+ final value = [_config.basePath ?? '', normalizedPrefix]
47
+ .where((s) => s.isNotEmpty)
48
+ .join('/');
49
+ return value.isNotEmpty ? '$value/' : '';
50
+ }
51
+
52
+ @override
53
+ Future<Map<String, String>> upload(String key, String filePath) async {
54
+ final bytes = await readFileBytes(filePath);
55
+ final contentType = getContentType(filePath);
56
+ final filename = basename(filePath);
57
+ final storageKey = _getStorageKey(key, filename);
58
+
59
+ await _client.putObject(storageKey, bytes, contentType);
60
+ return {'storageUri': 's3://${_config.bucketName}/$storageKey'};
61
+ }
62
+
63
+ @override
64
+ Future<bool> exists(String storageUri) async {
65
+ final parsed = parseStorageUri(storageUri, 's3');
66
+ _ensureExpectedBucket(parsed.bucket, _config.bucketName);
67
+ return _client.headObject(parsed.key);
68
+ }
69
+
70
+ @override
71
+ Future<void> delete(String storageUri) async {
72
+ final parsed = parseStorageUri(storageUri, 's3');
73
+ _ensureExpectedBucket(parsed.bucket, _config.bucketName);
74
+
75
+ final list = await _client.listObjects(parsed.key);
76
+ if (list.isEmpty) {
77
+ throw StateError('Bundle Not Found');
78
+ }
79
+ await _client.deleteObjects(list.map((o) => o.key).toList());
80
+ }
81
+
82
+ @override
83
+ Future<void> downloadFile(String storageUri, String filePath) async {
84
+ final parsed = parseStorageUri(storageUri, 's3');
85
+ _ensureExpectedBucket(parsed.bucket, _config.bucketName);
86
+ final bytes = await _client.getObjectAsBytes(parsed.key);
87
+ await writeFileBytes(filePath, bytes);
88
+ }
89
+
90
+ @override
91
+ Future<List<StorageObject>> listObjects([String? prefix]) =>
92
+ _client.listObjects(prefix == null ? null : _getListPrefix(prefix));
93
+
94
+ @override
95
+ Future<void> deleteObjects(List<String> keys) =>
96
+ _client.deleteObjects(keys.map(_getStorageKey).toList());
97
+ }
98
+
99
+ class _S3RuntimeProfile implements RuntimeStorageProfile {
100
+ _S3RuntimeProfile(this._client, this._config);
101
+
102
+ final AwsS3ClientLike _client;
103
+ final AwsS3StorageConfig _config;
104
+
105
+ @override
106
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async {
107
+ final parsed = parseStorageUri(storageUri, 's3');
108
+ _ensureExpectedBucket(parsed.bucket, _config.bucketName);
109
+ final fileUrl = await _client.getPresignedUrl(parsed.key);
110
+ return {'fileUrl': fileUrl};
111
+ }
112
+
113
+ @override
114
+ Future<String?> readText(String storageUri) async {
115
+ final parsed = parseStorageUri(storageUri, 's3');
116
+ _ensureExpectedBucket(parsed.bucket, _config.bucketName);
117
+ try {
118
+ return await _client.getObjectAsString(parsed.key);
119
+ } catch (_) {
120
+ return null;
121
+ }
122
+ }
123
+ }