@_nazmiforreal/flutter-ota 0.1.6 → 0.1.7
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/README.md +13 -13
- package/bin/{flutter-patcher-linux-x64 → flutter-ota-linux-x64} +0 -0
- package/bin/{flutter-patcher.js → flutter-ota.js} +4 -4
- package/dart-src/packages/cli-tools/bin/{flutter_patcher.dart → flutter_ota_kit.dart} +2 -2
- package/dart-src/packages/cli-tools/lib/{flutter_patcher_cli.dart → flutter_ota_kit_cli.dart} +5 -3
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -25
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +10 -4
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +5 -3
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +279 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +3 -3
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +294 -45
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -24
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +117 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +29 -78
- package/dart-src/packages/cli-tools/lib/src/operations.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/pack.dart +12 -6
- package/dart-src/packages/cli-tools/lib/src/runner.dart +21 -3
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/cli-tools/lib/src/verbose.dart +3 -0
- package/dart-src/packages/cli-tools/migrations/aws/README.md +7 -0
- package/dart-src/packages/cli-tools/migrations/postgres/bundles.sql +45 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/packages/cli-tools/migrations/postgres/hash_user_id.sql +30 -0
- package/dart-src/packages/cli-tools/migrations/postgres/is_device_eligible.sql +237 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +11 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +8 -10
- package/dart-src/packages/cli-tools/test/backends_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/cli_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +7 -7
- package/dart-src/packages/core/lib/{flutter_patcher_core.dart → flutter_ota_kit_core.dart} +1 -1
- package/dart-src/packages/core/pubspec.yaml +2 -2
- package/dart-src/packages/core/test/rollout_test.dart +1 -1
- package/dart-src/packages/core/test/semver_test.dart +1 -1
- package/dart-src/packages/core/test/types_test.dart +2 -2
- package/dart-src/packages/core/test/uuid_test.dart +1 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -1
- package/dart-src/plugins/aws/lib/{flutter_patcher_aws.dart → flutter_ota_kit_aws.dart} +0 -2
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_database.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +25 -3
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/plugins/aws/pubspec.yaml +4 -4
- package/dart-src/plugins/aws/test/aws_database_test.dart +3 -3
- package/dart-src/plugins/aws/test/aws_storage_test.dart +2 -2
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +1 -1
- package/dart-src/plugins/cloudflare/pubspec.yaml +4 -4
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +2 -2
- package/dart-src/plugins/plugin-core/lib/{flutter_patcher_plugin_core.dart → flutter_ota_kit_plugin_core.dart} +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/types.dart +2 -2
- package/dart-src/plugins/plugin-core/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +5 -5
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +1 -1
- package/dart-src/plugins/postgres/lib/{flutter_patcher_postgres.dart → flutter_ota_kit_postgres.dart} +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +9 -9
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +1 -1
- package/dart-src/plugins/postgres/pubspec.yaml +4 -4
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +2 -2
- package/dart-src/plugins/supabase/lib/edge.dart +1 -1
- package/dart-src/plugins/supabase/lib/{flutter_patcher_supabase.dart → flutter_ota_kit_supabase.dart} +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +5 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +2 -2
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +2 -5
- package/dart-src/plugins/supabase/pubspec.yaml +4 -4
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +5 -5
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +2 -2
- package/migrations/aws/README.md +7 -0
- package/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/migrations/cloudflare/bundles.sql +42 -0
- package/migrations/postgres/bundles.sql +45 -0
- package/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/migrations/postgres/hash_user_id.sql +30 -0
- package/migrations/postgres/is_device_eligible.sql +237 -0
- package/package.json +4 -4
- package/scripts/postinstall.js +15 -15
- package/scripts/vendor.sh +1 -1
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -496
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -743
- package/dart-src/packages/cli-tools/pubspec.lock +0 -638
- package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +0 -315
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +0 -72
- package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +0 -190
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +0 -12
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +0 -107
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +0 -233
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +0 -175
- package/dart-src/plugins/standalone/pubspec.yaml +0 -18
- package/dart-src/plugins/standalone/test/standalone_test.dart +0 -264
- /package/dart-src/plugins/cloudflare/lib/{flutter_patcher_cloudflare.dart → flutter_ota_kit_cloudflare.dart} +0 -0
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import 'dart:convert' show jsonDecode, jsonEncode, utf8;
|
|
2
|
-
import 'dart:io' show Directory, File;
|
|
3
|
-
import 'dart:typed_data' show Uint8List;
|
|
4
|
-
|
|
5
|
-
import 'package:archive/archive.dart';
|
|
6
|
-
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
7
|
-
import 'package:http/http.dart' as http;
|
|
8
|
-
import 'package:http/testing.dart' show MockClient;
|
|
9
|
-
import 'package:test/test.dart';
|
|
10
|
-
|
|
11
|
-
// Stateful in-memory fake of the standalone (self-hosted) REST server.
|
|
12
|
-
class _FakeServer {
|
|
13
|
-
final Map<String, Map<String, dynamic>> bundles = {};
|
|
14
|
-
final Map<String, List<int>> storage = {};
|
|
15
|
-
|
|
16
|
-
Future<http.Response> handle(http.Request request) async {
|
|
17
|
-
final path = request.url.path;
|
|
18
|
-
final method = request.method;
|
|
19
|
-
|
|
20
|
-
if (path == '/api/bundles/channels' && method == 'GET') {
|
|
21
|
-
final channels = bundles.values.map((b) => b['channel'] as String).toSet().toList();
|
|
22
|
-
return http.Response(jsonEncode({'data': {'channels': channels}}), 200);
|
|
23
|
-
}
|
|
24
|
-
if (path == '/api/bundles' && method == 'POST') {
|
|
25
|
-
final list = jsonDecode(request.body) as List;
|
|
26
|
-
for (final item in list) {
|
|
27
|
-
final b = (item as Map).cast<String, dynamic>();
|
|
28
|
-
bundles[b['id'] as String] = {...b};
|
|
29
|
-
}
|
|
30
|
-
return http.Response(jsonEncode({'success': true}), 201);
|
|
31
|
-
}
|
|
32
|
-
if (path == '/api/bundles' && method == 'GET') {
|
|
33
|
-
final channel = request.url.queryParameters['channel'];
|
|
34
|
-
final enabled = request.url.queryParameters['enabled'];
|
|
35
|
-
var items = bundles.values.toList();
|
|
36
|
-
if (channel != null) items = items.where((b) => b['channel'] == channel).toList();
|
|
37
|
-
if (enabled == 'true') items = items.where((b) => b['enabled'] == true).toList();
|
|
38
|
-
// Emulate the DB's default id-desc ordering (latest bundle first).
|
|
39
|
-
items = items.reversed.toList();
|
|
40
|
-
return http.Response(
|
|
41
|
-
jsonEncode({
|
|
42
|
-
'data': items,
|
|
43
|
-
'pagination': {
|
|
44
|
-
'total': items.length,
|
|
45
|
-
'hasNextPage': false,
|
|
46
|
-
'hasPreviousPage': false,
|
|
47
|
-
'currentPage': 1,
|
|
48
|
-
'totalPages': 1,
|
|
49
|
-
'nextCursor': null,
|
|
50
|
-
'previousCursor': null,
|
|
51
|
-
},
|
|
52
|
-
}),
|
|
53
|
-
200,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
if (path.startsWith('/api/bundles/') && method == 'PATCH') {
|
|
57
|
-
final id = path.substring('/api/bundles/'.length);
|
|
58
|
-
final patch = jsonDecode(request.body) as Map<String, dynamic>;
|
|
59
|
-
final existing = bundles[id];
|
|
60
|
-
if (existing == null) return http.Response('{"error":"not found"}', 404);
|
|
61
|
-
existing.addAll(patch);
|
|
62
|
-
return http.Response(jsonEncode({'success': true}), 200);
|
|
63
|
-
}
|
|
64
|
-
if (path.startsWith('/api/bundles/') && method == 'DELETE') {
|
|
65
|
-
final id = path.substring('/api/bundles/'.length);
|
|
66
|
-
bundles.remove(id);
|
|
67
|
-
return http.Response(jsonEncode({'success': true}), 200);
|
|
68
|
-
}
|
|
69
|
-
if (path.startsWith('/api/bundles/') && method == 'GET') {
|
|
70
|
-
final id = path.substring('/api/bundles/'.length);
|
|
71
|
-
final b = bundles[id];
|
|
72
|
-
if (b == null) return http.Response('{"error":"not found"}', 404);
|
|
73
|
-
return http.Response(jsonEncode(b), 200);
|
|
74
|
-
}
|
|
75
|
-
if (path == '/api/upload' && method == 'POST') {
|
|
76
|
-
final key = request.url.queryParameters['key']!;
|
|
77
|
-
storage[key] = request.bodyBytes;
|
|
78
|
-
return http.Response(jsonEncode({'storageUri': 'mem://$key'}), 201);
|
|
79
|
-
}
|
|
80
|
-
if (path == '/api/getDownloadUrl' && method == 'POST') {
|
|
81
|
-
final uri = (jsonDecode(request.body) as Map)['storageUri'] as String;
|
|
82
|
-
final key = uri.replaceFirst('mem://', '');
|
|
83
|
-
return http.Response(
|
|
84
|
-
jsonEncode({'fileUrl': 'http://localhost/mem/$key'}),
|
|
85
|
-
200,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
if (path == '/api/delete' && method == 'DELETE') {
|
|
89
|
-
final uri = (jsonDecode(request.body) as Map)['storageUri'] as String;
|
|
90
|
-
storage.remove(uri.replaceFirst('mem://', ''));
|
|
91
|
-
return http.Response(jsonEncode({'success': true}), 200);
|
|
92
|
-
}
|
|
93
|
-
if (request.url.host == 'localhost' && path.startsWith('/mem/')) {
|
|
94
|
-
final key = path.substring('/mem/'.length);
|
|
95
|
-
final bytes = storage[key];
|
|
96
|
-
if (bytes == null) return http.Response('not found', 404);
|
|
97
|
-
return http.Response.bytes(bytes, 200);
|
|
98
|
-
}
|
|
99
|
-
return http.Response(jsonEncode({'error': 'not found'}), 404);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
void main() {
|
|
104
|
-
group('cli backends', () {
|
|
105
|
-
test('standalone backend (self-hosted REST)', () async {
|
|
106
|
-
final fake = _FakeServer();
|
|
107
|
-
final client = MockClient(fake.handle);
|
|
108
|
-
final config = FlutterPatcherConfig(
|
|
109
|
-
provider: 'standalone',
|
|
110
|
-
supabase: const SupabaseConfigJson(url: 'https://x.supabase.co'),
|
|
111
|
-
standalone: StandaloneConfigJson(baseUrl: 'http://localhost'),
|
|
112
|
-
channel: 'production',
|
|
113
|
-
platform: 'android',
|
|
114
|
-
source: './dist',
|
|
115
|
-
);
|
|
116
|
-
final backend = resolveBackend(
|
|
117
|
-
config,
|
|
118
|
-
standaloneClientFactory: () => client,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
final source = Directory.systemTemp.createTempSync('fp-src-');
|
|
122
|
-
File('${source.path}/app.so').writeAsStringSync('fake-libapp-bytes');
|
|
123
|
-
try {
|
|
124
|
-
final bundle = await deployBundle(
|
|
125
|
-
backend,
|
|
126
|
-
DeployOptions(
|
|
127
|
-
source: source.path,
|
|
128
|
-
channel: 'production',
|
|
129
|
-
platform: 'android',
|
|
130
|
-
message: 'first',
|
|
131
|
-
targetAppVersion: '>=1.0.0',
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
expect(bundle.id, isNotEmpty);
|
|
135
|
-
expect(bundle.storageUri, isNotEmpty);
|
|
136
|
-
expect(await backend.storage.exists(bundle.storageUri), isTrue);
|
|
137
|
-
|
|
138
|
-
final dl = File('${Directory.systemTemp.path}/fp-dl-${bundle.id}.zip');
|
|
139
|
-
await backend.storage.downloadFile(bundle.storageUri, dl.path);
|
|
140
|
-
expect(dl.existsSync() && await dl.length() > 0, isTrue);
|
|
141
|
-
await dl.delete();
|
|
142
|
-
|
|
143
|
-
final listed = await listBundles(backend, const ListOptions());
|
|
144
|
-
expect(listed.data.where((b) => b.id == bundle.id), hasLength(1));
|
|
145
|
-
|
|
146
|
-
final live = await getChannel(backend, 'production');
|
|
147
|
-
expect(live?.id, bundle.id);
|
|
148
|
-
|
|
149
|
-
final second = await deployBundle(
|
|
150
|
-
backend,
|
|
151
|
-
DeployOptions(
|
|
152
|
-
source: source.path,
|
|
153
|
-
channel: 'production',
|
|
154
|
-
platform: 'android',
|
|
155
|
-
message: 'second',
|
|
156
|
-
targetAppVersion: '>=1.0.0',
|
|
157
|
-
),
|
|
158
|
-
);
|
|
159
|
-
await promoteBundle(backend, second.id, 'production');
|
|
160
|
-
final disabled = await rollbackChannel(backend, 'production');
|
|
161
|
-
expect(disabled, second.id);
|
|
162
|
-
|
|
163
|
-
await deleteBundle(backend, bundle.id);
|
|
164
|
-
final afterDelete = await listBundles(
|
|
165
|
-
backend,
|
|
166
|
-
const ListOptions(channel: 'production'),
|
|
167
|
-
);
|
|
168
|
-
expect(
|
|
169
|
-
afterDelete.data.where((b) => b.id == bundle.id).isEmpty,
|
|
170
|
-
isTrue,
|
|
171
|
-
);
|
|
172
|
-
} finally {
|
|
173
|
-
source.deleteSync(recursive: true);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('standalone backend signs the deployed bundle (md5 fileHash + '
|
|
178
|
-
'metadata.signature verifiable)', () async {
|
|
179
|
-
final (privateB64, publicB64) = await generateEd25519KeyPair();
|
|
180
|
-
final fake = _FakeServer();
|
|
181
|
-
final client = MockClient(fake.handle);
|
|
182
|
-
final config = FlutterPatcherConfig(
|
|
183
|
-
provider: 'standalone',
|
|
184
|
-
supabase: const SupabaseConfigJson(url: 'https://x.supabase.co'),
|
|
185
|
-
standalone: StandaloneConfigJson(baseUrl: 'http://localhost'),
|
|
186
|
-
channel: 'production',
|
|
187
|
-
platform: 'android',
|
|
188
|
-
source: './dist',
|
|
189
|
-
);
|
|
190
|
-
final backend = resolveBackend(
|
|
191
|
-
config,
|
|
192
|
-
standaloneClientFactory: () => client,
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
final source = Directory.systemTemp.createTempSync('fp-src-signed-');
|
|
196
|
-
File('${source.path}/app.so').writeAsStringSync('fake-libapp-bytes');
|
|
197
|
-
try {
|
|
198
|
-
final bundle = await deployBundle(
|
|
199
|
-
backend,
|
|
200
|
-
DeployOptions(
|
|
201
|
-
source: source.path,
|
|
202
|
-
channel: 'production',
|
|
203
|
-
platform: 'android',
|
|
204
|
-
message: 'signed',
|
|
205
|
-
targetAppVersion: '>=1.0.0',
|
|
206
|
-
signingKeyBase64: privateB64,
|
|
207
|
-
),
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// fileHash must be a plain MD5 hex (device rejects anything else).
|
|
211
|
-
expect(bundle.fileHash, matches(RegExp(r'^[0-9a-f]{32}$')));
|
|
212
|
-
expect(bundle.metadata, isNotNull);
|
|
213
|
-
final signature = bundle.metadata!.signature;
|
|
214
|
-
expect(signature, isNotNull);
|
|
215
|
-
expect(signature, isNotEmpty);
|
|
216
|
-
|
|
217
|
-
// The signature verifies over the MD5-hex UTF-8 bytes with the SPKI
|
|
218
|
-
// public key, exactly as the Android device SDK checks it.
|
|
219
|
-
final verified = await ed25519Verify(
|
|
220
|
-
utf8.encode(bundle.fileHash),
|
|
221
|
-
signature!,
|
|
222
|
-
publicB64,
|
|
223
|
-
);
|
|
224
|
-
expect(verified, isTrue);
|
|
225
|
-
} finally {
|
|
226
|
-
source.deleteSync(recursive: true);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('build + deploy uploads the real patch.zip (no nested-zip mangling)', () async {
|
|
231
|
-
// 1) Build a synthetic multi-ABI APK in memory.
|
|
232
|
-
final apk = Archive();
|
|
233
|
-
for (final abi in const ['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
|
|
234
|
-
final bytes = utf8.encode('LIBAPP_$abi');
|
|
235
|
-
apk.addFile(ArchiveFile('lib/$abi/libapp.so', bytes.length, bytes));
|
|
236
|
-
}
|
|
237
|
-
final apkDir = Directory.systemTemp.createTempSync('fp-apk-');
|
|
238
|
-
final apkFile = File('${apkDir.path}/app.apk')
|
|
239
|
-
..writeAsBytesSync(Uint8List.fromList(ZipEncoder().encode(apk)));
|
|
240
|
-
|
|
241
|
-
// 2) Pack it.
|
|
242
|
-
final distDir = Directory.systemTemp.createTempSync('fp-dist-');
|
|
243
|
-
await packPatch(
|
|
244
|
-
apkPath: apkFile.path,
|
|
245
|
-
version: '9.9.9',
|
|
246
|
-
targetVersionCode: 42,
|
|
247
|
-
out: distDir.path,
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// 3) Deploy the packed dist/ with a signing key.
|
|
251
|
-
final (privateB64, publicB64) = await generateEd25519KeyPair();
|
|
252
|
-
final fake = _FakeServer();
|
|
253
|
-
final client = MockClient(fake.handle);
|
|
254
|
-
final config = FlutterPatcherConfig(
|
|
255
|
-
provider: 'standalone',
|
|
256
|
-
supabase: const SupabaseConfigJson(url: 'https://x.supabase.co'),
|
|
257
|
-
standalone: StandaloneConfigJson(baseUrl: 'http://localhost'),
|
|
258
|
-
channel: 'production',
|
|
259
|
-
platform: 'android',
|
|
260
|
-
source: distDir.path,
|
|
261
|
-
);
|
|
262
|
-
final backend = resolveBackend(
|
|
263
|
-
config,
|
|
264
|
-
standaloneClientFactory: () => client,
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
final bundle = await deployBundle(
|
|
268
|
-
backend,
|
|
269
|
-
DeployOptions(
|
|
270
|
-
source: distDir.path,
|
|
271
|
-
channel: 'production',
|
|
272
|
-
platform: 'android',
|
|
273
|
-
message: 'built',
|
|
274
|
-
targetAppVersion: '>=1.0.0',
|
|
275
|
-
signingKeyBase64: privateB64,
|
|
276
|
-
),
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
// 4) Download what the device would fetch and make sure it is the
|
|
280
|
-
// genuine patch (root manifest.json + per-ABI libapp.so), NOT a
|
|
281
|
-
// zip that nests the real patch under dist/patch.zip (the old bug).
|
|
282
|
-
final dl = File('${distDir.path}/downloaded.zip');
|
|
283
|
-
await backend.storage.downloadFile(bundle.storageUri, dl.path);
|
|
284
|
-
final downloaded = ZipDecoder().decodeBytes(dl.readAsBytesSync());
|
|
285
|
-
|
|
286
|
-
final names = downloaded.files.map((f) => f.name).toList();
|
|
287
|
-
expect(names, contains('manifest.json'));
|
|
288
|
-
expect(names, contains('lib/arm64-v8a/libapp.so'));
|
|
289
|
-
expect(names, isNot(contains('dist/patch.zip')),
|
|
290
|
-
reason: 'deploy must upload the real patch, not re-zip the dir');
|
|
291
|
-
|
|
292
|
-
final manifest = Map<String, dynamic>.from(
|
|
293
|
-
jsonDecode(utf8.decode(
|
|
294
|
-
downloaded.files.firstWhere((f) => f.name == 'manifest.json').content
|
|
295
|
-
as List<int>,
|
|
296
|
-
)) as Map,
|
|
297
|
-
);
|
|
298
|
-
expect(manifest['schemaVersion'], 2);
|
|
299
|
-
expect((manifest['lib'] as Map).keys, hasLength(3));
|
|
300
|
-
|
|
301
|
-
// 5) signature chain still holds on the deployed bundle.
|
|
302
|
-
expect(
|
|
303
|
-
await ed25519Verify(
|
|
304
|
-
utf8.encode(bundle.fileHash),
|
|
305
|
-
bundle.metadata!.signature!,
|
|
306
|
-
publicB64,
|
|
307
|
-
),
|
|
308
|
-
isTrue,
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
apkDir.deleteSync(recursive: true);
|
|
312
|
-
distDir.deleteSync(recursive: true);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
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);
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import 'dart:convert' show base64, base64Decode, utf8;
|
|
2
|
-
import 'dart:math' show Random;
|
|
3
|
-
import 'dart:typed_data' show Uint8List;
|
|
4
|
-
|
|
5
|
-
import 'package:asn1lib/asn1lib.dart';
|
|
6
|
-
import 'package:flutter_patcher_aws/flutter_patcher_aws.dart'
|
|
7
|
-
show
|
|
8
|
-
AwsLambdaEdgeStorageConfig,
|
|
9
|
-
cloudfrontSignedUrl,
|
|
10
|
-
s3LambdaEdgeStorage,
|
|
11
|
-
verifyCloudfrontSignedUrl;
|
|
12
|
-
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
13
|
-
show RuntimeStorageProfile;
|
|
14
|
-
import 'package:pointycastle/export.dart'
|
|
15
|
-
show
|
|
16
|
-
FortunaRandom,
|
|
17
|
-
KeyParameter,
|
|
18
|
-
ParametersWithRandom,
|
|
19
|
-
RSAKeyGenerator,
|
|
20
|
-
RSAKeyGeneratorParameters,
|
|
21
|
-
RSAPrivateKey,
|
|
22
|
-
RSAPublicKey;
|
|
23
|
-
import 'package:test/test.dart';
|
|
24
|
-
|
|
25
|
-
import 'mock_aws_s3_client.dart' show MockAwsS3Client, Store;
|
|
26
|
-
|
|
27
|
-
List<int> _b64urlDecode(String s) {
|
|
28
|
-
final normalized = s.replaceAll('-', '+').replaceAll('_', '/');
|
|
29
|
-
final padded = normalized.padRight(
|
|
30
|
-
normalized.length + (4 - normalized.length % 4) % 4,
|
|
31
|
-
'=',
|
|
32
|
-
);
|
|
33
|
-
return base64Decode(padded);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
String _wrapPem(String label, String b64) {
|
|
37
|
-
final chunks = <String>[];
|
|
38
|
-
for (var i = 0; i < b64.length; i += 64) {
|
|
39
|
-
chunks.add(b64.substring(i, i + 64 > b64.length ? b64.length : i + 64));
|
|
40
|
-
}
|
|
41
|
-
return '-----BEGIN $label-----\n${chunks.join('\n')}\n-----END $label-----\n';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
String _encodePrivatePkcs1(RSAPrivateKey k, RSAPublicKey pub) {
|
|
45
|
-
final p = k.p!;
|
|
46
|
-
final q = k.q!;
|
|
47
|
-
final d = k.privateExponent!;
|
|
48
|
-
final dP = d % (p - BigInt.one);
|
|
49
|
-
final dQ = d % (q - BigInt.one);
|
|
50
|
-
final qInv = q.modInverse(p);
|
|
51
|
-
final seq = ASN1Sequence();
|
|
52
|
-
seq.add(ASN1Integer(BigInt.zero));
|
|
53
|
-
seq.add(ASN1Integer(k.modulus!));
|
|
54
|
-
seq.add(ASN1Integer(pub.exponent!));
|
|
55
|
-
seq.add(ASN1Integer(d));
|
|
56
|
-
seq.add(ASN1Integer(p));
|
|
57
|
-
seq.add(ASN1Integer(q));
|
|
58
|
-
seq.add(ASN1Integer(dP));
|
|
59
|
-
seq.add(ASN1Integer(dQ));
|
|
60
|
-
seq.add(ASN1Integer(qInv));
|
|
61
|
-
return _wrapPem('RSA PRIVATE KEY', base64.encode(seq.encodedBytes));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
String _encodePublic(RSAPublicKey pub) {
|
|
65
|
-
final keySeq = ASN1Sequence();
|
|
66
|
-
keySeq.add(ASN1Integer(pub.modulus!));
|
|
67
|
-
keySeq.add(ASN1Integer(pub.exponent!));
|
|
68
|
-
final spki = ASN1Sequence();
|
|
69
|
-
final alg = ASN1Sequence();
|
|
70
|
-
alg.add(ASN1ObjectIdentifier.fromComponents([1, 2, 840, 113549, 1, 1, 1]));
|
|
71
|
-
alg.add(ASN1Null());
|
|
72
|
-
spki.add(alg);
|
|
73
|
-
spki.add(ASN1BitString(keySeq.encodedBytes));
|
|
74
|
-
return _wrapPem('PUBLIC KEY', base64.encode(spki.encodedBytes));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
(String, String) _generateKeyPair() {
|
|
78
|
-
final rng = FortunaRandom();
|
|
79
|
-
final seedSource = Random.secure();
|
|
80
|
-
rng.seed(
|
|
81
|
-
KeyParameter(
|
|
82
|
-
Uint8List.fromList(
|
|
83
|
-
List<int>.generate(32, (_) => seedSource.nextInt(256)),
|
|
84
|
-
),
|
|
85
|
-
),
|
|
86
|
-
);
|
|
87
|
-
final kg = RSAKeyGenerator()
|
|
88
|
-
..init(
|
|
89
|
-
ParametersWithRandom(
|
|
90
|
-
RSAKeyGeneratorParameters(BigInt.from(65537), 2048, 12),
|
|
91
|
-
rng,
|
|
92
|
-
),
|
|
93
|
-
);
|
|
94
|
-
final pair = kg.generateKeyPair();
|
|
95
|
-
final priv = pair.privateKey as RSAPrivateKey;
|
|
96
|
-
final pub = pair.publicKey as RSAPublicKey;
|
|
97
|
-
return (_encodePrivatePkcs1(priv, pub), _encodePublic(pub));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
AwsLambdaEdgeStorageConfig _edgeConfig(
|
|
101
|
-
Store store,
|
|
102
|
-
String privatePem, {
|
|
103
|
-
String? publicBaseUrl,
|
|
104
|
-
Future<String> Function()? publicBaseUrlResolver,
|
|
105
|
-
}) =>
|
|
106
|
-
AwsLambdaEdgeStorageConfig(
|
|
107
|
-
bucketName: 'test-bucket',
|
|
108
|
-
region: 'us-east-1',
|
|
109
|
-
accessKeyId: 'ak',
|
|
110
|
-
secretAccessKey: 'sk',
|
|
111
|
-
keyPairId: 'KP123',
|
|
112
|
-
publicBaseUrl: publicBaseUrl,
|
|
113
|
-
publicBaseUrlResolver: publicBaseUrlResolver,
|
|
114
|
-
getPrivateKey: () async => privatePem,
|
|
115
|
-
clientFactory: (_) => MockAwsS3Client(store),
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
void main() {
|
|
119
|
-
group('aws cloudfront signed url', () {
|
|
120
|
-
final (privatePem, publicPem) = _generateKeyPair();
|
|
121
|
-
|
|
122
|
-
test('cloudfrontSignedUrl round-trips and verifies', () {
|
|
123
|
-
const url = 'https://cdn.example.com/bundles/x.zip';
|
|
124
|
-
final signed = cloudfrontSignedUrl(
|
|
125
|
-
url: url,
|
|
126
|
-
keyPairId: 'KP123',
|
|
127
|
-
privateKeyPem: privatePem,
|
|
128
|
-
dateLessThan: DateTime.now().add(const Duration(days: 365)),
|
|
129
|
-
);
|
|
130
|
-
expect(signed, contains('Policy='));
|
|
131
|
-
expect(signed, contains('Signature='));
|
|
132
|
-
expect(signed, contains('Key-Pair-Id=KP123'));
|
|
133
|
-
expect(
|
|
134
|
-
verifyCloudfrontSignedUrl(signedUrl: signed, publicKeyPem: publicPem),
|
|
135
|
-
isTrue,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
final uri = Uri.parse(signed);
|
|
139
|
-
final policyJson = utf8.decode(_b64urlDecode(uri.queryParameters['Policy']!));
|
|
140
|
-
expect(policyJson, contains(url));
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('s3LambdaEdgeStorage signs s3 download urls', () async {
|
|
144
|
-
final store = Store();
|
|
145
|
-
final plugin = s3LambdaEdgeStorage(
|
|
146
|
-
_edgeConfig(store, privatePem, publicBaseUrl: 'https://cdn.example.com'),
|
|
147
|
-
);
|
|
148
|
-
final RuntimeStorageProfile runtime = plugin.profiles.runtime!;
|
|
149
|
-
|
|
150
|
-
final res = await runtime.getDownloadUrl('s3://test-bucket/bundles/x.zip');
|
|
151
|
-
final fileUrl = res['fileUrl']!;
|
|
152
|
-
expect(fileUrl, contains('Key-Pair-Id=KP123'));
|
|
153
|
-
expect(
|
|
154
|
-
verifyCloudfrontSignedUrl(signedUrl: fileUrl, publicKeyPem: publicPem),
|
|
155
|
-
isTrue,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
final uri = Uri.parse(fileUrl);
|
|
159
|
-
final policy = utf8.decode(_b64urlDecode(uri.queryParameters['Policy']!));
|
|
160
|
-
expect(policy, contains('https://cdn.example.com/bundles/x.zip'));
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('s3LambdaEdgeStorage resolves publicBaseUrl via function', () async {
|
|
164
|
-
final store = Store();
|
|
165
|
-
final plugin = s3LambdaEdgeStorage(
|
|
166
|
-
_edgeConfig(
|
|
167
|
-
store,
|
|
168
|
-
privatePem,
|
|
169
|
-
publicBaseUrlResolver: () async => 'https://edge.acme.dev',
|
|
170
|
-
),
|
|
171
|
-
);
|
|
172
|
-
final runtime = plugin.profiles.runtime!;
|
|
173
|
-
final res = await runtime.getDownloadUrl('s3://test-bucket/a.zip');
|
|
174
|
-
expect(
|
|
175
|
-
verifyCloudfrontSignedUrl(
|
|
176
|
-
signedUrl: res['fileUrl']!,
|
|
177
|
-
publicKeyPem: publicPem,
|
|
178
|
-
),
|
|
179
|
-
isTrue,
|
|
180
|
-
);
|
|
181
|
-
expect(res['fileUrl'], contains('https://edge.acme.dev/a.zip'));
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('plugin name carries WithCloudFrontSignedUrl suffix', () {
|
|
185
|
-
final store = Store();
|
|
186
|
-
final plugin = s3LambdaEdgeStorage(_edgeConfig(store, privatePem));
|
|
187
|
-
expect(plugin.name, 's3StorageWithCloudFrontSignedUrl');
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
File without changes
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export 'src/standalone_config.dart'
|
|
2
|
-
show
|
|
3
|
-
StandaloneClientFactory,
|
|
4
|
-
StandaloneRepositoryConfig,
|
|
5
|
-
StandaloneRepositoryRoutes,
|
|
6
|
-
StandaloneStorageConfig,
|
|
7
|
-
StandaloneStorageRoutes,
|
|
8
|
-
buildPath,
|
|
9
|
-
joinUrl,
|
|
10
|
-
mergeHeaders;
|
|
11
|
-
export 'src/standalone_database.dart' show standaloneRepository;
|
|
12
|
-
export 'src/standalone_storage.dart' show standaloneStorage;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import 'dart:convert' show jsonEncode;
|
|
2
|
-
|
|
3
|
-
import 'package:http/http.dart' as http;
|
|
4
|
-
import 'package:http/http.dart';
|
|
5
|
-
|
|
6
|
-
/// A factory that returns the [http.Client] used by standalone plugins.
|
|
7
|
-
///
|
|
8
|
-
/// Injectable so tests can supply a mock client.
|
|
9
|
-
typedef StandaloneClientFactory = Client Function();
|
|
10
|
-
|
|
11
|
-
/// Configuration for the standalone repository (the database half of the
|
|
12
|
-
/// self-hosted backend).
|
|
13
|
-
class StandaloneRepositoryConfig {
|
|
14
|
-
const StandaloneRepositoryConfig({
|
|
15
|
-
required this.baseUrl,
|
|
16
|
-
this.commonHeaders,
|
|
17
|
-
this.routes,
|
|
18
|
-
this.clientFactory,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
final String baseUrl;
|
|
22
|
-
final Map<String, String>? commonHeaders;
|
|
23
|
-
final StandaloneRepositoryRoutes? routes;
|
|
24
|
-
final StandaloneClientFactory? clientFactory;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Overridable route templates for the repository REST contract.
|
|
28
|
-
class StandaloneRepositoryRoutes {
|
|
29
|
-
const StandaloneRepositoryRoutes({
|
|
30
|
-
this.create = '/api/bundles',
|
|
31
|
-
this.list = '/api/bundles',
|
|
32
|
-
this.channels = '/api/bundles/channels',
|
|
33
|
-
this.retrieve = '/api/bundles/:id',
|
|
34
|
-
this.update = '/api/bundles/:id',
|
|
35
|
-
this.delete = '/api/bundles/:id',
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
final String create;
|
|
39
|
-
final String list;
|
|
40
|
-
final String channels;
|
|
41
|
-
final String retrieve;
|
|
42
|
-
final String update;
|
|
43
|
-
final String delete;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/// Configuration for the standalone storage (the storage half of the
|
|
47
|
-
/// self-hosted backend).
|
|
48
|
-
class StandaloneStorageConfig {
|
|
49
|
-
const StandaloneStorageConfig({
|
|
50
|
-
required this.baseUrl,
|
|
51
|
-
this.commonHeaders,
|
|
52
|
-
this.routes,
|
|
53
|
-
this.clientFactory,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
final String baseUrl;
|
|
57
|
-
final Map<String, String>? commonHeaders;
|
|
58
|
-
final StandaloneStorageRoutes? routes;
|
|
59
|
-
final StandaloneClientFactory? clientFactory;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/// Overridable route templates for the storage REST contract.
|
|
63
|
-
class StandaloneStorageRoutes {
|
|
64
|
-
const StandaloneStorageRoutes({
|
|
65
|
-
this.upload = '/api/upload',
|
|
66
|
-
this.getDownloadUrl = '/api/getDownloadUrl',
|
|
67
|
-
this.delete = '/api/delete',
|
|
68
|
-
this.readText = '/api/readText',
|
|
69
|
-
this.list = '/api/list',
|
|
70
|
-
this.serveFile = '/api/_file',
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
final String upload;
|
|
74
|
-
final String getDownloadUrl;
|
|
75
|
-
final String delete;
|
|
76
|
-
final String readText;
|
|
77
|
-
final String list;
|
|
78
|
-
final String serveFile;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// Build a request path from a route template that may contain `:id`.
|
|
82
|
-
String buildPath(String template, {String? id}) {
|
|
83
|
-
var path = template;
|
|
84
|
-
if (id != null) path = path.replaceAll(':id', id);
|
|
85
|
-
return path;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Join [baseUrl] and [path], ensuring exactly one `/` between them.
|
|
89
|
-
String joinUrl(String baseUrl, String path) {
|
|
90
|
-
final base = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
|
|
91
|
-
final p = path.startsWith('/') ? path : '/$path';
|
|
92
|
-
return '$base$p';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/// Merge [commonHeaders] with extra per-request headers.
|
|
96
|
-
Map<String, String> mergeHeaders(
|
|
97
|
-
Map<String, String>? common,
|
|
98
|
-
Map<String, String>? extra,
|
|
99
|
-
) {
|
|
100
|
-
final result = <String, String>{};
|
|
101
|
-
if (common != null) result.addAll(common);
|
|
102
|
-
if (extra != null) result.addAll(extra);
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/// Envelope helper: standalone create expects `[bundle]`.
|
|
107
|
-
String encodeBundleList(Map<String, dynamic> bundle) => jsonEncode([bundle]);
|