@_nazmiforreal/flutter-ota 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/bin/flutter-patcher.js +48 -39
  2. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  3. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  4. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  5. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  6. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  7. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  8. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  21. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  22. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  23. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  24. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  25. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  30. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  31. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  38. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  39. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  40. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  41. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  42. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  43. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  44. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  45. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  46. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  47. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  48. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  49. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  50. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  51. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  52. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  53. package/dart-src/packages/core/lib/src/status.dart +24 -0
  54. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  55. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  56. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  57. package/dart-src/packages/core/pubspec.yaml +9 -0
  58. package/dart-src/packages/core/tool/dbg.dart +6 -0
  59. package/dart-src/plugins/aws/.gitkeep +0 -0
  60. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  61. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  62. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  63. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  64. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  65. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  66. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  67. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  68. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  69. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  70. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  71. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  72. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  73. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  74. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  75. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  76. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  77. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  78. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  79. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  81. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  82. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  83. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  84. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  85. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  86. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  87. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  88. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  89. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  90. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  91. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  92. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  93. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  94. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  95. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  96. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  97. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  98. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  99. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  100. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  101. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  102. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  103. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  104. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  105. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  106. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  107. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  108. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  109. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  110. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  111. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  112. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  113. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  114. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  115. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  116. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  117. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  118. package/dart-src/plugins/postgres/.gitkeep +0 -0
  119. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  120. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  121. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  122. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  123. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  124. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  125. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  126. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  127. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  128. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  129. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  130. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  131. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  132. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  133. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  134. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  135. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  136. package/dart-src/plugins/standalone/.gitkeep +0 -0
  137. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  138. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  139. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  140. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  141. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  142. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  143. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  144. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  145. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  146. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  147. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  148. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  149. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  150. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  151. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  152. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  153. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  154. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  155. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  156. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  157. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  158. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  159. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  160. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  161. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  162. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  163. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  164. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  165. package/package.json +2 -1
  166. package/scripts/postinstall.js +53 -25
  167. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,233 @@
