@_nazmiforreal/flutter-ota 0.1.0 → 0.1.1

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 (177) hide show
  1. package/bin/flutter-patcher.js +37 -39
  2. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
  4. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  5. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  6. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  7. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  8. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  19. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  20. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  21. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  22. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  23. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  24. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  25. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  30. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  31. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  38. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  39. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  40. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  41. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  42. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  43. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  44. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  45. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  46. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  47. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  48. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  49. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  50. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  51. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  52. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  53. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  54. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  55. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  56. package/dart-src/packages/core/lib/src/status.dart +24 -0
  57. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  58. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  59. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  60. package/dart-src/packages/core/pubspec.lock +381 -0
  61. package/dart-src/packages/core/pubspec.yaml +9 -0
  62. package/dart-src/packages/core/tool/dbg.dart +6 -0
  63. package/dart-src/plugins/aws/.gitkeep +0 -0
  64. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  65. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  66. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  67. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  68. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  69. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  70. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  71. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  72. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  73. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  74. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  75. package/dart-src/plugins/aws/pubspec.lock +435 -0
  76. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  77. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  78. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  79. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  81. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  82. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  83. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  84. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  85. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  86. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  87. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  88. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  89. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  90. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  91. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  92. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  93. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  94. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  95. package/dart-src/plugins/cloudflare/pubspec.lock +411 -0
  96. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  97. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  98. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  99. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  100. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  101. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  102. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  103. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  104. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  105. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  106. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  107. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  108. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  109. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  110. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  111. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  112. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  113. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  114. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  115. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  116. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  117. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  118. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  119. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  120. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  121. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  122. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  123. package/dart-src/plugins/plugin-core/pubspec.lock +396 -0
  124. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  125. package/dart-src/plugins/postgres/.gitkeep +0 -0
  126. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  127. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  128. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  129. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  130. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  131. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  132. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  133. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  134. package/dart-src/plugins/postgres/pubspec.lock +427 -0
  135. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  136. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  137. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  138. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  139. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  140. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  141. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  142. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  143. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  144. package/dart-src/plugins/standalone/.gitkeep +0 -0
  145. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  146. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  147. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  148. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  149. package/dart-src/plugins/standalone/pubspec.lock +411 -0
  150. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  151. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  152. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  153. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  154. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  155. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  156. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  157. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  158. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  159. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  160. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  161. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  162. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  163. package/dart-src/plugins/supabase/pubspec.lock +515 -0
  164. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  165. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  166. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  167. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  168. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  169. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  170. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  171. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  172. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  173. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  174. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  175. package/package.json +2 -1
  176. package/scripts/postinstall.js +32 -23
  177. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -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);