@_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,968 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHmac } from "node:crypto";
3
+ import {
4
+ access,
5
+ mkdir,
6
+ mkdtemp,
7
+ readdir,
8
+ readFile,
9
+ rm,
10
+ symlink,
11
+ writeFile,
12
+ } from "node:fs/promises";
13
+ import path from "node:path";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
15
+
16
+ import { resolvePackageVersion, transformEnv } from "@hot-updater/cli-tools";
17
+ import {
18
+ type Bundle,
19
+ type GetBundlesArgs,
20
+ NIL_UUID,
21
+ type UpdateInfo,
22
+ } from "@hot-updater/core";
23
+ import { createHotUpdater } from "@hot-updater/server";
24
+ import {
25
+ setupBsdiffManifestUpdateInfoTestSuite,
26
+ setupGetUpdateInfoTestSuite,
27
+ } from "@hot-updater/test-utils";
28
+ import { createClient } from "@supabase/supabase-js";
29
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
30
+
31
+ import {
32
+ assertDockerComposeAvailable,
33
+ findOpenPort,
34
+ runCheckedCommand,
35
+ spawnRuntime,
36
+ stopRuntime,
37
+ formatRuntimeLogs,
38
+ waitForHttpOk,
39
+ } from "../../../../packages/test-utils/src/runtimeProcess";
40
+ import { supabaseDatabase } from "../../src/supabaseDatabase";
41
+ import { supabaseStorage } from "../../src/supabaseStorage";
42
+
43
+ const __filename = fileURLToPath(import.meta.url);
44
+ const __dirname = path.dirname(__filename);
45
+ const WORKSPACE_ROOT = path.resolve(__dirname, "../../../..");
46
+ const FUNCTION_NAME = "hot-updater-function";
47
+ const FUNCTION_BASE_PATH = `/${FUNCTION_NAME}`;
48
+ const HOT_UPDATER_BASE_PATH = "/";
49
+ const LEGACY_HOT_UPDATER_BASE_PATH = "/api/check-update";
50
+ const BUCKET_NAME = "hot-updater-bundles";
51
+ const DENO_DOCKER_IMAGE = "denoland/deno:alpine";
52
+ const DENO_CACHE_VOLUME = "hot-updater-supabase-deno-cache";
53
+ const POSTGRES_IMAGE = "postgres:15-alpine";
54
+ const POSTGREST_IMAGE = "postgrest/postgrest:v14.6";
55
+ const STORAGE_IMAGE = "supabase/storage-api:v1.44.2";
56
+ const IMGPROXY_IMAGE = "darthsim/imgproxy:v3.30.1";
57
+ const NGINX_IMAGE = "nginx:1.27-alpine";
58
+ const POSTGRES_PASSWORD = "postgres";
59
+ const POSTGRES_DB = "postgres";
60
+ const JWT_SECRET = "super-secret-jwt-token-with-at-least-32-chars";
61
+ const JWT_EXPIRY_SECONDS = 60 * 60 * 24 * 365;
62
+ const ANON_KEY = createLegacyJwt("anon");
63
+ const SERVICE_ROLE_KEY = createLegacyJwt("service_role");
64
+ const REQUIRED_BUILD_ARTIFACTS = [
65
+ {
66
+ command: "pnpm --filter @hot-updater/core build",
67
+ path: path.join(WORKSPACE_ROOT, "packages/core/dist/index.mjs"),
68
+ },
69
+ {
70
+ command: "pnpm --filter @hot-updater/server build",
71
+ path: path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
72
+ },
73
+ {
74
+ command: "pnpm --filter @hot-updater/plugin-core build",
75
+ path: path.join(WORKSPACE_ROOT, "plugins/plugin-core/dist/index.mjs"),
76
+ },
77
+ {
78
+ command: "pnpm --filter @hot-updater/supabase build",
79
+ path: path.join(WORKSPACE_ROOT, "plugins/supabase/dist/index.mjs"),
80
+ },
81
+ ] as const;
82
+
83
+ assertDockerComposeAvailable(
84
+ "supabase edge runtime acceptance requires Docker Compose and a running Docker daemon.",
85
+ );
86
+
87
+ const ensureBuiltArtifacts = async (
88
+ artifacts: ReadonlyArray<{ command: string; path: string }>,
89
+ ) => {
90
+ for (const artifact of artifacts) {
91
+ try {
92
+ await access(artifact.path);
93
+ } catch {
94
+ throw new Error(
95
+ `Missing built artifact at ${artifact.path}. Run \`${artifact.command}\` before running this test.`,
96
+ );
97
+ }
98
+ }
99
+ };
100
+
101
+ const createCanonicalPath = (args: GetBundlesArgs) => {
102
+ const channel = args.channel ?? "production";
103
+ const minBundleId = args.minBundleId ?? NIL_UUID;
104
+ const cohortSegment = args.cohort
105
+ ? `/${encodeURIComponent(args.cohort)}`
106
+ : "";
107
+ const joinHotUpdaterPath = (routePath: string) =>
108
+ HOT_UPDATER_BASE_PATH === "/"
109
+ ? routePath
110
+ : `${HOT_UPDATER_BASE_PATH}${routePath}`;
111
+
112
+ if (args._updateStrategy === "appVersion") {
113
+ return joinHotUpdaterPath(
114
+ `/app-version/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.appVersion)}/${encodeURIComponent(channel)}/${encodeURIComponent(minBundleId)}/${encodeURIComponent(args.bundleId)}${cohortSegment}`,
115
+ );
116
+ }
117
+
118
+ return joinHotUpdaterPath(
119
+ `/fingerprint/${encodeURIComponent(args.platform)}/${encodeURIComponent(args.fingerprintHash)}/${encodeURIComponent(channel)}/${encodeURIComponent(minBundleId)}/${encodeURIComponent(args.bundleId)}${cohortSegment}`,
120
+ );
121
+ };
122
+
123
+ const toRuntimeBundle = (bundle: Bundle): Bundle => {
124
+ return {
125
+ ...bundle,
126
+ storageUri: `supabase-storage://${BUCKET_NAME}/${bundle.id}/bundle.zip`,
127
+ };
128
+ };
129
+
130
+ describe.sequential("supabase edge runtime acceptance", () => {
131
+ let runtimeRoot: string | undefined;
132
+ let storageRepoPath = "";
133
+ let composeFilePath = "";
134
+ let composeProjectName = "";
135
+ let gatewayPort = 0;
136
+ let edgePort = 0;
137
+ let gatewayBaseUrl = "";
138
+ let edgeRuntime: ReturnType<typeof spawnRuntime> | undefined;
139
+ let seedHotUpdater: ReturnType<typeof createHotUpdater>;
140
+ let supabaseAdmin: ReturnType<typeof createClient>;
141
+
142
+ beforeAll(async () => {
143
+ await ensureBuiltArtifacts(REQUIRED_BUILD_ARTIFACTS);
144
+
145
+ runtimeRoot = await mkdtemp(
146
+ path.join(WORKSPACE_ROOT, "plugins/supabase/runtime-acceptance-"),
147
+ );
148
+ storageRepoPath = path.join(runtimeRoot, "storage-repo");
149
+ gatewayPort = await findOpenPort();
150
+ edgePort = await findOpenPort();
151
+ gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
152
+ composeProjectName = `hot-updater-supabase-${process.pid}-${Date.now()}`;
153
+ composeFilePath = path.join(runtimeRoot, "docker-compose.yml");
154
+
155
+ runCheckedCommand({
156
+ command: "git",
157
+ args: [
158
+ "clone",
159
+ "--depth",
160
+ "1",
161
+ "https://github.com/supabase/storage.git",
162
+ storageRepoPath,
163
+ ],
164
+ cwd: WORKSPACE_ROOT,
165
+ });
166
+
167
+ await writeSupabaseRuntimeFiles({
168
+ runtimeRoot,
169
+ gatewayPort,
170
+ storageRepoPath,
171
+ });
172
+
173
+ try {
174
+ runCheckedCommand({
175
+ command: "docker",
176
+ args: [
177
+ "compose",
178
+ "-p",
179
+ composeProjectName,
180
+ "-f",
181
+ composeFilePath,
182
+ "up",
183
+ "-d",
184
+ ],
185
+ cwd: WORKSPACE_ROOT,
186
+ });
187
+ } catch (error) {
188
+ let dbLogs = "";
189
+
190
+ try {
191
+ const result = spawnSync(
192
+ "docker",
193
+ [
194
+ "compose",
195
+ "-p",
196
+ composeProjectName,
197
+ "-f",
198
+ composeFilePath,
199
+ "logs",
200
+ "--no-color",
201
+ "db",
202
+ ],
203
+ {
204
+ cwd: WORKSPACE_ROOT,
205
+ encoding: "utf8",
206
+ },
207
+ );
208
+ dbLogs = [result.stdout, result.stderr].filter(Boolean).join("\n");
209
+ } catch {
210
+ dbLogs = "failed to collect database logs";
211
+ }
212
+
213
+ throw new Error(
214
+ [
215
+ error instanceof Error ? error.message : String(error),
216
+ "",
217
+ "Database logs:",
218
+ dbLogs,
219
+ ].join("\n"),
220
+ );
221
+ }
222
+
223
+ await waitForRestApiReady(gatewayBaseUrl, 180_000);
224
+ await waitForUrlOk(`${gatewayBaseUrl}/storage/v1/status`, 180_000);
225
+
226
+ supabaseAdmin = createClient(gatewayBaseUrl, SERVICE_ROLE_KEY);
227
+ await ensureBucketExists(supabaseAdmin);
228
+
229
+ seedHotUpdater = createHotUpdater({
230
+ database: supabaseDatabase({
231
+ supabaseUrl: gatewayBaseUrl,
232
+ supabaseAnonKey: SERVICE_ROLE_KEY,
233
+ }),
234
+ storages: [
235
+ supabaseStorage({
236
+ supabaseUrl: gatewayBaseUrl,
237
+ supabaseAnonKey: SERVICE_ROLE_KEY,
238
+ bucketName: BUCKET_NAME,
239
+ }),
240
+ ],
241
+ basePath: HOT_UPDATER_BASE_PATH,
242
+ routes: {
243
+ updateCheck: true,
244
+ bundles: false,
245
+ },
246
+ });
247
+
248
+ edgeRuntime = spawnRuntime({
249
+ command: "docker",
250
+ args: [
251
+ "run",
252
+ "--rm",
253
+ "--network",
254
+ `${composeProjectName}_default`,
255
+ "--add-host",
256
+ "host.docker.internal:host-gateway",
257
+ "-p",
258
+ `127.0.0.1:${edgePort}:8000`,
259
+ "-e",
260
+ `SUPABASE_URL=http://host.docker.internal:${gatewayPort}`,
261
+ "-e",
262
+ `SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}`,
263
+ "-e",
264
+ "DENO_DIR=/deno-dir",
265
+ "-v",
266
+ `${WORKSPACE_ROOT}:${WORKSPACE_ROOT}:ro`,
267
+ "-v",
268
+ `${runtimeRoot}:${runtimeRoot}`,
269
+ "-v",
270
+ `${DENO_CACHE_VOLUME}:/deno-dir`,
271
+ "-w",
272
+ runtimeRoot,
273
+ DENO_DOCKER_IMAGE,
274
+ "run",
275
+ "--no-lock",
276
+ "--node-modules-dir=manual",
277
+ "--allow-env",
278
+ "--allow-net",
279
+ "--allow-read",
280
+ "--allow-sys",
281
+ "--unstable-sloppy-imports",
282
+ "--import-map",
283
+ path.join(runtimeRoot, "import_map.json"),
284
+ path.join(runtimeRoot, "supabase/edge-functions/index.ts"),
285
+ ],
286
+ cwd: WORKSPACE_ROOT,
287
+ });
288
+
289
+ await waitForHttpOk({
290
+ url: `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/ping`,
291
+ child: edgeRuntime.child,
292
+ logs: edgeRuntime.logs,
293
+ timeoutMs: 90_000,
294
+ });
295
+ }, 300_000);
296
+
297
+ beforeEach(async () => {
298
+ if (!supabaseAdmin) {
299
+ throw new Error("Supabase admin client was not initialized.");
300
+ }
301
+
302
+ const { error } = await supabaseAdmin
303
+ .from("bundles")
304
+ .delete()
305
+ .neq("id", NIL_UUID);
306
+
307
+ if (error) {
308
+ throw error;
309
+ }
310
+ });
311
+
312
+ afterAll(async () => {
313
+ if (edgeRuntime) {
314
+ await stopRuntime(edgeRuntime.child);
315
+ }
316
+
317
+ if (composeFilePath) {
318
+ runCheckedCommand({
319
+ command: "docker",
320
+ args: [
321
+ "compose",
322
+ "-p",
323
+ composeProjectName,
324
+ "-f",
325
+ composeFilePath,
326
+ "down",
327
+ "-v",
328
+ "--remove-orphans",
329
+ ],
330
+ cwd: WORKSPACE_ROOT,
331
+ });
332
+ }
333
+
334
+ if (runtimeRoot) {
335
+ await rm(runtimeRoot, { recursive: true, force: true });
336
+ }
337
+ }, 60_000);
338
+
339
+ const seedRuntimeBundles = async (bundles: Bundle[]) => {
340
+ for (const bundle of bundles.map(toRuntimeBundle)) {
341
+ await seedHotUpdater.insertBundle(bundle);
342
+ }
343
+ };
344
+
345
+ const requestUpdateInfo = async (
346
+ args: GetBundlesArgs,
347
+ ): Promise<UpdateInfo | null> => {
348
+ const response = await fetch(
349
+ `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}${createCanonicalPath(args)}`,
350
+ );
351
+
352
+ if (!response.ok) {
353
+ throw new Error(
354
+ [
355
+ `Edge runtime returned ${response.status} ${response.statusText}`,
356
+ await response.text(),
357
+ edgeRuntime ? formatRuntimeLogs(edgeRuntime.logs) : "",
358
+ ].join("\n\n"),
359
+ );
360
+ }
361
+
362
+ return response.json();
363
+ };
364
+
365
+ const getUpdateInfo = async (bundles: Bundle[], args: GetBundlesArgs) => {
366
+ if (!supabaseAdmin) {
367
+ throw new Error("Supabase admin client was not initialized.");
368
+ }
369
+
370
+ for (const bundle of bundles) {
371
+ await uploadBundleObject(supabaseAdmin, bundle.id);
372
+ }
373
+ await seedRuntimeBundles(bundles);
374
+ return requestUpdateInfo(args);
375
+ };
376
+
377
+ setupGetUpdateInfoTestSuite({
378
+ getUpdateInfo,
379
+ manifestArtifacts: {
380
+ prepareArtifacts: async (fixture) => {
381
+ await Promise.all([
382
+ uploadStorageObject(
383
+ supabaseAdmin,
384
+ `${fixture.currentBundleId}/manifest.json`,
385
+ JSON.stringify(fixture.currentManifest),
386
+ "application/json",
387
+ ),
388
+ uploadStorageObject(
389
+ supabaseAdmin,
390
+ `${fixture.nextBundleId}/manifest.json`,
391
+ JSON.stringify(fixture.nextManifest),
392
+ "application/json",
393
+ ),
394
+ uploadStorageObject(
395
+ supabaseAdmin,
396
+ `${fixture.nextBundleId}/files/${fixture.changedAssetPath}.br`,
397
+ "next-bundle-bytes",
398
+ "application/javascript",
399
+ ),
400
+ ]);
401
+
402
+ return {
403
+ currentArtifacts: {
404
+ assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/files`,
405
+ manifestFileHash: "sig:manifest-current",
406
+ manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/manifest.json`,
407
+ },
408
+ nextArtifacts: {
409
+ assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/files`,
410
+ manifestFileHash: "sig:manifest-next",
411
+ manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
412
+ },
413
+ };
414
+ },
415
+ expectFileUrl: (fileUrl, fixture) => {
416
+ expect(fileUrl).toContain(
417
+ `/storage/v1/object/sign/${BUCKET_NAME}/${fixture.nextBundleId}/files/${fixture.changedAssetPath}.br`,
418
+ );
419
+ },
420
+ expectManifestUrl: (manifestUrl, fixture) => {
421
+ expect(manifestUrl).toContain(
422
+ `/storage/v1/object/sign/${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
423
+ );
424
+ },
425
+ },
426
+ });
427
+
428
+ setupBsdiffManifestUpdateInfoTestSuite({
429
+ seedBundles: seedRuntimeBundles,
430
+ getUpdateInfo: requestUpdateInfo,
431
+ prepareArtifacts: async (fixture) => {
432
+ await Promise.all([
433
+ uploadStorageObject(
434
+ supabaseAdmin,
435
+ `${fixture.currentBundleId}/manifest.json`,
436
+ JSON.stringify(fixture.currentManifest),
437
+ "application/json",
438
+ ),
439
+ uploadStorageObject(
440
+ supabaseAdmin,
441
+ `${fixture.nextBundleId}/manifest.json`,
442
+ JSON.stringify(fixture.nextManifest),
443
+ "application/json",
444
+ ),
445
+ uploadStorageObject(
446
+ supabaseAdmin,
447
+ `${fixture.nextBundleId}/files/${fixture.assetPath}`,
448
+ "next-bundle-bytes",
449
+ "application/javascript",
450
+ ),
451
+ uploadStorageObject(
452
+ supabaseAdmin,
453
+ fixture.patchPath,
454
+ "patch-bytes",
455
+ "application/octet-stream",
456
+ ),
457
+ uploadBundleObject(supabaseAdmin, fixture.currentBundleId),
458
+ uploadBundleObject(supabaseAdmin, fixture.nextBundleId),
459
+ ]);
460
+
461
+ return {
462
+ currentArtifacts: {
463
+ assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/files`,
464
+ manifestFileHash: "sig:manifest-current",
465
+ manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.currentBundleId}/manifest.json`,
466
+ },
467
+ nextArtifacts: {
468
+ assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/files`,
469
+ manifestFileHash: "sig:manifest-next",
470
+ manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.nextBundleId}/manifest.json`,
471
+ patches: [
472
+ {
473
+ baseBundleId: fixture.currentBundleId,
474
+ baseFileHash: "hash-old-bundle",
475
+ patchFileHash: "hash-bsdiff",
476
+ patchStorageUri: `supabase-storage://${BUCKET_NAME}/${fixture.patchPath}`,
477
+ },
478
+ ],
479
+ },
480
+ };
481
+ },
482
+ expectPatchUrl: (patchUrl, fixture) => {
483
+ expect(patchUrl).toContain(
484
+ `/storage/v1/object/sign/${BUCKET_NAME}/${fixture.patchPath}`,
485
+ );
486
+ },
487
+ });
488
+
489
+ it("serves canonical routes from the edge function entrypoint", async () => {
490
+ const bundle = toRuntimeBundle({
491
+ id: "00000000-0000-0000-0000-000000000001",
492
+ platform: "ios",
493
+ targetAppVersion: "1.0",
494
+ shouldForceUpdate: false,
495
+ enabled: true,
496
+ fileHash: "hash",
497
+ gitCommitHash: null,
498
+ message: "hello",
499
+ channel: "production",
500
+ storageUri: "storage://unused",
501
+ fingerprintHash: null,
502
+ });
503
+
504
+ await uploadBundleObject(supabaseAdmin, bundle.id);
505
+ await seedHotUpdater.insertBundle(bundle);
506
+
507
+ const response = await fetch(
508
+ `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}${createCanonicalPath({
509
+ appVersion: "1.0",
510
+ bundleId: NIL_UUID,
511
+ platform: "ios",
512
+ _updateStrategy: "appVersion",
513
+ })}`,
514
+ );
515
+
516
+ expect(response.ok).toBe(true);
517
+ await expect(response.json()).resolves.toMatchObject({
518
+ id: "00000000-0000-0000-0000-000000000001",
519
+ status: "UPDATE",
520
+ });
521
+ });
522
+
523
+ it("does not support the legacy exact path", async () => {
524
+ const response = await fetch(
525
+ `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}${LEGACY_HOT_UPDATER_BASE_PATH}`,
526
+ );
527
+
528
+ expect(response.status).toBe(404);
529
+ });
530
+
531
+ it("does not expose management routes from the edge function entrypoint", async () => {
532
+ const response = await fetch(
533
+ `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/api/bundles`,
534
+ );
535
+
536
+ expect(response.status).toBe(404);
537
+ await expect(response.json()).resolves.toEqual({
538
+ error: "Not found",
539
+ });
540
+ });
541
+ });
542
+
543
+ function base64UrlEncode(value: string | Buffer) {
544
+ return Buffer.from(value)
545
+ .toString("base64")
546
+ .replace(/\+/g, "-")
547
+ .replace(/\//g, "_")
548
+ .replace(/=+$/g, "");
549
+ }
550
+
551
+ function createLegacyJwt(role: "anon" | "service_role") {
552
+ const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
553
+ const payload = base64UrlEncode(
554
+ JSON.stringify({
555
+ role,
556
+ iss: "supabase-test",
557
+ iat: Math.floor(Date.now() / 1000),
558
+ exp: Math.floor(Date.now() / 1000) + JWT_EXPIRY_SECONDS,
559
+ }),
560
+ );
561
+ const signature = createHmac("sha256", JWT_SECRET)
562
+ .update(`${header}.${payload}`)
563
+ .digest("base64")
564
+ .replace(/\+/g, "-")
565
+ .replace(/\//g, "_")
566
+ .replace(/=+$/g, "");
567
+
568
+ return `${header}.${payload}.${signature}`;
569
+ }
570
+
571
+ const waitForUrlOk = async (url: string, timeoutMs = 90_000) => {
572
+ const deadline = Date.now() + timeoutMs;
573
+ let lastError = "no response";
574
+
575
+ while (Date.now() < deadline) {
576
+ try {
577
+ const response = await fetch(url);
578
+ if (response.ok) {
579
+ return;
580
+ }
581
+
582
+ lastError = `${response.status} ${response.statusText}`;
583
+ } catch (error) {
584
+ lastError = error instanceof Error ? error.message : String(error);
585
+ }
586
+
587
+ await sleep(500);
588
+ }
589
+
590
+ throw new Error(`Timed out waiting for ${url}: ${lastError}`);
591
+ };
592
+
593
+ const waitForRestApiReady = async (baseUrl: string, timeoutMs = 90_000) => {
594
+ const deadline = Date.now() + timeoutMs;
595
+ let lastError = "no response";
596
+
597
+ while (Date.now() < deadline) {
598
+ try {
599
+ const response = await fetch(
600
+ `${baseUrl}/rest/v1/bundles?select=id&limit=1`,
601
+ {
602
+ headers: {
603
+ apikey: SERVICE_ROLE_KEY,
604
+ Authorization: `Bearer ${SERVICE_ROLE_KEY}`,
605
+ },
606
+ },
607
+ );
608
+ if (response.ok) {
609
+ return;
610
+ }
611
+
612
+ lastError = `${response.status} ${response.statusText}: ${await response.text()}`;
613
+ } catch (error) {
614
+ lastError = error instanceof Error ? error.message : String(error);
615
+ }
616
+
617
+ await sleep(500);
618
+ }
619
+
620
+ throw new Error(`Timed out waiting for PostgREST: ${lastError}`);
621
+ };
622
+
623
+ const sleep = async (ms: number) => {
624
+ await new Promise((resolve) => setTimeout(resolve, ms));
625
+ };
626
+
627
+ const ensureBucketExists = async (
628
+ supabaseAdmin: ReturnType<typeof createClient>,
629
+ ) => {
630
+ const { data: buckets, error: listError } =
631
+ await supabaseAdmin.storage.listBuckets();
632
+
633
+ if (listError) {
634
+ throw listError;
635
+ }
636
+
637
+ if (buckets.some((bucket) => bucket.name === BUCKET_NAME)) {
638
+ return;
639
+ }
640
+
641
+ const { error } = await supabaseAdmin.storage.createBucket(BUCKET_NAME);
642
+
643
+ if (error) {
644
+ throw error;
645
+ }
646
+ };
647
+
648
+ const uploadBundleObject = async (
649
+ supabaseAdmin: ReturnType<typeof createClient>,
650
+ bundleId: string,
651
+ ) => {
652
+ await uploadStorageObject(
653
+ supabaseAdmin,
654
+ `${bundleId}/bundle.zip`,
655
+ Buffer.from("zip"),
656
+ "application/zip",
657
+ );
658
+ };
659
+
660
+ const uploadStorageObject = async (
661
+ supabaseAdmin: ReturnType<typeof createClient>,
662
+ key: string,
663
+ body: string | Buffer,
664
+ contentType: string,
665
+ ) => {
666
+ const { error } = await supabaseAdmin.storage
667
+ .from(BUCKET_NAME)
668
+ .upload(key, body, {
669
+ contentType,
670
+ cacheControl: "31536000",
671
+ upsert: true,
672
+ });
673
+
674
+ if (error) {
675
+ throw error;
676
+ }
677
+ };
678
+
679
+ const loadSupabaseInitSql = async (storageRepoPath: string) => {
680
+ const storageMigrationsDir = path.join(storageRepoPath, "migrations/tenant");
681
+ const storageMigrationFiles = (await readdir(storageMigrationsDir))
682
+ .filter((file) => file.endsWith(".sql"))
683
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
684
+ const storageMigrations = await Promise.all(
685
+ storageMigrationFiles.map(async (file) => {
686
+ const contents = await readFile(
687
+ path.join(storageMigrationsDir, file),
688
+ "utf8",
689
+ );
690
+ const trimmed = contents.trimEnd();
691
+ return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
692
+ }),
693
+ );
694
+
695
+ const migrationsDir = path.join(
696
+ WORKSPACE_ROOT,
697
+ "plugins/supabase/supabase/migrations",
698
+ );
699
+ const migrationFiles = (await readdir(migrationsDir))
700
+ .filter((file) => file.endsWith(".sql"))
701
+ .sort();
702
+ const migrations = await Promise.all(
703
+ migrationFiles.map(async (file) => {
704
+ const contents = await readFile(path.join(migrationsDir, file), "utf8");
705
+ return contents.replaceAll("%%BUCKET_NAME%%", BUCKET_NAME);
706
+ }),
707
+ );
708
+
709
+ return `
710
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
711
+
712
+ DO $$
713
+ BEGIN
714
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
715
+ CREATE ROLE anon NOLOGIN NOINHERIT;
716
+ END IF;
717
+
718
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
719
+ CREATE ROLE authenticated NOLOGIN NOINHERIT;
720
+ END IF;
721
+
722
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
723
+ CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS;
724
+ END IF;
725
+
726
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN
727
+ CREATE ROLE authenticator LOGIN PASSWORD '${POSTGRES_PASSWORD}' NOINHERIT;
728
+ END IF;
729
+
730
+ IF NOT EXISTS (
731
+ SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin'
732
+ ) THEN
733
+ CREATE ROLE supabase_storage_admin LOGIN PASSWORD '${POSTGRES_PASSWORD}' SUPERUSER;
734
+ END IF;
735
+ END $$;
736
+
737
+ GRANT anon TO authenticator;
738
+ GRANT authenticated TO authenticator;
739
+ GRANT service_role TO authenticator;
740
+
741
+ ${migrations.join("\n\n")}
742
+
743
+ SET search_path TO storage, public, extensions;
744
+
745
+ ${storageMigrations.join("\n\n")}
746
+
747
+ SET search_path TO public;
748
+
749
+ GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
750
+ GRANT USAGE ON TYPE platforms TO anon, authenticated, service_role;
751
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon, authenticated;
752
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO service_role;
753
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated, service_role;
754
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated, service_role;
755
+
756
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public
757
+ GRANT SELECT ON TABLES TO anon, authenticated;
758
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public
759
+ GRANT ALL PRIVILEGES ON TABLES TO service_role;
760
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public
761
+ GRANT USAGE, SELECT ON SEQUENCES TO anon, authenticated, service_role;
762
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public
763
+ GRANT EXECUTE ON FUNCTIONS TO anon, authenticated, service_role;
764
+ `.trim();
765
+ };
766
+
767
+ const createComposeFile = ({
768
+ gatewayPort,
769
+ runtimeRoot,
770
+ }: {
771
+ gatewayPort: number;
772
+ runtimeRoot: string;
773
+ }) => {
774
+ return `
775
+ services:
776
+ db:
777
+ image: ${POSTGRES_IMAGE}
778
+ environment:
779
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
780
+ POSTGRES_DB: ${POSTGRES_DB}
781
+ healthcheck:
782
+ test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"]
783
+ interval: 5s
784
+ timeout: 5s
785
+ retries: 20
786
+ volumes:
787
+ - ${path.join(runtimeRoot, "db-init")}:/docker-entrypoint-initdb.d:ro
788
+
789
+ rest:
790
+ image: ${POSTGREST_IMAGE}
791
+ depends_on:
792
+ db:
793
+ condition: service_healthy
794
+ environment:
795
+ PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
796
+ PGRST_DB_SCHEMAS: public,storage
797
+ PGRST_DB_MAX_ROWS: 1000
798
+ PGRST_DB_EXTRA_SEARCH_PATH: public
799
+ PGRST_DB_ANON_ROLE: anon
800
+ PGRST_JWT_SECRET: ${JWT_SECRET}
801
+ PGRST_DB_USE_LEGACY_GUCS: "false"
802
+ PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
803
+ PGRST_APP_SETTINGS_JWT_EXP: "3600"
804
+
805
+ imgproxy:
806
+ image: ${IMGPROXY_IMAGE}
807
+ environment:
808
+ IMGPROXY_BIND: ":5001"
809
+ IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
810
+ IMGPROXY_USE_ETAG: "true"
811
+
812
+ storage:
813
+ image: ${STORAGE_IMAGE}
814
+ restart: on-failure
815
+ depends_on:
816
+ db:
817
+ condition: service_healthy
818
+ rest:
819
+ condition: service_started
820
+ imgproxy:
821
+ condition: service_started
822
+ environment:
823
+ ANON_KEY: ${ANON_KEY}
824
+ SERVICE_KEY: ${SERVICE_ROLE_KEY}
825
+ POSTGREST_URL: http://rest:3000
826
+ AUTH_JWT_SECRET: ${JWT_SECRET}
827
+ DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
828
+ STORAGE_PUBLIC_URL: http://gateway:8000
829
+ REQUEST_ALLOW_X_FORWARDED_PATH: "true"
830
+ FILE_SIZE_LIMIT: 52428800
831
+ STORAGE_BACKEND: file
832
+ GLOBAL_S3_BUCKET: ${BUCKET_NAME}
833
+ FILE_STORAGE_BACKEND_PATH: /var/lib/storage
834
+ TENANT_ID: stub
835
+ REGION: stub
836
+ ENABLE_IMAGE_TRANSFORMATION: "false"
837
+ IMGPROXY_URL: http://imgproxy:5001
838
+ S3_PROTOCOL_ACCESS_KEY_ID: stub
839
+ S3_PROTOCOL_ACCESS_KEY_SECRET: stub
840
+ volumes:
841
+ - storage-data:/var/lib/storage
842
+
843
+ gateway:
844
+ image: ${NGINX_IMAGE}
845
+ depends_on:
846
+ storage:
847
+ condition: service_started
848
+ rest:
849
+ condition: service_started
850
+ ports:
851
+ - "0.0.0.0:${gatewayPort}:8000"
852
+ volumes:
853
+ - ${path.join(runtimeRoot, "nginx.conf")}:/etc/nginx/nginx.conf:ro
854
+
855
+ volumes:
856
+ storage-data:
857
+ `.trim();
858
+ };
859
+
860
+ const createNginxConfig = () => {
861
+ return `
862
+ events {}
863
+
864
+ http {
865
+ client_max_body_size 100m;
866
+
867
+ server {
868
+ listen 8000;
869
+
870
+ location /rest/v1/ {
871
+ proxy_pass http://rest:3000/;
872
+ proxy_http_version 1.1;
873
+ proxy_set_header Host $host;
874
+ proxy_set_header Authorization $http_authorization;
875
+ proxy_set_header apikey $http_apikey;
876
+ proxy_set_header Content-Profile $http_content_profile;
877
+ proxy_set_header Accept-Profile $http_accept_profile;
878
+ proxy_set_header Prefer $http_prefer;
879
+ proxy_set_header Range $http_range;
880
+ proxy_set_header Range-Unit $http_range_unit;
881
+ proxy_set_header Content-Type $http_content_type;
882
+ }
883
+
884
+ location /storage/v1/ {
885
+ proxy_pass http://storage:5000/;
886
+ proxy_http_version 1.1;
887
+ proxy_set_header Host $host;
888
+ proxy_set_header Authorization $http_authorization;
889
+ proxy_set_header apikey $http_apikey;
890
+ proxy_set_header x-forwarded-path $request_uri;
891
+ proxy_set_header Content-Type $http_content_type;
892
+ proxy_set_header Content-Length $content_length;
893
+ }
894
+ }
895
+ }
896
+ `.trim();
897
+ };
898
+
899
+ const writeSupabaseRuntimeFiles = async ({
900
+ runtimeRoot,
901
+ gatewayPort,
902
+ storageRepoPath,
903
+ }: {
904
+ runtimeRoot: string;
905
+ gatewayPort: number;
906
+ storageRepoPath: string;
907
+ }) => {
908
+ await mkdir(path.join(runtimeRoot, "db-init"), { recursive: true });
909
+ await mkdir(path.join(runtimeRoot, "supabase/edge-functions"), {
910
+ recursive: true,
911
+ });
912
+ await symlink(
913
+ path.join(WORKSPACE_ROOT, "plugins/supabase/src"),
914
+ path.join(runtimeRoot, "src"),
915
+ );
916
+ await symlink(
917
+ path.join(WORKSPACE_ROOT, "plugins/supabase/node_modules"),
918
+ path.join(runtimeRoot, "node_modules"),
919
+ );
920
+
921
+ const transformedEntry = transformEnv(
922
+ path.join(
923
+ WORKSPACE_ROOT,
924
+ "plugins/supabase/supabase/edge-functions/index.ts",
925
+ ),
926
+ {
927
+ FUNCTION_NAME,
928
+ },
929
+ );
930
+ const importMap = {
931
+ imports: {
932
+ "@hot-updater/server": pathToFileURL(
933
+ path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
934
+ ).href,
935
+ "@hot-updater/supabase": pathToFileURL(
936
+ path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
937
+ ).href,
938
+ hono: `npm:hono@${resolvePackageVersion("hono", {
939
+ searchFrom: path.join(WORKSPACE_ROOT, "plugins/supabase"),
940
+ })}`,
941
+ },
942
+ };
943
+
944
+ await writeFile(
945
+ path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
946
+ `
947
+ export { supabaseEdgeFunctionDatabase } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseEdgeFunctionDatabase.ts")).href)};
948
+ export { supabaseEdgeFunctionStorage } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseEdgeFunctionStorage.ts")).href)};
949
+ `.trim(),
950
+ );
951
+ await writeFile(
952
+ path.join(runtimeRoot, "supabase/edge-functions/index.ts"),
953
+ transformedEntry,
954
+ );
955
+ await writeFile(
956
+ path.join(runtimeRoot, "import_map.json"),
957
+ JSON.stringify(importMap),
958
+ );
959
+ await writeFile(
960
+ path.join(runtimeRoot, "db-init/00-init.sql"),
961
+ await loadSupabaseInitSql(storageRepoPath),
962
+ );
963
+ await writeFile(
964
+ path.join(runtimeRoot, "docker-compose.yml"),
965
+ createComposeFile({ runtimeRoot, gatewayPort }),
966
+ );
967
+ await writeFile(path.join(runtimeRoot, "nginx.conf"), createNginxConfig());
968
+ };