@hot-updater/server 0.21.9 → 0.21.11

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.
@@ -0,0 +1,441 @@
1
+ import type {
2
+ AppUpdateInfo,
3
+ AppVersionGetBundlesArgs,
4
+ Bundle,
5
+ FingerprintGetBundlesArgs,
6
+ GetBundlesArgs,
7
+ Platform,
8
+ UpdateInfo,
9
+ } from "@hot-updater/core";
10
+ import { NIL_UUID } from "@hot-updater/core";
11
+ import { filterCompatibleAppVersions } from "@hot-updater/plugin-core";
12
+ import type { InferFumaDB } from "fumadb";
13
+ import { fumadb } from "fumadb";
14
+ import type { FumaDBAdapter } from "fumadb/adapters";
15
+ import { calculatePagination } from "../calculatePagination";
16
+ import { v0_21_0 } from "../schema/v0_21_0";
17
+ import type { PaginationInfo } from "../types";
18
+ import type { DatabaseAPI } from "./types";
19
+
20
+ export const HotUpdaterDB = fumadb({
21
+ namespace: "hot_updater",
22
+ schemas: [v0_21_0],
23
+ });
24
+
25
+ export type HotUpdaterClient = InferFumaDB<typeof HotUpdaterDB>;
26
+
27
+ export type Migrator = ReturnType<HotUpdaterClient["createMigrator"]>;
28
+
29
+ export function createOrmDatabaseCore({
30
+ database,
31
+ resolveFileUrl,
32
+ }: {
33
+ database: FumaDBAdapter;
34
+ resolveFileUrl: (storageUri: string | null) => Promise<string | null>;
35
+ }): {
36
+ api: DatabaseAPI;
37
+ adapterName: string;
38
+ createMigrator: () => Migrator;
39
+ generateSchema: HotUpdaterClient["generateSchema"];
40
+ } {
41
+ const client = HotUpdaterDB.client(database);
42
+
43
+ const api: DatabaseAPI = {
44
+ async getBundleById(id: string): Promise<Bundle | null> {
45
+ const version = await client.version();
46
+ const orm = client.orm(version);
47
+ const result = await orm.findFirst("bundles", {
48
+ select: [
49
+ "id",
50
+ "platform",
51
+ "should_force_update",
52
+ "enabled",
53
+ "file_hash",
54
+ "git_commit_hash",
55
+ "message",
56
+ "channel",
57
+ "storage_uri",
58
+ "target_app_version",
59
+ "fingerprint_hash",
60
+ "metadata",
61
+ ],
62
+ where: (b) => b("id", "=", id),
63
+ });
64
+ if (!result) return null;
65
+ const bundle: Bundle = {
66
+ id: result.id,
67
+ platform: result.platform as Platform,
68
+ shouldForceUpdate: Boolean(result.should_force_update),
69
+ enabled: Boolean(result.enabled),
70
+ fileHash: result.file_hash,
71
+ gitCommitHash: result.git_commit_hash ?? null,
72
+ message: result.message ?? null,
73
+ channel: result.channel,
74
+ storageUri: result.storage_uri,
75
+ targetAppVersion: result.target_app_version ?? null,
76
+ fingerprintHash: result.fingerprint_hash ?? null,
77
+ };
78
+ return bundle;
79
+ },
80
+
81
+ async getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null> {
82
+ const version = await client.version();
83
+ const orm = client.orm(version);
84
+
85
+ type UpdateSelectRow = {
86
+ id: string;
87
+ should_force_update: boolean;
88
+ message: string | null;
89
+ storage_uri: string | null;
90
+ file_hash: string;
91
+ };
92
+
93
+ const toUpdateInfo = (
94
+ row: UpdateSelectRow,
95
+ status: "UPDATE" | "ROLLBACK",
96
+ ): UpdateInfo => ({
97
+ id: row.id,
98
+ shouldForceUpdate:
99
+ status === "ROLLBACK" ? true : Boolean(row.should_force_update),
100
+ message: row.message ?? null,
101
+ status,
102
+ storageUri: row.storage_uri ?? null,
103
+ fileHash: row.file_hash ?? null,
104
+ });
105
+
106
+ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO: UpdateInfo = {
107
+ id: NIL_UUID,
108
+ message: null,
109
+ shouldForceUpdate: true,
110
+ status: "ROLLBACK",
111
+ storageUri: null,
112
+ fileHash: null,
113
+ };
114
+
115
+ const appVersionStrategy = async ({
116
+ platform,
117
+ appVersion,
118
+ bundleId,
119
+ minBundleId = NIL_UUID,
120
+ channel = "production",
121
+ }: AppVersionGetBundlesArgs): Promise<UpdateInfo | null> => {
122
+ const versionRows = await orm.findMany("bundles", {
123
+ select: ["target_app_version"],
124
+ where: (b) => b.and(b("platform", "=", platform)),
125
+ });
126
+ const allTargetVersions = Array.from(
127
+ new Set(
128
+ (versionRows ?? [])
129
+ .map((r) => r.target_app_version)
130
+ .filter((v): v is string => Boolean(v)),
131
+ ),
132
+ );
133
+ const compatibleVersions = filterCompatibleAppVersions(
134
+ allTargetVersions,
135
+ appVersion,
136
+ );
137
+
138
+ const baseRows =
139
+ compatibleVersions.length === 0
140
+ ? []
141
+ : await orm.findMany("bundles", {
142
+ select: [
143
+ "id",
144
+ "should_force_update",
145
+ "message",
146
+ "storage_uri",
147
+ "file_hash",
148
+ "channel",
149
+ "target_app_version",
150
+ "enabled",
151
+ ],
152
+ where: (b) =>
153
+ b.and(
154
+ b("enabled", "=", true),
155
+ b("platform", "=", platform),
156
+ b("id", ">=", minBundleId ?? NIL_UUID),
157
+ b("channel", "=", channel),
158
+ b.isNotNull("target_app_version"),
159
+ ),
160
+ });
161
+
162
+ const candidates = (baseRows ?? []).filter((r) =>
163
+ r.target_app_version
164
+ ? compatibleVersions.includes(r.target_app_version)
165
+ : false,
166
+ );
167
+
168
+ const byIdDesc = (a: { id: string }, b: { id: string }) =>
169
+ b.id.localeCompare(a.id);
170
+ const sorted = (candidates ?? []).slice().sort(byIdDesc);
171
+
172
+ const latestCandidate = sorted[0] ?? null;
173
+ const currentBundle = sorted.find((b) => b.id === bundleId);
174
+ const updateCandidate =
175
+ sorted.find((b) => b.id.localeCompare(bundleId) > 0) ?? null;
176
+ const rollbackCandidate =
177
+ sorted.find((b) => b.id.localeCompare(bundleId) < 0) ?? null;
178
+
179
+ if (bundleId === NIL_UUID) {
180
+ if (latestCandidate && latestCandidate.id !== bundleId) {
181
+ return toUpdateInfo(latestCandidate, "UPDATE");
182
+ }
183
+ return null;
184
+ }
185
+
186
+ if (currentBundle) {
187
+ if (
188
+ latestCandidate &&
189
+ latestCandidate.id.localeCompare(currentBundle.id) > 0
190
+ ) {
191
+ return toUpdateInfo(latestCandidate, "UPDATE");
192
+ }
193
+ return null;
194
+ }
195
+
196
+ if (updateCandidate) {
197
+ return toUpdateInfo(updateCandidate, "UPDATE");
198
+ }
199
+ if (rollbackCandidate) {
200
+ return toUpdateInfo(rollbackCandidate, "ROLLBACK");
201
+ }
202
+
203
+ if (minBundleId && bundleId.localeCompare(minBundleId) <= 0) {
204
+ return null;
205
+ }
206
+ return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
207
+ };
208
+
209
+ const fingerprintStrategy = async ({
210
+ platform,
211
+ fingerprintHash,
212
+ bundleId,
213
+ minBundleId = NIL_UUID,
214
+ channel = "production",
215
+ }: FingerprintGetBundlesArgs): Promise<UpdateInfo | null> => {
216
+ const candidates = await orm.findMany("bundles", {
217
+ select: [
218
+ "id",
219
+ "should_force_update",
220
+ "message",
221
+ "storage_uri",
222
+ "file_hash",
223
+ "channel",
224
+ "fingerprint_hash",
225
+ "enabled",
226
+ ],
227
+ where: (b) =>
228
+ b.and(
229
+ b("enabled", "=", true),
230
+ b("platform", "=", platform),
231
+ b("id", ">=", minBundleId ?? NIL_UUID),
232
+ b("channel", "=", channel),
233
+ b("fingerprint_hash", "=", fingerprintHash),
234
+ ),
235
+ });
236
+
237
+ const byIdDesc = (a: { id: string }, b: { id: string }) =>
238
+ b.id.localeCompare(a.id);
239
+ const sorted = (candidates ?? []).slice().sort(byIdDesc);
240
+
241
+ const latestCandidate = sorted[0] ?? null;
242
+ const currentBundle = sorted.find((b) => b.id === bundleId);
243
+ const updateCandidate =
244
+ sorted.find((b) => b.id.localeCompare(bundleId) > 0) ?? null;
245
+ const rollbackCandidate =
246
+ sorted.find((b) => b.id.localeCompare(bundleId) < 0) ?? null;
247
+
248
+ if (bundleId === NIL_UUID) {
249
+ if (latestCandidate && latestCandidate.id !== bundleId) {
250
+ return toUpdateInfo(latestCandidate, "UPDATE");
251
+ }
252
+ return null;
253
+ }
254
+
255
+ if (currentBundle) {
256
+ if (
257
+ latestCandidate &&
258
+ latestCandidate.id.localeCompare(currentBundle.id) > 0
259
+ ) {
260
+ return toUpdateInfo(latestCandidate, "UPDATE");
261
+ }
262
+ return null;
263
+ }
264
+
265
+ if (updateCandidate) {
266
+ return toUpdateInfo(updateCandidate, "UPDATE");
267
+ }
268
+ if (rollbackCandidate) {
269
+ return toUpdateInfo(rollbackCandidate, "ROLLBACK");
270
+ }
271
+
272
+ if (minBundleId && bundleId.localeCompare(minBundleId) <= 0) {
273
+ return null;
274
+ }
275
+ return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
276
+ };
277
+
278
+ if (args._updateStrategy === "appVersion") {
279
+ return appVersionStrategy(args);
280
+ }
281
+ if (args._updateStrategy === "fingerprint") {
282
+ return fingerprintStrategy(args);
283
+ }
284
+ return null;
285
+ },
286
+
287
+ async getAppUpdateInfo(
288
+ args: GetBundlesArgs,
289
+ ): Promise<AppUpdateInfo | null> {
290
+ const info = await this.getUpdateInfo(args);
291
+ if (!info) return null;
292
+ const { storageUri, ...rest } = info as UpdateInfo & {
293
+ storageUri: string | null;
294
+ };
295
+ const fileUrl = await resolveFileUrl(storageUri ?? null);
296
+ return { ...rest, fileUrl };
297
+ },
298
+
299
+ async getChannels(): Promise<string[]> {
300
+ const version = await client.version();
301
+ const orm = client.orm(version);
302
+ const rows = await orm.findMany("bundles", {
303
+ select: ["channel"],
304
+ });
305
+ const set = new Set(rows?.map((r) => r.channel) ?? []);
306
+ return Array.from(set);
307
+ },
308
+
309
+ async getBundles(options: {
310
+ where?: { channel?: string; platform?: string };
311
+ limit: number;
312
+ offset: number;
313
+ }): Promise<{ data: Bundle[]; pagination: PaginationInfo }> {
314
+ const version = await client.version();
315
+ const orm = client.orm(version);
316
+ const { where, limit, offset } = options;
317
+
318
+ const rows = await orm.findMany("bundles", {
319
+ select: [
320
+ "id",
321
+ "platform",
322
+ "should_force_update",
323
+ "enabled",
324
+ "file_hash",
325
+ "git_commit_hash",
326
+ "message",
327
+ "channel",
328
+ "storage_uri",
329
+ "target_app_version",
330
+ "fingerprint_hash",
331
+ "metadata",
332
+ ],
333
+ where: (b) => {
334
+ const conditions = [];
335
+ if (where?.channel) {
336
+ conditions.push(b("channel", "=", where.channel));
337
+ }
338
+ if (where?.platform) {
339
+ conditions.push(b("platform", "=", where.platform));
340
+ }
341
+ return conditions.length > 0 ? b.and(...conditions) : true;
342
+ },
343
+ });
344
+
345
+ const all: Bundle[] = rows
346
+ .map(
347
+ (r): Bundle => ({
348
+ id: r.id,
349
+ platform: r.platform as Platform,
350
+ shouldForceUpdate: Boolean(r.should_force_update),
351
+ enabled: Boolean(r.enabled),
352
+ fileHash: r.file_hash,
353
+ gitCommitHash: r.git_commit_hash ?? null,
354
+ message: r.message ?? null,
355
+ channel: r.channel,
356
+ storageUri: r.storage_uri,
357
+ targetAppVersion: r.target_app_version ?? null,
358
+ fingerprintHash: r.fingerprint_hash ?? null,
359
+ }),
360
+ )
361
+ .sort((a, b) => b.id.localeCompare(a.id));
362
+
363
+ const total = all.length;
364
+ const sliced = all.slice(offset, offset + limit);
365
+
366
+ return {
367
+ data: sliced,
368
+ pagination: calculatePagination(total, { limit, offset }),
369
+ };
370
+ },
371
+
372
+ async insertBundle(bundle: Bundle): Promise<void> {
373
+ const version = await client.version();
374
+ const orm = client.orm(version);
375
+ const values = {
376
+ id: bundle.id,
377
+ platform: bundle.platform,
378
+ should_force_update: bundle.shouldForceUpdate,
379
+ enabled: bundle.enabled,
380
+ file_hash: bundle.fileHash,
381
+ git_commit_hash: bundle.gitCommitHash,
382
+ message: bundle.message,
383
+ channel: bundle.channel,
384
+ storage_uri: bundle.storageUri,
385
+ target_app_version: bundle.targetAppVersion,
386
+ fingerprint_hash: bundle.fingerprintHash,
387
+ metadata: bundle.metadata ?? {},
388
+ };
389
+ const { id, ...updateValues } = values;
390
+ await orm.upsert("bundles", {
391
+ where: (b) => b("id", "=", id),
392
+ create: values,
393
+ update: updateValues,
394
+ });
395
+ },
396
+
397
+ async updateBundleById(
398
+ bundleId: string,
399
+ newBundle: Partial<Bundle>,
400
+ ): Promise<void> {
401
+ const version = await client.version();
402
+ const orm = client.orm(version);
403
+ const current = await this.getBundleById(bundleId);
404
+ if (!current) throw new Error("targetBundleId not found");
405
+ const merged: Bundle = { ...current, ...newBundle };
406
+ const values = {
407
+ id: merged.id,
408
+ platform: merged.platform,
409
+ should_force_update: merged.shouldForceUpdate,
410
+ enabled: merged.enabled,
411
+ file_hash: merged.fileHash,
412
+ git_commit_hash: merged.gitCommitHash,
413
+ message: merged.message,
414
+ channel: merged.channel,
415
+ storage_uri: merged.storageUri,
416
+ target_app_version: merged.targetAppVersion,
417
+ fingerprint_hash: merged.fingerprintHash,
418
+ metadata: merged.metadata ?? {},
419
+ };
420
+ const { id: id2, ...updateValues2 } = values;
421
+ await orm.upsert("bundles", {
422
+ where: (b) => b("id", "=", id2),
423
+ create: values,
424
+ update: updateValues2,
425
+ });
426
+ },
427
+
428
+ async deleteBundleById(bundleId: string): Promise<void> {
429
+ const version = await client.version();
430
+ const orm = client.orm(version);
431
+ await orm.deleteMany("bundles", { where: (b) => b("id", "=", bundleId) });
432
+ },
433
+ };
434
+
435
+ return {
436
+ api,
437
+ adapterName: client.adapter.name,
438
+ createMigrator: () => client.createMigrator(),
439
+ generateSchema: client.generateSchema,
440
+ };
441
+ }
@@ -0,0 +1,119 @@
1
+ import type {
2
+ AppUpdateInfo,
3
+ Bundle,
4
+ GetBundlesArgs,
5
+ UpdateInfo,
6
+ } from "@hot-updater/core";
7
+ import { getUpdateInfo as getUpdateInfoJS } from "@hot-updater/js";
8
+ import type { DatabasePlugin } from "@hot-updater/plugin-core";
9
+ import type { DatabaseAPI } from "./types";
10
+
11
+ export function createPluginDatabaseCore(
12
+ plugin: DatabasePlugin,
13
+ resolveFileUrl: (storageUri: string | null) => Promise<string | null>,
14
+ ): {
15
+ api: DatabaseAPI;
16
+ adapterName: string;
17
+ createMigrator: () => never;
18
+ generateSchema: () => never;
19
+ } {
20
+ const api: DatabaseAPI = {
21
+ async getBundleById(id: string): Promise<Bundle | null> {
22
+ return plugin.getBundleById(id);
23
+ },
24
+
25
+ async getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null> {
26
+ const where: { channel?: string; platform?: string } = {};
27
+
28
+ if ("platform" in args && args.platform) {
29
+ where.platform = args.platform;
30
+ }
31
+
32
+ const channel =
33
+ "channel" in args && args.channel ? args.channel : "production";
34
+ where.channel = channel;
35
+
36
+ const { pagination } = await plugin.getBundles({
37
+ where,
38
+ limit: 1,
39
+ offset: 0,
40
+ });
41
+
42
+ if (pagination.total === 0) {
43
+ return getUpdateInfoJS([], args);
44
+ }
45
+
46
+ const { data } = await plugin.getBundles({
47
+ where,
48
+ limit: pagination.total,
49
+ offset: 0,
50
+ });
51
+
52
+ const bundles = data;
53
+ return getUpdateInfoJS(bundles, args);
54
+ },
55
+
56
+ async getAppUpdateInfo(
57
+ args: GetBundlesArgs,
58
+ ): Promise<AppUpdateInfo | null> {
59
+ const info = await this.getUpdateInfo(args);
60
+ if (!info) {
61
+ return null;
62
+ }
63
+ const { storageUri, ...rest } = info as UpdateInfo & {
64
+ storageUri: string | null;
65
+ };
66
+ const fileUrl = await resolveFileUrl(storageUri ?? null);
67
+ return { ...rest, fileUrl };
68
+ },
69
+
70
+ async getChannels(): Promise<string[]> {
71
+ return plugin.getChannels();
72
+ },
73
+
74
+ async getBundles(options: {
75
+ where?: { channel?: string; platform?: string };
76
+ limit: number;
77
+ offset: number;
78
+ }) {
79
+ return plugin.getBundles(options);
80
+ },
81
+
82
+ async insertBundle(bundle: Bundle): Promise<void> {
83
+ await plugin.appendBundle(bundle);
84
+ await plugin.commitBundle();
85
+ },
86
+
87
+ async updateBundleById(
88
+ bundleId: string,
89
+ newBundle: Partial<Bundle>,
90
+ ): Promise<void> {
91
+ await plugin.updateBundle(bundleId, newBundle);
92
+ await plugin.commitBundle();
93
+ },
94
+
95
+ async deleteBundleById(bundleId: string): Promise<void> {
96
+ const bundle = await plugin.getBundleById(bundleId);
97
+ if (!bundle) {
98
+ return;
99
+ }
100
+ await plugin.deleteBundle(bundle);
101
+ await plugin.commitBundle();
102
+ },
103
+ };
104
+
105
+ return {
106
+ api,
107
+ adapterName: plugin.name,
108
+ createMigrator: () => {
109
+ throw new Error(
110
+ "createMigrator is only available for Kysely/Prisma/Drizzle database adapters.",
111
+ );
112
+ },
113
+ generateSchema: () => {
114
+ throw new Error(
115
+ "generateSchema is only available for Kysely/Prisma/Drizzle database adapters.",
116
+ );
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,57 @@
1
+ import type {
2
+ AppUpdateInfo,
3
+ Bundle,
4
+ GetBundlesArgs,
5
+ UpdateInfo,
6
+ } from "@hot-updater/core";
7
+ import type { DatabasePlugin, StoragePlugin } from "@hot-updater/plugin-core";
8
+ import type { FumaDBAdapter } from "fumadb/adapters";
9
+ import type { PaginationInfo } from "../types";
10
+
11
+ export type DatabasePluginFactory = () => DatabasePlugin;
12
+
13
+ export type DatabaseAdapter =
14
+ | FumaDBAdapter
15
+ | DatabasePlugin
16
+ | DatabasePluginFactory;
17
+
18
+ export function isDatabasePluginFactory(
19
+ adapter: DatabaseAdapter,
20
+ ): adapter is DatabasePluginFactory {
21
+ return typeof adapter === "function";
22
+ }
23
+
24
+ export function isDatabasePlugin(
25
+ adapter: DatabaseAdapter,
26
+ ): adapter is DatabasePlugin {
27
+ return (
28
+ typeof adapter === "object" &&
29
+ adapter !== null &&
30
+ "getBundleById" in adapter &&
31
+ "getBundles" in adapter &&
32
+ "getChannels" in adapter
33
+ );
34
+ }
35
+
36
+ export function isFumaAdapter(
37
+ adapter: DatabaseAdapter,
38
+ ): adapter is FumaDBAdapter {
39
+ return !isDatabasePluginFactory(adapter) && !isDatabasePlugin(adapter);
40
+ }
41
+
42
+ export interface DatabaseAPI {
43
+ getBundleById(id: string): Promise<Bundle | null>;
44
+ getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null>;
45
+ getAppUpdateInfo(args: GetBundlesArgs): Promise<AppUpdateInfo | null>;
46
+ getChannels(): Promise<string[]>;
47
+ getBundles(options: {
48
+ where?: { channel?: string; platform?: string };
49
+ limit: number;
50
+ offset: number;
51
+ }): Promise<{ data: Bundle[]; pagination: PaginationInfo }>;
52
+ insertBundle(bundle: Bundle): Promise<void>;
53
+ updateBundleById(bundleId: string, newBundle: Partial<Bundle>): Promise<void>;
54
+ deleteBundleById(bundleId: string): Promise<void>;
55
+ }
56
+
57
+ export type StoragePluginFactory = () => StoragePlugin;
@@ -1,7 +1,6 @@
1
1
  import { PGlite } from "@electric-sql/pglite";
2
2
  import type { Bundle } from "@hot-updater/core";
3
3
  import { NIL_UUID } from "@hot-updater/core";
4
- import { kyselyAdapter } from "@hot-updater/server/adapters/kysely";
5
4
  import { standaloneRepository } from "@hot-updater/standalone";
6
5
  import { Kysely } from "kysely";
7
6
  import { PGliteDialect } from "kysely-pglite-dialect";
@@ -9,6 +8,7 @@ import { HttpResponse, http } from "msw";
9
8
  import { setupServer } from "msw/node";
10
9
  import { uuidv7 } from "uuidv7";
11
10
  import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
11
+ import { kyselyAdapter } from "./adapters/kysely";
12
12
  import { createHotUpdater } from "./db";
13
13
 
14
14
  /**
@@ -108,7 +108,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
108
108
  // Create standalone repository pointing to our test server
109
109
  const repo = standaloneRepository({
110
110
  baseUrl: `${baseUrl}/hot-updater`,
111
- })({ cwd: process.cwd() });
111
+ })();
112
112
 
113
113
  const bundleId = uuidv7();
114
114
  const bundle = createTestBundle({
@@ -146,7 +146,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
146
146
  // Create standalone repository
147
147
  const repo = standaloneRepository({
148
148
  baseUrl: `${baseUrl}/hot-updater`,
149
- })({ cwd: process.cwd() });
149
+ })();
150
150
 
151
151
  // Use standalone repository to retrieve
152
152
  const retrieved = await repo.getBundleById(bundleId);
@@ -173,7 +173,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
173
173
  // Create standalone repository
174
174
  const repo = standaloneRepository({
175
175
  baseUrl: `${baseUrl}/hot-updater`,
176
- })({ cwd: process.cwd() });
176
+ })();
177
177
 
178
178
  // Delete via standalone repository
179
179
  await repo.deleteBundle(bundle);
@@ -199,7 +199,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
199
199
  // Create standalone repository
200
200
  const repo = standaloneRepository({
201
201
  baseUrl: `${baseUrl}/hot-updater`,
202
- })({ cwd: process.cwd() });
202
+ })();
203
203
 
204
204
  // Get all bundles
205
205
  const result = await repo.getBundles({ limit: 50, offset: 0 });
@@ -220,7 +220,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
220
220
  it("Full E2E: create → retrieve → update → delete via standalone", async () => {
221
221
  const repo = standaloneRepository({
222
222
  baseUrl: `${baseUrl}/hot-updater`,
223
- })({ cwd: process.cwd() });
223
+ })();
224
224
 
225
225
  // Step 1: Create bundle via standalone
226
226
  const bundleId = uuidv7();
@@ -258,7 +258,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
258
258
  it("Multiple bundles in single commit (standalone sends array)", async () => {
259
259
  const repo = standaloneRepository({
260
260
  baseUrl: `${baseUrl}/hot-updater`,
261
- })({ cwd: process.cwd() });
261
+ })();
262
262
 
263
263
  // Append multiple bundles
264
264
  const bundleId1 = uuidv7();
@@ -316,7 +316,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
316
316
  // Create standalone repository with matching basePath
317
317
  const repo = standaloneRepository({
318
318
  baseUrl: `${baseUrl}/api/v2`,
319
- })({ cwd: process.cwd() });
319
+ })();
320
320
 
321
321
  // Test create and retrieve
322
322
  const bundleId = uuidv7();
@@ -336,7 +336,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
336
336
  it("Handler returns 404 when bundle not found (standalone handles gracefully)", async () => {
337
337
  const repo = standaloneRepository({
338
338
  baseUrl: `${baseUrl}/hot-updater`,
339
- })({ cwd: process.cwd() });
339
+ })();
340
340
 
341
341
  // Try to get non-existent bundle
342
342
  const result = await repo.getBundleById("non-existent-bundle");