@hot-updater/supabase 1.0.0-rc.18 → 1.0.0-rc.21

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.
@@ -1,1301 +0,0 @@
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 type { Bundle } from "@hot-updater/core";
18
- import { type HotUpdaterCoreApi, rowToBundle } from "@hot-updater/plugin-core";
19
- import { createHotUpdater } from "@hot-updater/server";
20
- import {
21
- createDatabaseEngine,
22
- createSqlAdapter,
23
- type RetryOptions,
24
- toolingTargetOf,
25
- } from "@hot-updater/server/database";
26
- import {
27
- createDatabaseCoreApi,
28
- createDatabasePluginApis,
29
- } from "@hot-updater/server/db";
30
- import type { CoreReader } from "@hot-updater/server/plugins";
31
- import { apiKeys } from "@hot-updater/server/plugins/api-keys";
32
- import {
33
- createInsightsModel,
34
- insights,
35
- insightsSchema,
36
- type BundleEventRow,
37
- } from "@hot-updater/server/plugins/insights";
38
- import { insightsTestSuite } from "@hot-updater/server/plugins/insights/testing";
39
- import {
40
- runContentionHarness,
41
- setupDatabaseTestSuite,
42
- startHttpTestServer,
43
- withAdapterLatency,
44
- } from "@hot-updater/test-utils";
45
- import { createClient } from "@supabase/supabase-js";
46
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
47
-
48
- import {
49
- assertDockerComposeAvailable,
50
- findOpenPort,
51
- runCheckedCommand,
52
- spawnRuntime,
53
- stopRuntime,
54
- waitForHttpOk,
55
- } from "../../../../packages/test-utils/src/runtimeProcess";
56
- import { plugins } from "../../src/plugins";
57
- import { supabaseDatabase } from "../../src/supabaseDatabase";
58
- import { supabaseExecutor } from "../../src/supabaseExecutor";
59
- import {
60
- SUPABASE_SETTINGS_TABLE,
61
- SUPABASE_TABLE_PREFIX,
62
- supabaseTableNames,
63
- } from "../../src/supabaseSchema";
64
-
65
- const __filename = fileURLToPath(import.meta.url);
66
- const __dirname = path.dirname(__filename);
67
- const WORKSPACE_ROOT = path.resolve(__dirname, "../../../..");
68
- const FUNCTION_NAME = "hot-updater-function";
69
- const FUNCTION_BASE_PATH = `/${FUNCTION_NAME}`;
70
- const API_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
71
- const BUCKET_NAME = "hot-updater-bundles";
72
- const DENO_DOCKER_IMAGE = "denoland/deno:alpine";
73
- const DENO_CACHE_VOLUME = "hot-updater-supabase-deno-cache";
74
- const POSTGRES_IMAGE = "postgres:15-alpine";
75
- const POSTGREST_IMAGE = "postgrest/postgrest:v14.6";
76
- const STORAGE_IMAGE = "supabase/storage-api:v1.44.2";
77
- const IMGPROXY_IMAGE = "darthsim/imgproxy:v3.30.1";
78
- const NGINX_IMAGE = "nginx:1.27-alpine";
79
- const POSTGRES_PASSWORD = "postgres";
80
- const POSTGRES_DB = "postgres";
81
- const JWT_SECRET = "super-secret-jwt-token-with-at-least-32-chars";
82
- const JWT_EXPIRY_SECONDS = 60 * 60 * 24 * 365;
83
- const ANON_KEY = createLegacyJwt("anon");
84
- const SERVICE_ROLE_KEY = createLegacyJwt("service_role");
85
- const REQUIRED_BUILD_ARTIFACTS = [
86
- {
87
- command: "pnpm --filter @hot-updater/core build",
88
- path: path.join(WORKSPACE_ROOT, "packages/core/dist/index.mjs"),
89
- },
90
- {
91
- command: "pnpm --filter @hot-updater/server build",
92
- path: path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
93
- },
94
- {
95
- command: "pnpm --filter @hot-updater/plugin-core build",
96
- path: path.join(WORKSPACE_ROOT, "plugins/plugin-core/dist/index.mjs"),
97
- },
98
- {
99
- command: "pnpm --filter @hot-updater/supabase build",
100
- path: path.join(WORKSPACE_ROOT, "plugins/supabase/dist/index.mjs"),
101
- },
102
- ] as const;
103
-
104
- assertDockerComposeAvailable(
105
- "supabase edge runtime acceptance requires Docker Compose and a running Docker daemon.",
106
- );
107
-
108
- const ensureBuiltArtifacts = async (
109
- artifacts: ReadonlyArray<{ command: string; path: string }>,
110
- ) => {
111
- for (const artifact of artifacts) {
112
- try {
113
- await access(artifact.path);
114
- } catch {
115
- throw new Error(
116
- `Missing built artifact at ${artifact.path}. Run \`${artifact.command}\` before running this test.`,
117
- );
118
- }
119
- }
120
- };
121
-
122
- const toRuntimeBundle = (bundle: Bundle): Bundle => {
123
- return {
124
- ...bundle,
125
- manifestStorageUri: `supabase-storage://${BUCKET_NAME}/${bundle.id}/manifest.json`,
126
- assetBaseStorageUri: `supabase-storage://${BUCKET_NAME}/assets`,
127
- };
128
- };
129
-
130
- const assetHashForBundle = (bundleId: string) => `asset-hash-${bundleId}`;
131
-
132
- const runtimeBundle = (id: string, overrides: Partial<Bundle> = {}): Bundle =>
133
- toRuntimeBundle({
134
- platform: "ios",
135
- gitCommitHash: null,
136
- manifestStorageUri: "storage://unused/manifest.json",
137
- manifestFileHash: `manifest-hash-${id}`,
138
- assetBaseStorageUri: "storage://unused/assets",
139
- ...overrides,
140
- id,
141
- });
142
-
143
- /**
144
- * PRD D6: B3's rollout moves through the apply RPC, measured and recorded in
145
- * plans/evidence/supabase-ingestion-ceiling.md.
146
- * HOT_UPDATER_INGESTION_CEILING=1 runs the whole ladder of rates. The stack
147
- * runs in local Docker, so the figures describe this host and setup, not a
148
- * Supabase project's limits.
149
- */
150
- const INGESTION_LADDER = process.env.HOT_UPDATER_INGESTION_CEILING === "1";
151
- const INGESTION_MOVES = INGESTION_LADDER ? 600 : 200;
152
- const INGESTION_RATES = INGESTION_LADDER ? [50, 100, 200, 400, 800] : [50];
153
- const INGESTION_WRITERS = 16;
154
- const INGESTION_LATENCY_MS = 5;
155
-
156
- /** Event `n` moves installation `install` onto release `release` on one channel. */
157
- const rolloutMove = (
158
- n: number,
159
- install: number,
160
- release: "a" | "b",
161
- receivedAt: number,
162
- ) =>
163
- ({
164
- id: `01900000-0000-7000-8000-${String(n).padStart(12, "0")}`,
165
- type: "UPDATE_APPLIED",
166
- install_id: `install-${install}`,
167
- user_id: `user-${install}`,
168
- from_release_id: release === "b" ? "release-a" : "release-0",
169
- from_bundle_id: release === "b" ? "bundle-a" : "bundle-0",
170
- to_release_id: `release-${release}`,
171
- to_bundle_id: `bundle-${release}`,
172
- platform: "ios",
173
- app_version: "1.0.0",
174
- channel: "production",
175
- metadata: {
176
- cohort: "1",
177
- update_strategy: "appVersion",
178
- fingerprint_hash: null,
179
- sdk_version: null,
180
- },
181
- received_at_ms: receivedAt,
182
- }) as BundleEventRow;
183
-
184
- /** Writes `bundle` with an enabled production release for app version 1.0. */
185
- const deployToProduction = (core: HotUpdaterCoreApi, bundle: Bundle) =>
186
- core.deploy([
187
- {
188
- bundle,
189
- release: {
190
- channel: "production",
191
- enabled: true,
192
- fingerprintHash: null,
193
- message: "hello",
194
- shouldForceUpdate: false,
195
- targetAppVersion: "1.0",
196
- },
197
- },
198
- ]);
199
-
200
- /** A stored bundle, in the shape it was deployed in. */
201
- const storedBundle = async (core: HotUpdaterCoreApi, id: string) => {
202
- const detail = await core.getBundle(id);
203
- return detail && rowToBundle(detail.bundle, detail.patches);
204
- };
205
-
206
- describe.sequential("supabase edge runtime acceptance", () => {
207
- let runtimeRoot: string | undefined;
208
- let storageRepoPath = "";
209
- let composeFilePath = "";
210
- let composeProjectName = "";
211
- let gatewayPort = 0;
212
- let edgePort = 0;
213
- let gatewayBaseUrl = "";
214
- let edgeRuntime: ReturnType<typeof spawnRuntime> | undefined;
215
- let database: ReturnType<typeof supabaseDatabase>;
216
- let core: HotUpdaterCoreApi;
217
- let supabaseAdmin: ReturnType<typeof createClient>;
218
-
219
- /** Registers the API key the edge function's client routes accept. */
220
- const registerRuntimeApiKey = () =>
221
- createDatabasePluginApis(database, [apiKeys()]).apiKeys.register({
222
- apiKey: API_KEY,
223
- name: "Runtime acceptance",
224
- });
225
-
226
- const runDatabaseSql = (statement: string): void => {
227
- runCheckedCommand({
228
- command: "docker",
229
- args: [
230
- "compose",
231
- "-p",
232
- composeProjectName,
233
- "-f",
234
- composeFilePath,
235
- "exec",
236
- "-T",
237
- "db",
238
- "psql",
239
- "-U",
240
- "postgres",
241
- "-d",
242
- POSTGRES_DB,
243
- "-v",
244
- "ON_ERROR_STOP=1",
245
- "-c",
246
- statement,
247
- ],
248
- cwd: WORKSPACE_ROOT,
249
- });
250
- };
251
-
252
- /** Empties every data table but `keep`, the managed plugins' included; the settings rows stay. */
253
- const truncateDataTables = (keep: readonly string[] = []) => {
254
- const tables = supabaseTableNames(toolingTargetOf(plugins).schema)
255
- .filter(
256
- (table) => table !== SUPABASE_SETTINGS_TABLE && !keep.includes(table),
257
- )
258
- .map((table) => `public."${table}"`);
259
- runDatabaseSql(`TRUNCATE ${tables.join(", ")} CASCADE`);
260
- };
261
-
262
- beforeAll(async () => {
263
- await ensureBuiltArtifacts(REQUIRED_BUILD_ARTIFACTS);
264
-
265
- runtimeRoot = await mkdtemp(
266
- path.join(WORKSPACE_ROOT, "plugins/supabase/runtime-acceptance-"),
267
- );
268
- storageRepoPath = path.join(runtimeRoot, "storage-repo");
269
- gatewayPort = await findOpenPort();
270
- edgePort = await findOpenPort();
271
- gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
272
- composeProjectName = `hot-updater-supabase-${process.pid}-${Date.now()}`;
273
- composeFilePath = path.join(runtimeRoot, "docker-compose.yml");
274
-
275
- runCheckedCommand({
276
- command: "git",
277
- args: [
278
- "clone",
279
- "--depth",
280
- "1",
281
- "https://github.com/supabase/storage.git",
282
- storageRepoPath,
283
- ],
284
- cwd: WORKSPACE_ROOT,
285
- });
286
-
287
- await writeSupabaseRuntimeFiles({
288
- runtimeRoot,
289
- gatewayPort,
290
- storageRepoPath,
291
- });
292
-
293
- try {
294
- runCheckedCommand({
295
- command: "docker",
296
- args: [
297
- "compose",
298
- "-p",
299
- composeProjectName,
300
- "-f",
301
- composeFilePath,
302
- "up",
303
- "-d",
304
- ],
305
- cwd: WORKSPACE_ROOT,
306
- });
307
- } catch (error) {
308
- let dbLogs = "";
309
-
310
- try {
311
- const result = spawnSync(
312
- "docker",
313
- [
314
- "compose",
315
- "-p",
316
- composeProjectName,
317
- "-f",
318
- composeFilePath,
319
- "logs",
320
- "--no-color",
321
- "db",
322
- ],
323
- {
324
- cwd: WORKSPACE_ROOT,
325
- encoding: "utf8",
326
- },
327
- );
328
- dbLogs = [result.stdout, result.stderr].filter(Boolean).join("\n");
329
- } catch {
330
- dbLogs = "failed to collect database logs";
331
- }
332
-
333
- throw new Error(
334
- [
335
- error instanceof Error ? error.message : String(error),
336
- "",
337
- "Database logs:",
338
- dbLogs,
339
- ].join("\n"),
340
- );
341
- }
342
-
343
- await waitForRestApiReady(gatewayBaseUrl, 180_000);
344
- await waitForUrlOk(`${gatewayBaseUrl}/storage/v1/status`, 180_000);
345
-
346
- supabaseAdmin = createClient(gatewayBaseUrl, SERVICE_ROLE_KEY);
347
- await ensureBucketExists(supabaseAdmin);
348
-
349
- database = supabaseDatabase({
350
- supabaseUrl: gatewayBaseUrl,
351
- supabaseServiceRoleKey: SERVICE_ROLE_KEY,
352
- });
353
- await registerRuntimeApiKey();
354
- core = createDatabaseCoreApi(database);
355
-
356
- edgeRuntime = spawnRuntime({
357
- command: "docker",
358
- args: [
359
- "run",
360
- "--rm",
361
- "--network",
362
- `${composeProjectName}_default`,
363
- "--add-host",
364
- "host.docker.internal:host-gateway",
365
- "-p",
366
- `127.0.0.1:${edgePort}:8000`,
367
- "-e",
368
- `SUPABASE_URL=http://host.docker.internal:${gatewayPort}`,
369
- "-e",
370
- `SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}`,
371
- "-e",
372
- "DENO_DIR=/deno-dir",
373
- "-v",
374
- `${WORKSPACE_ROOT}:${WORKSPACE_ROOT}:ro`,
375
- "-v",
376
- `${runtimeRoot}:${runtimeRoot}`,
377
- "-v",
378
- `${DENO_CACHE_VOLUME}:/deno-dir`,
379
- "-w",
380
- runtimeRoot,
381
- DENO_DOCKER_IMAGE,
382
- "run",
383
- "--no-lock",
384
- "--node-modules-dir=manual",
385
- "--allow-env",
386
- "--allow-net",
387
- "--allow-read",
388
- "--allow-sys",
389
- "--unstable-sloppy-imports",
390
- "--import-map",
391
- path.join(runtimeRoot, "import_map.json"),
392
- path.join(runtimeRoot, "supabase/edge-functions/index.ts"),
393
- ],
394
- cwd: WORKSPACE_ROOT,
395
- });
396
-
397
- await waitForHttpOk({
398
- url: `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/ping`,
399
- child: edgeRuntime.child,
400
- logs: edgeRuntime.logs,
401
- timeoutMs: 90_000,
402
- });
403
- }, 300_000);
404
-
405
- beforeEach(async () => {
406
- if (!supabaseAdmin) {
407
- throw new Error("Supabase admin client was not initialized.");
408
- }
409
-
410
- // The API key the suite registers last serves the edge function's routes.
411
- truncateDataTables([`${SUPABASE_TABLE_PREFIX}api_keys`]);
412
- });
413
-
414
- afterAll(async () => {
415
- if (edgeRuntime) {
416
- await stopRuntime(edgeRuntime.child);
417
- }
418
-
419
- if (composeFilePath) {
420
- runCheckedCommand({
421
- command: "docker",
422
- args: [
423
- "compose",
424
- "-p",
425
- composeProjectName,
426
- "-f",
427
- composeFilePath,
428
- "down",
429
- "-v",
430
- "--remove-orphans",
431
- ],
432
- cwd: WORKSPACE_ROOT,
433
- });
434
- }
435
-
436
- if (runtimeRoot) {
437
- await rm(runtimeRoot, { recursive: true, force: true });
438
- }
439
- }, 60_000);
440
-
441
- setupDatabaseTestSuite({
442
- name: "Supabase PostgreSQL and PostgREST conformance",
443
- createDatabase: () => database,
444
- migrate: () => undefined,
445
- reset: () => truncateDataTables(),
446
- dispose: async () => {
447
- await registerRuntimeApiKey();
448
- },
449
- createHttpClient: (options) =>
450
- startHttpTestServer(
451
- createHotUpdater({
452
- ...options,
453
- plugins: [insights()],
454
- clientAccess: "public",
455
- }).handlers,
456
- ),
457
- plugins: [
458
- insightsTestSuite({
459
- createModel: (database) =>
460
- createInsightsModel(
461
- createDatabasePluginApis(database, [insights()]).insights,
462
- ),
463
- }),
464
- ],
465
- });
466
-
467
- it("returns one canonical Channel row under concurrent inserts", async () => {
468
- const core = createDatabaseCoreApi(
469
- supabaseDatabase({
470
- supabaseUrl: gatewayBaseUrl,
471
- supabaseServiceRoleKey: SERVICE_ROLE_KEY,
472
- }),
473
- );
474
- const channelName = "concurrent-channel";
475
- const results = await Promise.all([
476
- core.ensureChannel(channelName),
477
- core.ensureChannel(channelName),
478
- ]);
479
-
480
- expect(new Set(results.map(({ id }) => id)).size).toBe(1);
481
- const stored = await supabaseAdmin
482
- .from("hot_updater_v1_channels")
483
- .select("id, name")
484
- .eq("name", channelName);
485
- if (stored.error) throw stored.error;
486
- expect(stored.data).toEqual([results[0]]);
487
- });
488
-
489
- it("rolls back a patch-bearing insert when one base bundle is missing", async () => {
490
- const base = runtimeBundle("00000000-0000-0000-0000-000000000101");
491
- const owner = {
492
- ...base,
493
- id: "00000000-0000-0000-0000-000000000102",
494
- manifestFileHash: "manifest-hash-owner",
495
- patches: [
496
- {
497
- baseBundleId: base.id,
498
- baseFileHash: assetHashForBundle(base.id),
499
- patchFileHash: "hash-valid-patch",
500
- patchStorageUri: "storage://valid-patch",
501
- byteSize: 3_000_000_002,
502
- },
503
- {
504
- baseBundleId: "00000000-0000-0000-0000-000000000199",
505
- baseFileHash: "hash-missing-base",
506
- patchFileHash: "hash-invalid-patch",
507
- patchStorageUri: "storage://invalid-patch",
508
- byteSize: 3_000_000_003,
509
- },
510
- ],
511
- } satisfies Bundle;
512
- await deployToProduction(core, base);
513
-
514
- await expect(deployToProduction(core, owner)).rejects.toBeDefined();
515
-
516
- const ownerResult = await supabaseAdmin
517
- .from("hot_updater_v1_bundles")
518
- .select("id")
519
- .eq("id", owner.id)
520
- .maybeSingle();
521
- if (ownerResult.error) throw ownerResult.error;
522
- expect(ownerResult.data).toBeNull();
523
- const patchResult = await supabaseAdmin
524
- .from("hot_updater_v1_bundle_patches")
525
- .select("id")
526
- .eq("bundle_id", owner.id);
527
- if (patchResult.error) throw patchResult.error;
528
- expect(patchResult.data).toEqual([]);
529
- });
530
-
531
- it("preserves extension defaults and generated values during atomic inserts", async () => {
532
- runDatabaseSql(
533
- [
534
- "ALTER TABLE public.hot_updater_v1_bundles",
535
- "ADD COLUMN tenant_tag text NOT NULL DEFAULT 'default-tenant',",
536
- "ADD COLUMN manifest_file_hash_upper text GENERATED ALWAYS AS (upper(manifest_file_hash)) STORED",
537
- ].join(" "),
538
- );
539
-
540
- try {
541
- const base = runtimeBundle("00000000-0000-0000-0000-000000000151");
542
- const owner = {
543
- ...base,
544
- id: "00000000-0000-0000-0000-000000000152",
545
- manifestFileHash: "manifest-hash-owner",
546
- patches: [
547
- {
548
- baseBundleId: base.id,
549
- baseFileHash: assetHashForBundle(base.id),
550
- patchFileHash: "hash-patch",
551
- patchStorageUri: "storage://patch",
552
- byteSize: 3_000_000_002,
553
- },
554
- ],
555
- } satisfies Bundle;
556
- await deployToProduction(core, base);
557
-
558
- await deployToProduction(core, owner);
559
-
560
- const result = await supabaseAdmin
561
- .from("hot_updater_v1_bundles")
562
- .select("tenant_tag, manifest_file_hash_upper")
563
- .eq("id", owner.id)
564
- .single();
565
- if (result.error) throw result.error;
566
- expect(result.data).toEqual({
567
- tenant_tag: "default-tenant",
568
- manifest_file_hash_upper: "MANIFEST-HASH-OWNER",
569
- });
570
- } finally {
571
- runDatabaseSql(
572
- "ALTER TABLE public.hot_updater_v1_bundles DROP COLUMN tenant_tag, DROP COLUMN manifest_file_hash_upper",
573
- );
574
- }
575
- });
576
-
577
- it("does not resolve public-schema JSON function shadows in atomic RPCs", async () => {
578
- runDatabaseSql(`
579
- CREATE FUNCTION public.jsonb_populate_record(public.hot_updater_v1_bundles, jsonb)
580
- RETURNS public.hot_updater_v1_bundles
581
- LANGUAGE plpgsql
582
- AS $$
583
- BEGIN
584
- RAISE EXCEPTION 'public jsonb_populate_record shadow executed';
585
- END;
586
- $$
587
- `);
588
-
589
- try {
590
- const base = runtimeBundle("00000000-0000-0000-0000-000000000551");
591
- const owner = {
592
- ...base,
593
- id: "00000000-0000-0000-0000-000000000552",
594
- manifestFileHash: "manifest-hash-owner",
595
- patches: [
596
- {
597
- baseBundleId: base.id,
598
- baseFileHash: assetHashForBundle(base.id),
599
- patchFileHash: "hash-patch",
600
- patchStorageUri: "storage://patch",
601
- byteSize: 3_000_000_002,
602
- },
603
- ],
604
- } satisfies Bundle;
605
- await deployToProduction(core, base);
606
-
607
- await deployToProduction(core, owner);
608
-
609
- await expect(storedBundle(core, owner.id)).resolves.toMatchObject({
610
- id: owner.id,
611
- patches: owner.patches,
612
- });
613
- } finally {
614
- runDatabaseSql(
615
- "DROP FUNCTION public.jsonb_populate_record(public.hot_updater_v1_bundles, jsonb)",
616
- );
617
- }
618
- });
619
-
620
- it("rolls back scalar and patch replacement when a new base is missing", async () => {
621
- const base = runtimeBundle("00000000-0000-0000-0000-000000000201");
622
- const owner = {
623
- ...base,
624
- id: "00000000-0000-0000-0000-000000000202",
625
- manifestFileHash: "manifest-hash-owner",
626
- gitCommitHash: "before",
627
- patches: [
628
- {
629
- baseBundleId: base.id,
630
- baseFileHash: assetHashForBundle(base.id),
631
- patchFileHash: "hash-old-patch",
632
- patchStorageUri: "storage://old-patch",
633
- byteSize: 3_000_000_002,
634
- },
635
- ],
636
- } satisfies Bundle;
637
- await deployToProduction(core, base);
638
- await deployToProduction(core, owner);
639
-
640
- await expect(
641
- core.updateBundle(owner.id, {
642
- gitCommitHash: "after",
643
- patches: [
644
- {
645
- baseBundleId: "00000000-0000-0000-0000-000000000299",
646
- baseFileHash: "hash-missing-base",
647
- patchFileHash: "hash-invalid-patch",
648
- patchStorageUri: "storage://invalid-patch",
649
- byteSize: 3_000_000_003,
650
- },
651
- ],
652
- }),
653
- ).rejects.toBeDefined();
654
-
655
- await expect(storedBundle(core, owner.id)).resolves.toMatchObject({
656
- gitCommitHash: "before",
657
- patches: owner.patches,
658
- });
659
- });
660
-
661
- it("atomically applies explicit nulls and an empty patch list", async () => {
662
- const base = runtimeBundle("00000000-0000-0000-0000-000000000301");
663
- const owner = {
664
- ...base,
665
- id: "00000000-0000-0000-0000-000000000302",
666
- manifestFileHash: "manifest-hash-owner",
667
- gitCommitHash: "before",
668
- patches: [
669
- {
670
- baseBundleId: base.id,
671
- baseFileHash: assetHashForBundle(base.id),
672
- patchFileHash: "hash-old-patch",
673
- patchStorageUri: "storage://old-patch",
674
- byteSize: 3_000_000_002,
675
- },
676
- ],
677
- } satisfies Bundle;
678
- await deployToProduction(core, base);
679
- await deployToProduction(core, owner);
680
-
681
- await core.updateBundle(owner.id, {
682
- gitCommitHash: null,
683
- patches: [],
684
- });
685
-
686
- await expect(storedBundle(core, owner.id)).resolves.toMatchObject({
687
- gitCommitHash: null,
688
- patches: [],
689
- });
690
- });
691
-
692
- it("maps a missing aggregate update to the public not-found error", async () => {
693
- const result = core.updateBundle("00000000-0000-0000-0000-000000000401", {
694
- patches: [],
695
- });
696
-
697
- await expect(result).rejects.toMatchObject({
698
- name: "DatabaseBundleNotFoundError",
699
- bundleId: "00000000-0000-0000-0000-000000000401",
700
- });
701
- });
702
-
703
- it("denies the apply RPC to the anonymous role", async () => {
704
- const response = await fetch(
705
- `${gatewayBaseUrl}/rest/v1/rpc/hot_updater_v1_apply`,
706
- {
707
- method: "POST",
708
- headers: {
709
- apikey: ANON_KEY,
710
- Authorization: `Bearer ${ANON_KEY}`,
711
- "Content-Type": "application/json",
712
- },
713
- body: JSON.stringify({ p_statements: [] }),
714
- },
715
- );
716
-
717
- expect(response.ok).toBe(false);
718
- });
719
-
720
- it("records the Insights ingestion ceiling through the apply RPC", async () => {
721
- const adapter = createSqlAdapter({
722
- executor: supabaseExecutor(
723
- createClient(gatewayBaseUrl, SERVICE_ROLE_KEY),
724
- ),
725
- tablePrefix: SUPABASE_TABLE_PREFIX,
726
- });
727
- const module = { id: "insights", schema: insightsSchema } as const;
728
- const apiOf = (latencyMs: number, retry?: RetryOptions) =>
729
- insights().init({
730
- db: createDatabaseEngine({
731
- adapter:
732
- latencyMs > 0 ? withAdapterLatency(adapter, latencyMs) : adapter,
733
- schema: toolingTargetOf([insights()]).schema,
734
- ...(retry === undefined ? {} : { retry }),
735
- }).database(module),
736
- // Insights never reads core.
737
- core: {} as CoreReader,
738
- now: Date.now,
739
- }).api;
740
- const seeded = apiOf(0, { attempts: 64, baseDelayMs: 1, maxDelayMs: 20 });
741
- const start = Date.now() - 2 * 3_600_000;
742
- const steps = [];
743
- for (const [step, rate] of INGESTION_RATES.entries()) {
744
- // Each step moves its own installations, first onto A, then A→B.
745
- const first = step * INGESTION_MOVES;
746
- const seed = await runContentionHarness({
747
- transactions: INGESTION_MOVES,
748
- ratePerSecond: 5000,
749
- concurrency: 4,
750
- run: (i) =>
751
- seeded.recordEvent(
752
- rolloutMove(2 * first + i, first + i, "a", start + first + i),
753
- ),
754
- });
755
- expect(seed.errors).toEqual({});
756
- const retries = { rerun: 0, resend: 0, retriedTransactions: 0 };
757
- const api = apiOf(INGESTION_LATENCY_MS, {
758
- onRetry: (kind, attempt) => {
759
- retries[kind] += 1;
760
- if (attempt === 1) retries.retriedTransactions += 1;
761
- },
762
- });
763
- const report = await runContentionHarness({
764
- transactions: INGESTION_MOVES,
765
- ratePerSecond: rate,
766
- concurrency: INGESTION_WRITERS,
767
- run: (i) =>
768
- api.recordEvent(
769
- rolloutMove(
770
- 2 * first + INGESTION_MOVES + i,
771
- first + i,
772
- "b",
773
- Date.now(),
774
- ),
775
- ),
776
- });
777
- steps.push({
778
- rate,
779
- ...report,
780
- ...retries,
781
- retried: retries.retriedTransactions / INGESTION_MOVES,
782
- achieved: Math.round(report.committed / (report.durationMs / 1000)),
783
- });
784
- }
785
- // The ceiling: the highest rate kept up with, no errors and at most 5% retried.
786
- const ceiling = steps
787
- .filter(
788
- (step) =>
789
- Object.keys(step.errors).length === 0 &&
790
- step.retried <= 0.05 &&
791
- step.achieved >= step.rate * 0.9,
792
- )
793
- .at(-1)?.rate;
794
- console.info(
795
- "supabase-ingestion-ceiling",
796
- JSON.stringify({
797
- writers: INGESTION_WRITERS,
798
- latencyMs: INGESTION_LATENCY_MS,
799
- ceiling,
800
- steps,
801
- }),
802
- );
803
- for (const { committed, errors } of steps) {
804
- // Under contention a move may run out of retries, and nothing else fails.
805
- const { DatabaseConflictError: conflicts = 0, ...others } = errors;
806
- expect(others).toEqual({});
807
- expect(committed + conflicts).toBe(INGESTION_MOVES);
808
- }
809
- }, 600_000);
810
-
811
- it("serves unversioned Release Catalog routes from the edge function entrypoint", async () => {
812
- const bundle = toRuntimeBundle({
813
- id: "00000000-0000-0000-0000-000000000001",
814
- platform: "ios",
815
- gitCommitHash: null,
816
- manifestStorageUri: "storage://unused/manifest.json",
817
- manifestFileHash: "manifest-hash",
818
- assetBaseStorageUri: "storage://unused/assets",
819
- });
820
-
821
- await uploadBundleObject(supabaseAdmin, bundle.id);
822
- await deployToProduction(core, bundle);
823
-
824
- const unauthorized = await fetch(
825
- `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0`,
826
- );
827
- expect(unauthorized.status).toBe(401);
828
-
829
- const response = await fetch(
830
- `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0`,
831
- { headers: { "x-api-key": API_KEY } },
832
- );
833
-
834
- expect(response.ok).toBe(true);
835
- await expect(response.json()).resolves.toMatchObject({
836
- catalogId: expect.stringMatching(/^[0-9a-f-]{36}$/),
837
- releases: [{ bundleId: "00000000-0000-0000-0000-000000000001" }],
838
- });
839
- });
840
-
841
- it("does not support the legacy exact path", async () => {
842
- const response = await fetch(
843
- `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/api/check-update`,
844
- );
845
-
846
- expect(response.status).toBe(404);
847
- });
848
-
849
- it("does not expose management routes from the edge function entrypoint", async () => {
850
- const response = await fetch(
851
- `http://127.0.0.1:${edgePort}${FUNCTION_BASE_PATH}/admin/bundles`,
852
- );
853
-
854
- expect(response.status).toBe(404);
855
- await expect(response.json()).resolves.toEqual({
856
- error: "Not found",
857
- });
858
- });
859
- });
860
-
861
- function base64UrlEncode(value: string | Buffer) {
862
- return Buffer.from(value)
863
- .toString("base64")
864
- .replace(/\+/g, "-")
865
- .replace(/\//g, "_")
866
- .replace(/=+$/g, "");
867
- }
868
-
869
- function createLegacyJwt(role: "anon" | "service_role") {
870
- const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
871
- const payload = base64UrlEncode(
872
- JSON.stringify({
873
- role,
874
- iss: "supabase-test",
875
- iat: Math.floor(Date.now() / 1000),
876
- exp: Math.floor(Date.now() / 1000) + JWT_EXPIRY_SECONDS,
877
- }),
878
- );
879
- const signature = createHmac("sha256", JWT_SECRET)
880
- .update(`${header}.${payload}`)
881
- .digest("base64")
882
- .replace(/\+/g, "-")
883
- .replace(/\//g, "_")
884
- .replace(/=+$/g, "");
885
-
886
- return `${header}.${payload}.${signature}`;
887
- }
888
-
889
- const waitForUrlOk = async (url: string, timeoutMs = 90_000) => {
890
- const deadline = Date.now() + timeoutMs;
891
- let lastError = "no response";
892
-
893
- while (Date.now() < deadline) {
894
- try {
895
- const response = await fetch(url);
896
- if (response.ok) {
897
- return;
898
- }
899
-
900
- lastError = `${response.status} ${response.statusText}`;
901
- } catch (error) {
902
- lastError = error instanceof Error ? error.message : String(error);
903
- }
904
-
905
- await sleep(500);
906
- }
907
-
908
- throw new Error(`Timed out waiting for ${url}: ${lastError}`);
909
- };
910
-
911
- const waitForRestApiReady = async (baseUrl: string, timeoutMs = 90_000) => {
912
- const deadline = Date.now() + timeoutMs;
913
- let lastError = "no response";
914
-
915
- while (Date.now() < deadline) {
916
- try {
917
- const response = await fetch(
918
- `${baseUrl}/rest/v1/${SUPABASE_TABLE_PREFIX}bundles?select=id&limit=1`,
919
- {
920
- headers: {
921
- apikey: SERVICE_ROLE_KEY,
922
- Authorization: `Bearer ${SERVICE_ROLE_KEY}`,
923
- },
924
- },
925
- );
926
- if (response.ok) {
927
- return;
928
- }
929
-
930
- lastError = `${response.status} ${response.statusText}: ${await response.text()}`;
931
- } catch (error) {
932
- lastError = error instanceof Error ? error.message : String(error);
933
- }
934
-
935
- await sleep(500);
936
- }
937
-
938
- throw new Error(`Timed out waiting for PostgREST: ${lastError}`);
939
- };
940
-
941
- const sleep = async (ms: number) => {
942
- await new Promise((resolve) => setTimeout(resolve, ms));
943
- };
944
-
945
- const ensureBucketExists = async (
946
- supabaseAdmin: ReturnType<typeof createClient>,
947
- ) => {
948
- const { data: buckets, error: listError } =
949
- await supabaseAdmin.storage.listBuckets();
950
-
951
- if (listError) {
952
- throw listError;
953
- }
954
-
955
- if (buckets.some((bucket) => bucket.name === BUCKET_NAME)) {
956
- return;
957
- }
958
-
959
- const { error } = await supabaseAdmin.storage.createBucket(BUCKET_NAME);
960
-
961
- if (error) {
962
- throw error;
963
- }
964
- };
965
-
966
- const uploadBundleObject = async (
967
- supabaseAdmin: ReturnType<typeof createClient>,
968
- bundleId: string,
969
- ) => {
970
- await uploadStorageObject(
971
- supabaseAdmin,
972
- `${bundleId}/manifest.json`,
973
- JSON.stringify({ bundleId, assets: {} }),
974
- "application/json",
975
- );
976
- };
977
-
978
- const uploadStorageObject = async (
979
- supabaseAdmin: ReturnType<typeof createClient>,
980
- key: string,
981
- body: string | Buffer,
982
- contentType: string,
983
- ) => {
984
- const { error } = await supabaseAdmin.storage
985
- .from(BUCKET_NAME)
986
- .upload(key, body, {
987
- contentType,
988
- cacheControl: "31536000",
989
- upsert: true,
990
- });
991
-
992
- if (error) {
993
- throw error;
994
- }
995
- };
996
-
997
- const loadSupabaseInitSql = async (storageRepoPath: string) => {
998
- const storageMigrationsDir = path.join(storageRepoPath, "migrations/tenant");
999
- const storageMigrationFiles = (await readdir(storageMigrationsDir))
1000
- .filter((file) => file.endsWith(".sql"))
1001
- .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
1002
- const storageMigrations = await Promise.all(
1003
- storageMigrationFiles.map(async (file) => {
1004
- const contents = await readFile(
1005
- path.join(storageMigrationsDir, file),
1006
- "utf8",
1007
- );
1008
- const trimmed = contents.trimEnd();
1009
- return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
1010
- }),
1011
- );
1012
-
1013
- const migrationsDir = path.join(
1014
- WORKSPACE_ROOT,
1015
- "plugins/supabase/supabase/migrations",
1016
- );
1017
- const migrationFiles = (await readdir(migrationsDir))
1018
- .filter((file) => file.endsWith(".sql"))
1019
- .sort();
1020
- const migrations = await Promise.all(
1021
- migrationFiles.map(async (file) => {
1022
- const contents = await readFile(path.join(migrationsDir, file), "utf8");
1023
- return contents.replaceAll("%%BUCKET_NAME%%", BUCKET_NAME);
1024
- }),
1025
- );
1026
-
1027
- return `
1028
- CREATE EXTENSION IF NOT EXISTS pgcrypto;
1029
-
1030
- DO $$
1031
- BEGIN
1032
- IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
1033
- CREATE ROLE anon NOLOGIN NOINHERIT;
1034
- END IF;
1035
-
1036
- IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
1037
- CREATE ROLE authenticated NOLOGIN NOINHERIT;
1038
- END IF;
1039
-
1040
- IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
1041
- CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS;
1042
- END IF;
1043
-
1044
- IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticator') THEN
1045
- CREATE ROLE authenticator LOGIN PASSWORD '${POSTGRES_PASSWORD}' NOINHERIT;
1046
- END IF;
1047
-
1048
- IF NOT EXISTS (
1049
- SELECT 1 FROM pg_roles WHERE rolname = 'supabase_storage_admin'
1050
- ) THEN
1051
- CREATE ROLE supabase_storage_admin LOGIN PASSWORD '${POSTGRES_PASSWORD}' SUPERUSER;
1052
- END IF;
1053
- END $$;
1054
-
1055
- GRANT anon TO authenticator;
1056
- GRANT authenticated TO authenticator;
1057
- GRANT service_role TO authenticator;
1058
-
1059
- ${migrations.join("\n\n")}
1060
-
1061
- SET search_path TO storage, public, extensions;
1062
-
1063
- ${storageMigrations.join("\n\n")}
1064
-
1065
- SET search_path TO public;
1066
-
1067
- GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
1068
- GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon, authenticated;
1069
- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO service_role;
1070
- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated, service_role;
1071
- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated, service_role;
1072
-
1073
- REVOKE EXECUTE ON FUNCTION public.hot_updater_v1_apply(jsonb)
1074
- FROM anon, authenticated;
1075
-
1076
- ALTER DEFAULT PRIVILEGES IN SCHEMA public
1077
- GRANT SELECT ON TABLES TO anon, authenticated;
1078
- ALTER DEFAULT PRIVILEGES IN SCHEMA public
1079
- GRANT ALL PRIVILEGES ON TABLES TO service_role;
1080
- ALTER DEFAULT PRIVILEGES IN SCHEMA public
1081
- GRANT USAGE, SELECT ON SEQUENCES TO anon, authenticated, service_role;
1082
- ALTER DEFAULT PRIVILEGES IN SCHEMA public
1083
- GRANT EXECUTE ON FUNCTIONS TO anon, authenticated, service_role;
1084
- `.trim();
1085
- };
1086
-
1087
- const createComposeFile = ({
1088
- gatewayPort,
1089
- runtimeRoot,
1090
- }: {
1091
- gatewayPort: number;
1092
- runtimeRoot: string;
1093
- }) => {
1094
- return `
1095
- services:
1096
- db:
1097
- image: ${POSTGRES_IMAGE}
1098
- environment:
1099
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
1100
- POSTGRES_DB: ${POSTGRES_DB}
1101
- healthcheck:
1102
- test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"]
1103
- interval: 5s
1104
- timeout: 5s
1105
- retries: 20
1106
- volumes:
1107
- - ${path.join(runtimeRoot, "db-init")}:/docker-entrypoint-initdb.d:ro
1108
-
1109
- rest:
1110
- image: ${POSTGREST_IMAGE}
1111
- depends_on:
1112
- db:
1113
- condition: service_healthy
1114
- environment:
1115
- PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
1116
- PGRST_DB_SCHEMAS: public,storage
1117
- PGRST_DB_MAX_ROWS: 1000
1118
- PGRST_DB_EXTRA_SEARCH_PATH: public
1119
- PGRST_DB_ANON_ROLE: anon
1120
- PGRST_JWT_SECRET: ${JWT_SECRET}
1121
- PGRST_DB_USE_LEGACY_GUCS: "false"
1122
- PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
1123
- PGRST_APP_SETTINGS_JWT_EXP: "3600"
1124
-
1125
- imgproxy:
1126
- image: ${IMGPROXY_IMAGE}
1127
- environment:
1128
- IMGPROXY_BIND: ":5001"
1129
- IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
1130
- IMGPROXY_USE_ETAG: "true"
1131
-
1132
- storage:
1133
- image: ${STORAGE_IMAGE}
1134
- restart: on-failure
1135
- depends_on:
1136
- db:
1137
- condition: service_healthy
1138
- rest:
1139
- condition: service_started
1140
- imgproxy:
1141
- condition: service_started
1142
- environment:
1143
- ANON_KEY: ${ANON_KEY}
1144
- SERVICE_KEY: ${SERVICE_ROLE_KEY}
1145
- POSTGREST_URL: http://rest:3000
1146
- AUTH_JWT_SECRET: ${JWT_SECRET}
1147
- DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
1148
- STORAGE_PUBLIC_URL: http://gateway:8000
1149
- REQUEST_ALLOW_X_FORWARDED_PATH: "true"
1150
- FILE_SIZE_LIMIT: 52428800
1151
- STORAGE_BACKEND: file
1152
- GLOBAL_S3_BUCKET: ${BUCKET_NAME}
1153
- FILE_STORAGE_BACKEND_PATH: /var/lib/storage
1154
- TENANT_ID: stub
1155
- REGION: stub
1156
- ENABLE_IMAGE_TRANSFORMATION: "false"
1157
- IMGPROXY_URL: http://imgproxy:5001
1158
- S3_PROTOCOL_ACCESS_KEY_ID: stub
1159
- S3_PROTOCOL_ACCESS_KEY_SECRET: stub
1160
- volumes:
1161
- - storage-data:/var/lib/storage
1162
-
1163
- gateway:
1164
- image: ${NGINX_IMAGE}
1165
- depends_on:
1166
- storage:
1167
- condition: service_started
1168
- rest:
1169
- condition: service_started
1170
- ports:
1171
- - "0.0.0.0:${gatewayPort}:8000"
1172
- volumes:
1173
- - ${path.join(runtimeRoot, "nginx.conf")}:/etc/nginx/nginx.conf:ro
1174
-
1175
- volumes:
1176
- storage-data:
1177
- `.trim();
1178
- };
1179
-
1180
- const createNginxConfig = () => {
1181
- return `
1182
- events {}
1183
-
1184
- http {
1185
- client_max_body_size 100m;
1186
-
1187
- server {
1188
- listen 8000;
1189
-
1190
- location /rest/v1/ {
1191
- proxy_pass http://rest:3000/;
1192
- proxy_http_version 1.1;
1193
- proxy_set_header Host $host;
1194
- proxy_set_header Authorization $http_authorization;
1195
- proxy_set_header apikey $http_apikey;
1196
- proxy_set_header Content-Profile $http_content_profile;
1197
- proxy_set_header Accept-Profile $http_accept_profile;
1198
- proxy_set_header Prefer $http_prefer;
1199
- proxy_set_header Range $http_range;
1200
- proxy_set_header Range-Unit $http_range_unit;
1201
- proxy_set_header Content-Type $http_content_type;
1202
- }
1203
-
1204
- location /storage/v1/ {
1205
- proxy_pass http://storage:5000/;
1206
- proxy_http_version 1.1;
1207
- proxy_set_header Host $host;
1208
- proxy_set_header Authorization $http_authorization;
1209
- proxy_set_header apikey $http_apikey;
1210
- proxy_set_header x-forwarded-path $request_uri;
1211
- proxy_set_header Content-Type $http_content_type;
1212
- proxy_set_header Content-Length $content_length;
1213
- }
1214
- }
1215
- }
1216
- `.trim();
1217
- };
1218
-
1219
- const writeSupabaseRuntimeFiles = async ({
1220
- runtimeRoot,
1221
- gatewayPort,
1222
- storageRepoPath,
1223
- }: {
1224
- runtimeRoot: string;
1225
- gatewayPort: number;
1226
- storageRepoPath: string;
1227
- }) => {
1228
- await mkdir(path.join(runtimeRoot, "db-init"), { recursive: true });
1229
- await mkdir(path.join(runtimeRoot, "supabase/edge-functions"), {
1230
- recursive: true,
1231
- });
1232
- await symlink(
1233
- path.join(WORKSPACE_ROOT, "plugins/supabase/src"),
1234
- path.join(runtimeRoot, "src"),
1235
- );
1236
- await symlink(
1237
- path.join(WORKSPACE_ROOT, "plugins/supabase/node_modules"),
1238
- path.join(runtimeRoot, "node_modules"),
1239
- );
1240
-
1241
- const transformedEntry = transformEnv(
1242
- path.join(
1243
- WORKSPACE_ROOT,
1244
- "plugins/supabase/supabase/edge-functions/index.ts",
1245
- ),
1246
- {
1247
- BUCKET_NAME,
1248
- FUNCTION_NAME,
1249
- },
1250
- );
1251
- const importMap = {
1252
- imports: {
1253
- "@hot-updater/server": pathToFileURL(
1254
- path.join(WORKSPACE_ROOT, "packages/server/dist/index.mjs"),
1255
- ).href,
1256
- ...Object.fromEntries(
1257
- ["api-keys", "insights"].map((plugin) => [
1258
- `@hot-updater/server/plugins/${plugin}`,
1259
- pathToFileURL(
1260
- path.join(
1261
- WORKSPACE_ROOT,
1262
- `packages/server/dist/plugins/${plugin}/index.mjs`,
1263
- ),
1264
- ).href,
1265
- ]),
1266
- ),
1267
- "@hot-updater/supabase/edge": pathToFileURL(
1268
- path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
1269
- ).href,
1270
- hono: `npm:hono@${resolvePackageVersion("hono", {
1271
- searchFrom: path.join(WORKSPACE_ROOT, "plugins/supabase"),
1272
- })}`,
1273
- },
1274
- };
1275
-
1276
- await writeFile(
1277
- path.join(runtimeRoot, "hot-updater-supabase-edge.ts"),
1278
- `
1279
- export { plugins } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/plugins.ts")).href)};
1280
- export { supabaseDatabase } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseDatabase.ts")).href)};
1281
- export { supabaseEdgeFunctionStorage as supabaseStorage } from ${JSON.stringify(pathToFileURL(path.join(WORKSPACE_ROOT, "plugins/supabase/src/supabaseEdgeFunctionStorage.ts")).href)};
1282
- `.trim(),
1283
- );
1284
- await writeFile(
1285
- path.join(runtimeRoot, "supabase/edge-functions/index.ts"),
1286
- transformedEntry,
1287
- );
1288
- await writeFile(
1289
- path.join(runtimeRoot, "import_map.json"),
1290
- JSON.stringify(importMap),
1291
- );
1292
- await writeFile(
1293
- path.join(runtimeRoot, "db-init/00-init.sql"),
1294
- await loadSupabaseInitSql(storageRepoPath),
1295
- );
1296
- await writeFile(
1297
- path.join(runtimeRoot, "docker-compose.yml"),
1298
- createComposeFile({ runtimeRoot, gatewayPort }),
1299
- );
1300
- await writeFile(path.join(runtimeRoot, "nginx.conf"), createNginxConfig());
1301
- };