1
+ import 'dart:convert' show jsonDecode, jsonEncode;
2
+
3
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
4
+ show
5
+ AppVersionGetBundlesArgs,
6
+ Bundle,
7
+ FingerprintGetBundlesArgs,
8
+ GetBundlesArgs,
9
+ UpdateInfo,
10
+ UpdateStatus;
11
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
12
+ show
13
+ AbstractDatabasePlugin,
14
+ BundleChange,
15
+ BundleChangeOperation,
16
+ DatabaseBundleQueryOptions,
17
+ Paginated,
18
+ PaginationInfo,
19
+ createDatabasePlugin;
20
+ import 'package:http/http.dart' as http;
21
+ import 'package:http/http.dart';
22
+
23
+ import 'standalone_config.dart'
24
+ show
25
+ StandaloneRepositoryConfig,
26
+ StandaloneRepositoryRoutes,
27
+ buildPath,
28
+ joinUrl,
29
+ mergeHeaders;
30
+
31
+ /// Double-curried standalone repository factory.
32
+ ///
33
+ /// Faithful port of hot-updater `plugins/standalone/src/standaloneRepository.ts`.
34
+ DatabasePluginLike standaloneRepository(
35
+ StandaloneRepositoryConfig config, [
36
+ Object? hooks,
37
+ ]) =>
38
+ createDatabasePlugin<StandaloneRepositoryConfig>(
39
+ name: 'standalone-repository',
40
+ factory: (c) => _StandaloneRepository(c),
41
+ )(config);
42
+
43
+ /// Alias so the curried factory can be used uniformly with other backends.
44
+ typedef DatabasePluginLike = dynamic;
45
+
46
+ class _StandaloneRepository implements AbstractDatabasePlugin {
47
+ _StandaloneRepository(this.config)
48
+ : routes = config.routes ?? const StandaloneRepositoryRoutes();
49
+
50
+ final StandaloneRepositoryConfig config;
51
+ final StandaloneRepositoryRoutes routes;
52
+
53
+ Client _client() => config.clientFactory?.call() ?? http.Client();
54
+
55
+ Map<String, String> _headers([Map<String, String>? extra]) =>
56
+ mergeHeaders(config.commonHeaders, extra);
57
+
58
+ Future<dynamic> _decode(Response res) {
59
+ if (res.statusCode >= 400) {
60
+ throw StateError(
61
+ 'standalone repository request failed (${res.statusCode}): '
62
+ '${res.body}',
63
+ );
64
+ }
65
+ return Future.value(jsonDecode(res.body));
66
+ }
67
+
68
+ @override
69
+ bool get supportsCursorPagination => true;
70
+
71
+ @override
72
+ Future<List<String>> getChannels() async {
73
+ final client = _client();
74
+ final res = await client.get(
75
+ Uri.parse(joinUrl(config.baseUrl, routes.channels)),
76
+ headers: _headers({'Cache-Control': 'no-cache'}),
77
+ );
78
+ final decoded = await _decode(res) as Map<String, dynamic>;
79
+ final data = decoded['data'] as Map<String, dynamic>;
80
+ final channels = (data['channels'] as List?)?.cast<String>() ?? const [];
81
+ return channels;
82
+ }
83
+
84
+ @override
85
+ Future<Bundle?> getBundleById(String bundleId) async {
86
+ final client = _client();
87
+ final res = await client.get(
88
+ Uri.parse(joinUrl(config.baseUrl, buildPath(routes.retrieve, id: bundleId))),
89
+ headers: _headers({'Accept': 'application/json'}),
90
+ );
91
+ if (res.statusCode == 404) return null;
92
+ final decoded = await _decode(res);
93
+ return Bundle.fromJson(decoded as Map<String, dynamic>);
94
+ }
95
+
96
+ @override
97
+ Future<Paginated<List<Bundle>>> getBundles(
98
+ DatabaseBundleQueryOptions options,
99
+ ) async {
100
+ final client = _client();
101
+ final query = <String, String>{};
102
+ final where = options.where;
103
+ if (where != null) {
104
+ if (where.channel != null) query['channel'] = where.channel!;
105
+ if (where.platform != null) query['platform'] = where.platform!.value;
106
+ if (where.enabled != null) query['enabled'] = '${where.enabled}';
107
+ final id = where.id;
108
+ if (id != null) {
109
+ if (id.eq != null) query['idEq'] = id.eq!;
110
+ if (id.gt != null) query['idGt'] = id.gt!;
111
+ if (id.gte != null) query['idGte'] = id.gte!;
112
+ if (id.lt != null) query['idLt'] = id.lt!;
113
+ if (id.lte != null) query['idLte'] = id.lte!;
114
+ if (id.ins != null) query['idIn'] = id.ins!.join(',');
115
+ }
116
+ if (where.targetAppVersion != null) {
117
+ query['targetAppVersion'] = where.targetAppVersion!;
118
+ }
119
+ if (where.targetAppVersionIn != null) {
120
+ query['targetAppVersionIn'] = where.targetAppVersionIn!.join(',');
121
+ }
122
+ if (where.targetAppVersionNotNull != null) {
123
+ query['targetAppVersionNotNull'] = '${where.targetAppVersionNotNull}';
124
+ }
125
+ if (where.fingerprintHash != null) {
126
+ query['fingerprintHash'] = where.fingerprintHash!;
127
+ }
128
+ }
129
+ query['limit'] = '${options.limit}';
130
+ if (options.page != null) query['page'] = '${options.page}';
131
+ if (options.offset != null) query['offset'] = '${options.offset}';
132
+ if (options.cursor?.after != null) query['after'] = options.cursor!.after!;
133
+ if (options.cursor?.before != null) {
134
+ query['before'] = options.cursor!.before!;
135
+ }
136
+
137
+ final uri = Uri.parse(joinUrl(config.baseUrl, routes.list))
138
+ .replace(queryParameters: query);
139
+ final res = await client.get(
140
+ uri,
141
+ headers: _headers({'Cache-Control': 'no-cache'}),
142
+ );
143
+ final decoded = await _decode(res) as Map<String, dynamic>;
144
+ final data = (decoded['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
145
+ final bundles = data.map(Bundle.fromJson).toList();
146
+ final pagination =
147
+ (decoded['pagination'] as Map?)?.cast<String, dynamic>() ?? {};
148
+ return Paginated(
149
+ data: bundles,
150
+ pagination: PaginationInfo(
151
+ total: pagination['total'] as int? ?? bundles.length,
152
+ hasNextPage: pagination['hasNextPage'] as bool? ?? false,
153
+ hasPreviousPage: pagination['hasPreviousPage'] as bool? ?? false,
154
+ currentPage: pagination['currentPage'] as int? ?? 1,
155
+ totalPages: pagination['totalPages'] as int? ?? 1,
156
+ nextCursor: pagination['nextCursor'] as String?,
157
+ previousCursor: pagination['previousCursor'] as String?,
158
+ ),
159
+ );
160
+ }
161
+
162
+ @override
163
+ Future<void> commitBundle({
164
+ required List<BundleChange> changedSets,
165
+ }) async {
166
+ final client = _client();
167
+ for (final change in changedSets) {
168
+ final bundle = change.data;
169
+ switch (change.operation) {
170
+ case BundleChangeOperation.insert:
171
+ final res = await client.post(
172
+ Uri.parse(joinUrl(config.baseUrl, routes.create)),
173
+ headers: _headers({'Content-Type': 'application/json'}),
174
+ body: jsonEncode([bundle.toJson()]),
175
+ );
176
+ await _decode(res);
177
+ case BundleChangeOperation.update:
178
+ final res = await client.patch(
179
+ Uri.parse(joinUrl(config.baseUrl, buildPath(routes.update, id: bundle.id))),
180
+ headers: _headers({'Content-Type': 'application/json'}),
181
+ body: jsonEncode(bundle.toJson()),
182
+ );
183
+ await _decode(res);
184
+ case BundleChangeOperation.delete:
185
+ final res = await client.delete(
186
+ Uri.parse(joinUrl(config.baseUrl, buildPath(routes.delete, id: bundle.id))),
187
+ );
188
+ await _decode(res);
189
+ }
190
+ }
191
+ }
192
+
193
+ @override
194
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
195
+ final client = _client();
196
+ final path = args is AppVersionGetBundlesArgs
197
+ ? _appVersionPath(args)
198
+ : _fingerprintPath(args);
199
+ final res = await client.get(
200
+ Uri.parse(joinUrl(config.baseUrl, path)),
201
+ headers: _headers({'Cache-Control': 'no-cache'}),
202
+ );
203
+ if (res.statusCode == 404) return null;
204
+ final decoded = await _decode(res) as Map<String, dynamic>;
205
+ if (decoded['status'] == 'UP_TO_DATE') return null;
206
+ return UpdateInfo(
207
+ id: decoded['id'] as String,
208
+ shouldForceUpdate: (decoded['shouldForceUpdate'] ?? false) as bool,
209
+ message: decoded['message'] as String?,
210
+ status: UpdateStatus.fromValue(
211
+ decoded['status'] as String? ?? 'update-available',
212
+ ),
213
+ storageUri: decoded['fileUrl'] as String?,
214
+ fileHash: decoded['fileHash'] as String?,
215
+ );
216
+ }
217
+
218
+ String _fingerprintPath(GetBundlesArgs args) {
219
+ final fp = args is FingerprintGetBundlesArgs ? args.fingerprintHash : 'unknown';
220
+ final cohort = args.cohort != null ? '/${args.cohort}' : '';
221
+ return '/api/fingerprint/${args.platform.value}/$fp/'
222
+ '${args.channel}/${args.minBundleId}/${args.bundleId}$cohort';
223
+ }
224
+
225
+ String _appVersionPath(AppVersionGetBundlesArgs args) {
226
+ final cohort = args.cohort != null ? '/${args.cohort}' : '';
227
+ return '/api/app-version/${args.platform.value}/${args.appVersion}/'
228
+ '${args.channel}/${args.minBundleId}/${args.bundleId}$cohort';
229
+ }
230
+
231
+ @override
232
+ Future<void> onUnmount() async {}
233
+ }
@@ -0,0 +1,175 @@
1
+ import 'dart:convert' show jsonDecode, jsonEncode;
2
+ import 'dart:io' show File;
3
+
4
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
5
+ show
6
+ NodeStorageProfile,
7
+ RuntimeStorageProfile,
8
+ StorageObject,
9
+ createUniversalStoragePlugin;
10
+ import 'package:http/http.dart' as http;
11
+ import 'package:http/http.dart';
12
+
13
+ import 'standalone_config.dart'
14
+ show
15
+ StandaloneStorageConfig,
16
+ StandaloneStorageRoutes,
17
+ joinUrl,
18
+ mergeHeaders;
19
+
20
+ /// Single-curried standalone storage factory.
21
+ ///
22
+ /// Faithful port of hot-updater `plugins/standalone/src/standaloneStorage.ts`.
23
+ StoragePluginLike standaloneStorage(StandaloneStorageConfig config) =>
24
+ createUniversalStoragePlugin<StandaloneStorageConfig>(
25
+ name: 'standalone-storage',
26
+ supportedProtocol: 'http',
27
+ factory: (c) {
28
+ final storage = _StandaloneStorage(c);
29
+ return (node: storage, runtime: storage);
30
+ },
31
+ )(config);
32
+
33
+ /// Alias so the curried factory can be used uniformly with other backends.
34
+ typedef StoragePluginLike = dynamic;
35
+
36
+ class _StandaloneStorage implements NodeStorageProfile, RuntimeStorageProfile {
37
+ _StandaloneStorage(this.config)
38
+ : routes = config.routes ?? const StandaloneStorageRoutes();
39
+
40
+ final StandaloneStorageConfig config;
41
+ final StandaloneStorageRoutes routes;
42
+
43
+ Client _client() => config.clientFactory?.call() ?? http.Client();
44
+
45
+ Map<String, String> _headers([Map<String, String>? extra]) =>
46
+ mergeHeaders(config.commonHeaders, extra);
47
+
48
+ // --- Node profile ---------------------------------------------------------
49
+
50
+ @override
51
+ Future<Map<String, String>> upload(String key, String filePath) async {
52
+ final client = _client();
53
+ final bytes = await File(filePath).readAsBytes();
54
+ final uri = Uri.parse(joinUrl(config.baseUrl, routes.upload))
55
+ .replace(queryParameters: {'key': key});
56
+ final res = await client.post(
57
+ uri,
58
+ headers: _headers({'Content-Type': 'application/octet-stream'}),
59
+ body: bytes,
60
+ );
61
+ if (res.statusCode >= 400) {
62
+ throw StateError(
63
+ 'standalone storage upload failed (${res.statusCode}): ${res.body}',
64
+ );
65
+ }
66
+ final decoded = jsonDecode(res.body) as Map<String, dynamic>;
67
+ final storageUri = decoded['storageUri'] as String?;
68
+ if (storageUri == null) {
69
+ throw StateError('standalone storage upload returned no storageUri');
70
+ }
71
+ return {'storageUri': storageUri};
72
+ }
73
+
74
+ @override
75
+ Future<bool> exists(String storageUri) async {
76
+ final fileUrl = (await getDownloadUrl(storageUri))['fileUrl'];
77
+ if (fileUrl == null) return false;
78
+ final client = _client();
79
+ final res = await client.head(Uri.parse(fileUrl));
80
+ return res.statusCode >= 200 && res.statusCode < 300;
81
+ }
82
+
83
+ @override
84
+ Future<void> delete(String storageUri) async {
85
+ final client = _client();
86
+ final res = await client.delete(
87
+ Uri.parse(joinUrl(config.baseUrl, routes.delete)),
88
+ headers: _headers({'Content-Type': 'application/json'}),
89
+ body: jsonEncode({'storageUri': storageUri}),
90
+ );
91
+ if (res.statusCode >= 400) {
92
+ throw StateError(
93
+ 'standalone storage delete failed (${res.statusCode}): ${res.body}',
94
+ );
95
+ }
96
+ }
97
+
98
+ @override
99
+ Future<void> downloadFile(String storageUri, String filePath) async {
100
+ final fileUrl = (await getDownloadUrl(storageUri))['fileUrl'];
101
+ if (fileUrl == null) {
102
+ throw StateError('standalone storage could not resolve download url');
103
+ }
104
+ final client = _client();
105
+ final res = await client.get(Uri.parse(fileUrl));
106
+ if (res.statusCode >= 400) {
107
+ throw StateError(
108
+ 'standalone storage download failed (${res.statusCode})',
109
+ );
110
+ }
111
+ await File(filePath).writeAsBytes(res.bodyBytes);
112
+ }
113
+
114
+ @override
115
+ Future<List<StorageObject>> listObjects([String? prefix]) async {
116
+ final client = _client();
117
+ final uri = Uri.parse(joinUrl(config.baseUrl, routes.list))
118
+ .replace(queryParameters: prefix != null ? {'prefix': prefix} : null);
119
+ final res = await client.get(uri);
120
+ if (res.statusCode >= 400) return const [];
121
+ final decoded = jsonDecode(res.body) as Map<String, dynamic>;
122
+ final data = (decoded['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
123
+ return data
124
+ .map((o) => StorageObject(
125
+ key: o['key'] as String,
126
+ storageUri: o['storageUri'] as String,
127
+ size: o['size'] as int? ?? 0,
128
+ ))
129
+ .toList();
130
+ }
131
+
132
+ @override
133
+ Future<void> deleteObjects(List<String> keys) async {
134
+ for (final key in keys) {
135
+ await delete(key);
136
+ }
137
+ }
138
+
139
+ // --- Runtime profile ------------------------------------------------------
140
+
141
+ @override
142
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async {
143
+ final client = _client();
144
+ final res = await client.post(
145
+ Uri.parse(joinUrl(config.baseUrl, routes.getDownloadUrl)),
146
+ headers: _headers({'Content-Type': 'application/json'}),
147
+ body: jsonEncode({'storageUri': storageUri}),
148
+ );
149
+ if (res.statusCode >= 400) {
150
+ throw StateError(
151
+ 'standalone storage getDownloadUrl failed (${res.statusCode}): '
152
+ '${res.body}',
153
+ );
154
+ }
155
+ final decoded = jsonDecode(res.body) as Map<String, dynamic>;
156
+ final fileUrl = decoded['fileUrl'] as String?;
157
+ if (fileUrl == null) {
158
+ throw StateError('standalone storage getDownloadUrl returned no fileUrl');
159
+ }
160
+ return {'fileUrl': fileUrl};
161
+ }
162
+
163
+ @override
164
+ Future<String?> readText(String storageUri) async {
165
+ final client = _client();
166
+ final res = await client.post(
167
+ Uri.parse(joinUrl(config.baseUrl, routes.readText)),
168
+ headers: _headers({'Content-Type': 'application/json'}),
169
+ body: jsonEncode({'storageUri': storageUri}),
170
+ );
171
+ if (res.statusCode >= 400) return null;
172
+ final decoded = jsonDecode(res.body) as Map<String, dynamic>;
173
+ return decoded['data'] as String?;
174
+ }
175
+ }
@@ -0,0 +1,18 @@
1
+ name: flutter_patcher_standalone
2
+ description: Standalone self-hosted backend for flutter_patcher (REST repository + storage).
3
+ version: 0.1.0
4
+ publish_to: none
5
+
6
+ environment:
7
+ sdk: ">=3.3.0 <4.0.0"
8
+
9
+ dependencies:
10
+ flutter_patcher_core:
11
+ path: ../../packages/core
12
+ flutter_patcher_plugin_core:
13
+ path: ../../plugins/plugin-core
14
+ http: ^1.2.0
15
+
16
+ dev_dependencies:
17
+ test: ^1.25.0
18
+ lints: ^4.0.0
@@ -0,0 +1,10 @@
1
+ /// Edge-function entrypoint for flutter_patcher_supabase.
2
+ ///
3
+ /// Mirrors hot-updater's `plugins/supabase/src/edge.ts`: exposes only the
4
+ /// edge-runnable variants (runtime-only database + storage plugins).
5
+ library;
6
+
7
+ export 'src/supabase_edge_function_database.dart'
8
+ show supabaseEdgeFunctionDatabase, SupabaseEdgeFunctionDatabaseConfig;
9
+ export 'src/supabase_edge_function_storage.dart'
10
+ show supabaseEdgeFunctionStorage, SupabaseEdgeFunctionStorageConfig;
@@ -0,0 +1,21 @@
1
+ /// flutter_patcher_supabase — Supabase backend for flutter_patcher.
2
+ ///
3
+ /// Faithful Dart translation of hot-updater's `plugins/supabase`:
4
+ /// database + storage plugins (direct + edge-function variants) over the
5
+ /// Supabase REST (PostgREST) API.
6
+ library;
7
+
8
+ export 'src/supabase_config.dart';
9
+ export 'src/supabase_client_adapter.dart'
10
+ show SupabaseClientLike, SupabaseClientFactory;
11
+ export 'src/supabase_client_http.dart' show createSupabaseHttpClient;
12
+ export 'src/supabase_database.dart'
13
+ show supabaseDatabase, SupabaseDatabaseConfig;
14
+ export 'src/supabase_storage.dart'
15
+ show supabaseStorage, SupabaseStorageConfig, parseSupabaseStorageUri;
16
+ export 'src/supabase_signed_url_batcher.dart'
17
+ show createSupabaseSignedUrlBatcher, ResolveSignedUrl;
18
+ export 'src/supabase_edge_function_database.dart'
19
+ show supabaseEdgeFunctionDatabase, SupabaseEdgeFunctionDatabaseConfig;
20
+ export 'src/supabase_edge_function_storage.dart'
21
+ show supabaseEdgeFunctionStorage, SupabaseEdgeFunctionStorageConfig;
@@ -0,0 +1,129 @@
1
+ /// Faithful port of hot-updater `plugins/supabase/src/supabaseBundleMapper.ts`.
2
+ library;
3
+
4
+ import 'dart:convert';
5
+
6
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart';
7
+
8
+ import 'types.dart';
9
+
10
+ const String bundleSelectColumns =
11
+ 'id, channel, enabled, platform, should_force_update, file_hash, '
12
+ 'git_commit_hash, message, fingerprint_hash, target_app_version, '
13
+ 'storage_uri, metadata, manifest_storage_uri, manifest_file_hash, '
14
+ 'asset_base_storage_uri, rollout_cohort_count, target_cohorts';
15
+
16
+ /// Normalize metadata from various shapes to a plain map.
17
+ Map<String, Object?>? _normalizeMetadata(Object? value) {
18
+ if (value == null) return null;
19
+
20
+ if (value is String) {
21
+ try {
22
+ final parsed = jsonDecode(value);
23
+ return _normalizeMetadata(parsed);
24
+ } catch (_) {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ if (value is Map) return value.cast<String, Object?>();
30
+
31
+ return null;
32
+ }
33
+
34
+ /// Build the compound primary key for a patch row.
35
+ String _buildBundlePatchId(String bundleId, String baseBundleId) =>
36
+ '$bundleId:$baseBundleId';
37
+
38
+ /// Map a Supabase bundle row + optional patch rows to a [Bundle].
39
+ Bundle mapRowToBundle(
40
+ SupabaseBundleRow row, [
41
+ List<SupabaseBundlePatchRow> patchRows = const [],
42
+ ]) {
43
+ final rawMetadata = _normalizeMetadata(row.metadata);
44
+ final patches = patchRows.toList()
45
+ ..sort((a, b) {
46
+ final cmp = a.orderIndex.compareTo(b.orderIndex);
47
+ if (cmp != 0) return cmp;
48
+ return a.baseBundleId.compareTo(b.baseBundleId);
49
+ });
50
+
51
+ final patchArtifacts = patches
52
+ .map((p) => BundlePatchArtifact(
53
+ baseBundleId: p.baseBundleId,
54
+ baseFileHash: p.baseFileHash,
55
+ patchFileHash: p.patchFileHash,
56
+ patchStorageUri: p.patchStorageUri,
57
+ ))
58
+ .toList();
59
+
60
+ final primaryPatch =
61
+ patchArtifacts.isNotEmpty ? patchArtifacts.first : null;
62
+
63
+ return Bundle(
64
+ id: row.id,
65
+ channel: row.channel,
66
+ enabled: row.enabled,
67
+ shouldForceUpdate: row.shouldForceUpdate,
68
+ fileHash: row.fileHash,
69
+ gitCommitHash: row.gitCommitHash,
70
+ message: row.message,
71
+ platform: Platform.fromValue(row.platform),
72
+ targetAppVersion: row.targetAppVersion,
73
+ fingerprintHash: row.fingerprintHash,
74
+ storageUri: row.storageUri,
75
+ metadata:
76
+ rawMetadata != null ? BundleMetadata.fromJson(rawMetadata) : null,
77
+ manifestStorageUri: row.manifestStorageUri,
78
+ manifestFileHash: row.manifestFileHash,
79
+ assetBaseStorageUri: row.assetBaseStorageUri,
80
+ patches: patchArtifacts,
81
+ patchBaseBundleId: primaryPatch?.baseBundleId,
82
+ patchBaseFileHash: primaryPatch?.baseFileHash,
83
+ patchFileHash: primaryPatch?.patchFileHash,
84
+ patchStorageUri: primaryPatch?.patchStorageUri,
85
+ rolloutCohortCount:
86
+ row.rolloutCohortCount ?? defaultRolloutCohortCount,
87
+ targetCohorts: row.targetCohorts,
88
+ );
89
+ }
90
+
91
+ /// Map a [Bundle] to a Supabase row for upsert.
92
+ SupabaseBundleRow bundleToRow(Bundle bundle) => SupabaseBundleRow(
93
+ id: bundle.id,
94
+ channel: bundle.channel,
95
+ enabled: bundle.enabled,
96
+ shouldForceUpdate: bundle.shouldForceUpdate,
97
+ fileHash: bundle.fileHash,
98
+ gitCommitHash: bundle.gitCommitHash,
99
+ message: bundle.message,
100
+ platform: bundle.platform.value,
101
+ targetAppVersion: bundle.targetAppVersion,
102
+ fingerprintHash: bundle.fingerprintHash,
103
+ storageUri: bundle.storageUri,
104
+ metadata: stripBundleArtifactMetadata(bundle.metadata?.toJson()),
105
+ manifestStorageUri: getManifestStorageUri(bundle),
106
+ manifestFileHash: getManifestFileHash(bundle),
107
+ assetBaseStorageUri: getAssetBaseStorageUri(bundle),
108
+ rolloutCohortCount:
109
+ bundle.rolloutCohortCount ?? defaultRolloutCohortCount,
110
+ targetCohorts: bundle.targetCohorts,
111
+ );
112
+
113
+ /// Map a [Bundle] to its patch rows for upsert.
114
+ List<SupabaseBundlePatchRow> bundleToPatchRows(Bundle bundle) {
115
+ final patchArtifacts = getBundlePatches(bundle);
116
+ return [
117
+ for (var i = 0; i < patchArtifacts.length; i++)
118
+ SupabaseBundlePatchRow(
119
+ id: _buildBundlePatchId(
120
+ bundle.id, patchArtifacts[i].baseBundleId),
121
+ bundleId: bundle.id,
122
+ baseBundleId: patchArtifacts[i].baseBundleId,
123
+ baseFileHash: patchArtifacts[i].baseFileHash,
124
+ patchFileHash: patchArtifacts[i].patchFileHash,
125
+ patchStorageUri: patchArtifacts[i].patchStorageUri,
126
+ orderIndex: i,
127
+ ),
128
+ ];
129
+ }