@_nazmiforreal/flutter-ota 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flutter-patcher.js +48 -39
- package/dart-src/packages/cli-tools/.gitkeep +0 -0
- package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
- package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
- package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
- package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
- package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
- package/dart-src/packages/core/lib/src/bundle.dart +191 -0
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
- package/dart-src/packages/core/lib/src/metadata.dart +31 -0
- package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
- package/dart-src/packages/core/lib/src/platform.dart +15 -0
- package/dart-src/packages/core/lib/src/rollout.dart +209 -0
- package/dart-src/packages/core/lib/src/semver.dart +70 -0
- package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
- package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
- package/dart-src/packages/core/lib/src/status.dart +24 -0
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
- package/dart-src/packages/core/lib/src/uuid.dart +45 -0
- package/dart-src/packages/core/pubspec.yaml +9 -0
- package/dart-src/packages/core/tool/dbg.dart +6 -0
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
- package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
- package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
- package/dart-src/plugins/aws/pubspec.yaml +22 -0
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
- package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
- package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
- package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
- package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
- package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
- package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
- package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
- package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
- package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
- package/dart-src/plugins/postgres/pubspec.yaml +18 -0
- package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
- package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
- package/dart-src/plugins/standalone/pubspec.yaml +18 -0
- package/dart-src/plugins/supabase/lib/edge.dart +10 -0
- package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
- package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
- package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
- package/dart-src/plugins/supabase/pubspec.yaml +19 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +53 -25
- /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
|
@@ -0,0 +1,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('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
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);
|