@hot-updater/server 0.36.6 → 1.0.0-rc.0

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 (291) hide show
  1. package/dist/adapters/databaseConstraintErrors.cjs +23 -0
  2. package/dist/adapters/databaseConstraintErrors.mjs +21 -0
  3. package/dist/adapters/databasePluginUtils.cjs +117 -0
  4. package/dist/adapters/databasePluginUtils.mjs +106 -0
  5. package/dist/adapters/drizzle.cjs +53 -176
  6. package/dist/adapters/drizzle.d.cts +6 -4
  7. package/dist/adapters/drizzle.d.mts +6 -4
  8. package/dist/adapters/drizzle.mjs +54 -177
  9. package/dist/adapters/drizzleCrud.cjs +250 -0
  10. package/dist/adapters/drizzleCrud.mjs +249 -0
  11. package/dist/adapters/drizzleLazyDB.cjs +84 -36
  12. package/dist/adapters/drizzleLazyDB.mjs +84 -36
  13. package/dist/adapters/drizzleQuery.cjs +68 -0
  14. package/dist/adapters/drizzleQuery.mjs +67 -0
  15. package/dist/adapters/kysely.cjs +18 -138
  16. package/dist/adapters/kysely.d.cts +3 -12
  17. package/dist/adapters/kysely.d.mts +3 -12
  18. package/dist/adapters/kysely.mjs +19 -139
  19. package/dist/adapters/kyselyCrud.cjs +254 -0
  20. package/dist/adapters/kyselyCrud.mjs +253 -0
  21. package/dist/adapters/kyselyQuery.cjs +56 -0
  22. package/dist/adapters/kyselyQuery.mjs +55 -0
  23. package/dist/adapters/mongodb.cjs +21 -174
  24. package/dist/adapters/mongodb.d.cts +3 -2
  25. package/dist/adapters/mongodb.d.mts +3 -2
  26. package/dist/adapters/mongodb.mjs +21 -174
  27. package/dist/adapters/mongodbCollections.cjs +36 -0
  28. package/dist/adapters/mongodbCollections.mjs +30 -0
  29. package/dist/adapters/mongodbQuery.cjs +77 -0
  30. package/dist/adapters/mongodbQuery.mjs +70 -0
  31. package/dist/adapters/mongodbReads.cjs +148 -0
  32. package/dist/adapters/mongodbReads.mjs +147 -0
  33. package/dist/adapters/mongodbWrites.cjs +134 -0
  34. package/dist/adapters/mongodbWrites.mjs +133 -0
  35. package/dist/adapters/prisma.cjs +205 -180
  36. package/dist/adapters/prisma.d.cts +3 -3
  37. package/dist/adapters/prisma.d.mts +3 -3
  38. package/dist/adapters/prisma.mjs +206 -181
  39. package/dist/adapters/prismaQuery.cjs +58 -0
  40. package/dist/adapters/prismaQuery.mjs +56 -0
  41. package/dist/adapters/prismaRows.cjs +198 -0
  42. package/dist/adapters/prismaRows.mjs +188 -0
  43. package/dist/analytics/bounded/activeOverview.cjs +87 -0
  44. package/dist/analytics/bounded/activeOverview.mjs +85 -0
  45. package/dist/analytics/bounded/installationSearch.cjs +42 -0
  46. package/dist/analytics/bounded/installationSearch.mjs +42 -0
  47. package/dist/analytics/bounded/persistence.cjs +39 -0
  48. package/dist/analytics/bounded/persistence.mjs +38 -0
  49. package/dist/analytics/bounded/provider.cjs +170 -0
  50. package/dist/analytics/bounded/provider.d.cts +7 -0
  51. package/dist/analytics/bounded/provider.d.mts +7 -0
  52. package/dist/analytics/bounded/provider.mjs +170 -0
  53. package/dist/analytics/bounded/scan.cjs +144 -0
  54. package/dist/analytics/bounded/scan.mjs +138 -0
  55. package/dist/analytics/domain.d.cts +121 -0
  56. package/dist/analytics/domain.d.mts +121 -0
  57. package/dist/analytics/errors.cjs +22 -0
  58. package/dist/analytics/errors.mjs +20 -0
  59. package/dist/analytics/eventInput.cjs +111 -0
  60. package/dist/analytics/eventInput.mjs +111 -0
  61. package/dist/analytics/persistence.d.cts +6 -0
  62. package/dist/analytics/persistence.d.mts +6 -0
  63. package/dist/analytics/queryInput.cjs +58 -0
  64. package/dist/analytics/queryInput.mjs +55 -0
  65. package/dist/analytics/routes.cjs +63 -0
  66. package/dist/analytics/routes.mjs +61 -0
  67. package/dist/analytics/types.d.cts +17 -0
  68. package/dist/analytics/types.d.mts +17 -0
  69. package/dist/apiKeys.cjs +139 -0
  70. package/dist/apiKeys.d.cts +50 -0
  71. package/dist/apiKeys.d.mts +50 -0
  72. package/dist/apiKeys.mjs +133 -0
  73. package/dist/createHotUpdaterCore.cjs +48 -26
  74. package/dist/createHotUpdaterCore.d.cts +49 -17
  75. package/dist/createHotUpdaterCore.d.mts +49 -17
  76. package/dist/createHotUpdaterCore.mjs +51 -29
  77. package/dist/db/createBundleDiff.cjs +45 -68
  78. package/dist/db/createBundleDiff.d.cts +3 -3
  79. package/dist/db/createBundleDiff.d.mts +3 -3
  80. package/dist/db/createBundleDiff.mjs +47 -68
  81. package/dist/db/databasePluginCore.cjs +138 -0
  82. package/dist/db/databasePluginCore.mjs +137 -0
  83. package/dist/db/fixedMigrator.cjs +36 -93
  84. package/dist/db/fixedMigrator.mjs +36 -93
  85. package/dist/db/fixedMigratorMongo.cjs +319 -0
  86. package/dist/db/fixedMigratorMongo.mjs +319 -0
  87. package/dist/db/fixedMigratorShared.cjs +23 -0
  88. package/dist/db/fixedMigratorShared.mjs +18 -0
  89. package/dist/db/mongoMigrationExecution.cjs +11 -0
  90. package/dist/db/mongoMigrationExecution.mjs +11 -0
  91. package/dist/db/releaseCatalog.cjs +66 -0
  92. package/dist/db/releaseCatalog.d.cts +17 -0
  93. package/dist/db/releaseCatalog.d.mts +17 -0
  94. package/dist/db/releaseCatalog.mjs +64 -0
  95. package/dist/db/schema/mongodb.cjs +14 -6
  96. package/dist/db/schema/mongodb.mjs +14 -6
  97. package/dist/db/schema/registry.cjs +0 -2
  98. package/dist/db/schema/registry.mjs +1 -2
  99. package/dist/db/schema/sql.cjs +31 -14
  100. package/dist/db/schema/sql.mjs +32 -13
  101. package/dist/db/schema/sqlOperations.cjs +4 -4
  102. package/dist/db/schema/sqlOperations.mjs +5 -5
  103. package/dist/db/schemaGenerators.cjs +52 -18
  104. package/dist/db/schemaGenerators.mjs +52 -18
  105. package/dist/db/schemaReadiness.cjs +4 -3
  106. package/dist/db/schemaReadiness.mjs +4 -3
  107. package/dist/db/sqlMigrationExecution.cjs +67 -0
  108. package/dist/db/sqlMigrationExecution.mjs +66 -0
  109. package/dist/db/types.cjs +2 -6
  110. package/dist/db/types.d.cts +60 -16
  111. package/dist/db/types.d.mts +60 -16
  112. package/dist/db/types.mjs +3 -6
  113. package/dist/db/updateArtifacts.cjs +114 -60
  114. package/dist/db/updateArtifacts.mjs +117 -62
  115. package/dist/handler.cjs +108 -282
  116. package/dist/handler.d.cts +3 -46
  117. package/dist/handler.d.mts +3 -46
  118. package/dist/handler.mjs +107 -281
  119. package/dist/handlerBundleRoutes.cjs +155 -0
  120. package/dist/handlerBundleRoutes.mjs +155 -0
  121. package/dist/handlerErrors.cjs +9 -0
  122. package/dist/handlerErrors.mjs +9 -0
  123. package/dist/handlerParameters.cjs +39 -0
  124. package/dist/handlerParameters.mjs +34 -0
  125. package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
  126. package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
  127. package/dist/handlerReleaseManagementRoutes.cjs +125 -0
  128. package/dist/handlerReleaseManagementRoutes.mjs +124 -0
  129. package/dist/handlerTypes.d.cts +63 -0
  130. package/dist/handlerTypes.d.mts +63 -0
  131. package/dist/handlerVersionRoutes.cjs +10 -0
  132. package/dist/handlerVersionRoutes.d.cts +4 -0
  133. package/dist/handlerVersionRoutes.d.mts +4 -0
  134. package/dist/handlerVersionRoutes.mjs +9 -0
  135. package/dist/index.cjs +10 -1
  136. package/dist/index.d.cts +9 -3
  137. package/dist/index.d.mts +9 -3
  138. package/dist/index.mjs +5 -2
  139. package/dist/node.cjs +19 -12
  140. package/dist/node.d.cts +15 -10
  141. package/dist/node.d.mts +15 -10
  142. package/dist/node.mjs +19 -13
  143. package/dist/package.cjs +1 -1
  144. package/dist/package.mjs +1 -1
  145. package/dist/schema/dsl.cjs +19 -2
  146. package/dist/schema/dsl.mjs +17 -3
  147. package/dist/schema/index.cjs +1 -8
  148. package/dist/schema/index.mjs +2 -8
  149. package/dist/schema/types.cjs +3 -1
  150. package/dist/schema/types.mjs +3 -2
  151. package/dist/schema/v1_0_0.cjs +364 -0
  152. package/dist/schema/v1_0_0.mjs +357 -0
  153. package/dist/storageAccess.cjs +69 -25
  154. package/dist/storageAccess.mjs +68 -25
  155. package/dist/types/index.d.cts +4 -3
  156. package/dist/types/index.d.mts +5 -4
  157. package/package.json +8 -7
  158. package/src/adapters/databaseConstraintErrors.ts +25 -0
  159. package/src/adapters/databasePluginTestDatabase.ts +106 -0
  160. package/src/adapters/databasePluginUtils.spec.ts +29 -0
  161. package/src/adapters/databasePluginUtils.ts +214 -0
  162. package/src/adapters/drizzle.spec.ts +208 -47
  163. package/src/adapters/drizzle.ts +106 -293
  164. package/src/adapters/drizzleCrud.ts +419 -0
  165. package/src/adapters/drizzleLazyDB.ts +206 -100
  166. package/src/adapters/drizzleQuery.ts +164 -0
  167. package/src/adapters/kysely.spec.ts +126 -88
  168. package/src/adapters/kysely.ts +76 -334
  169. package/src/adapters/kyselyCrud.ts +588 -0
  170. package/src/adapters/kyselyQuery.ts +128 -0
  171. package/src/adapters/mongodb.spec.ts +141 -0
  172. package/src/adapters/mongodb.ts +60 -288
  173. package/src/adapters/mongodbCollections.ts +62 -0
  174. package/src/adapters/mongodbQuery.ts +226 -0
  175. package/src/adapters/mongodbReads.ts +311 -0
  176. package/src/adapters/mongodbRows.spec.ts +64 -0
  177. package/src/adapters/mongodbRows.ts +118 -0
  178. package/src/adapters/mongodbTestClient.ts +342 -0
  179. package/src/adapters/mongodbTestFilter.ts +166 -0
  180. package/src/adapters/mongodbWrites.ts +260 -0
  181. package/src/adapters/prisma.spec.ts +196 -0
  182. package/src/adapters/prisma.ts +417 -292
  183. package/src/adapters/prismaQuery.ts +129 -0
  184. package/src/adapters/prismaRows.spec.ts +98 -0
  185. package/src/adapters/prismaRows.ts +301 -0
  186. package/src/adapters/prismaTestClient.ts +396 -0
  187. package/src/analytics/analytics.integration.spec.ts +144 -0
  188. package/src/analytics/bounded/activeOverview.spec.ts +189 -0
  189. package/src/analytics/bounded/activeOverview.ts +148 -0
  190. package/src/analytics/bounded/installationSearch.spec.ts +130 -0
  191. package/src/analytics/bounded/installationSearch.ts +69 -0
  192. package/src/analytics/bounded/persistence.ts +43 -0
  193. package/src/analytics/bounded/provider.spec.ts +219 -0
  194. package/src/analytics/bounded/provider.ts +229 -0
  195. package/src/analytics/bounded/scan.ts +235 -0
  196. package/src/analytics/domain.ts +139 -0
  197. package/src/analytics/errors.ts +18 -0
  198. package/src/analytics/eventInput.ts +168 -0
  199. package/src/analytics/persistence.ts +10 -0
  200. package/src/analytics/queryInput.ts +102 -0
  201. package/src/analytics/routes.ts +123 -0
  202. package/src/analytics/types.ts +44 -0
  203. package/src/apiKeys.integration.spec.ts +192 -0
  204. package/src/apiKeys.spec.ts +162 -0
  205. package/src/apiKeys.ts +220 -0
  206. package/src/createHotUpdaterCore.ts +164 -100
  207. package/src/db/bundleRows.spec.ts +164 -0
  208. package/src/db/bundleRows.ts +10 -140
  209. package/src/db/createBundleDiff.spec.ts +172 -127
  210. package/src/db/createBundleDiff.ts +91 -110
  211. package/src/db/databasePluginCore.spec.ts +62 -0
  212. package/src/db/databasePluginCore.testFixtures.ts +76 -0
  213. package/src/db/databasePluginCore.ts +185 -0
  214. package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
  215. package/src/db/fixedMigrator.spec.ts +184 -42
  216. package/src/db/fixedMigrator.ts +64 -205
  217. package/src/db/fixedMigratorMongo.spec.ts +192 -0
  218. package/src/db/fixedMigratorMongo.ts +425 -0
  219. package/src/db/fixedMigratorShared.ts +36 -0
  220. package/src/db/hotUpdaterSchema.ts +0 -1
  221. package/src/db/index.spec.ts +333 -1025
  222. package/src/db/mongoMigrationExecution.ts +23 -0
  223. package/src/db/node-sqlite.d.ts +25 -0
  224. package/src/db/releaseCatalog.ts +148 -0
  225. package/src/db/schema/mongodb.ts +20 -6
  226. package/src/db/schema/sql.ts +52 -11
  227. package/src/db/schema/sqlOperations.ts +5 -4
  228. package/src/db/schemaGenerators.ts +107 -23
  229. package/src/db/schemaReadiness.ts +2 -1
  230. package/src/db/sqlMigrationExecution.spec.ts +65 -0
  231. package/src/db/sqlMigrationExecution.ts +108 -0
  232. package/src/db/types.spec.ts +55 -0
  233. package/src/db/types.ts +129 -65
  234. package/src/db/updateArtifacts.spec.ts +359 -0
  235. package/src/db/updateArtifacts.ts +290 -126
  236. package/src/handler-standalone.integration.spec.ts +401 -364
  237. package/src/handler.spec.ts +137 -582
  238. package/src/handler.testFixtures.ts +30 -0
  239. package/src/handler.ts +218 -616
  240. package/src/handlerAuth.integration.spec.ts +33 -0
  241. package/src/handlerBundleRoutes.ts +259 -0
  242. package/src/handlerErrors.ts +7 -0
  243. package/src/handlerManagement.spec.ts +262 -0
  244. package/src/handlerParameters.ts +105 -0
  245. package/src/handlerReleaseCatalogRoutes.ts +154 -0
  246. package/src/handlerReleaseManagementRoutes.ts +213 -0
  247. package/src/handlerTypes.ts +96 -0
  248. package/src/handlerVersionRoutes.ts +12 -0
  249. package/src/index.ts +22 -2
  250. package/src/node.spec.ts +79 -10
  251. package/src/node.ts +20 -17
  252. package/src/releaseCatalogRoutes.integration.spec.ts +238 -0
  253. package/src/runtime.spec.ts +126 -959
  254. package/src/runtime.testFixtures.ts +71 -0
  255. package/src/schema/dsl-all-versions.spec.ts +26 -17
  256. package/src/schema/dsl.ts +41 -2
  257. package/src/schema/index.ts +3 -9
  258. package/src/schema/types.ts +9 -3
  259. package/src/schema/v1_0_0.spec.ts +146 -0
  260. package/src/schema/v1_0_0.ts +465 -0
  261. package/src/storageAccess.spec.ts +127 -32
  262. package/src/storageAccess.ts +106 -55
  263. package/src/types/index.ts +2 -1
  264. package/dist/db/bundleRows.cjs +0 -82
  265. package/dist/db/bundleRows.d.cts +0 -33
  266. package/dist/db/bundleRows.d.mts +0 -33
  267. package/dist/db/bundleRows.mjs +0 -79
  268. package/dist/db/pluginCore.cjs +0 -242
  269. package/dist/db/pluginCore.mjs +0 -241
  270. package/dist/db/schema/sqlMigrations.cjs +0 -74
  271. package/dist/db/schema/sqlMigrations.mjs +0 -73
  272. package/dist/db/schemaEnhancements.cjs +0 -20
  273. package/dist/db/schemaEnhancements.mjs +0 -19
  274. package/dist/route.cjs +0 -7
  275. package/dist/route.mjs +0 -7
  276. package/dist/schema/v0_21_0.cjs +0 -38
  277. package/dist/schema/v0_21_0.mjs +0 -37
  278. package/dist/schema/v0_29_0.cjs +0 -45
  279. package/dist/schema/v0_29_0.mjs +0 -44
  280. package/dist/schema/v0_31_0.cjs +0 -82
  281. package/dist/schema/v0_31_0.mjs +0 -80
  282. package/src/db/pluginCore.spec.ts +0 -1095
  283. package/src/db/pluginCore.ts +0 -475
  284. package/src/db/pluginUpdateCheck.bench.ts +0 -254
  285. package/src/db/schema/sqlMigrations.ts +0 -219
  286. package/src/db/schemaEnhancements.ts +0 -55
  287. package/src/route.ts +0 -7
  288. package/src/schema/v0_21_0.ts +0 -52
  289. package/src/schema/v0_29_0.ts +0 -62
  290. package/src/schema/v0_31_0.spec.ts +0 -73
  291. package/src/schema/v0_31_0.ts +0 -120
@@ -1,635 +1,190 @@
1
- import { type Bundle, NIL_UUID } from "@hot-updater/core";
1
+ import type { ReleaseCatalog } from "@hot-updater/core";
2
+ import { Hono } from "hono";
2
3
  import { describe, expect, it, vi } from "vitest";
3
4
 
4
- import { createHandler, type HandlerAPI, type HandlerRoutes } from "./handler";
5
+ import {
6
+ createHandlers,
7
+ createHotUpdaterHandlers,
8
+ type HandlerAPI,
9
+ } from "./handler";
10
+ import { createApi } from "./handler.testFixtures";
11
+ import { HOT_UPDATER_INFRASTRUCTURE_GENERATION } from "./handlerVersionRoutes";
5
12
  import { HOT_UPDATER_SERVER_VERSION } from "./version";
6
13
 
7
- const NEXT_SDK_VERSION_FOR_TEST = "0.31.0";
8
- const CURRENT_PACKAGE_SDK_VERSION = "0.30.10";
9
-
10
- type TestEnv = {
11
- tenantId: string;
12
- };
13
-
14
- type TestContext = {
15
- env: TestEnv;
16
- };
17
-
18
- const testBundle: Bundle = {
19
- id: "bundle-1",
20
- platform: "ios",
21
- shouldForceUpdate: false,
22
- enabled: true,
23
- fileHash: "hash123",
24
- gitCommitHash: null,
25
- message: "Test bundle",
26
- channel: "production",
27
- storageUri: "s3://test-bucket/bundles/bundle-1.zip",
28
- targetAppVersion: "1.0.0",
29
- fingerprintHash: null,
30
- };
31
-
32
- const createApi = () =>
33
- ({
34
- getAppUpdateInfo: vi
35
- .fn<HandlerAPI<TestContext>["getAppUpdateInfo"]>()
36
- .mockResolvedValue({
37
- fileHash: null,
38
- fileUrl: null,
39
- id: NIL_UUID,
40
- message: null,
41
- shouldForceUpdate: true,
42
- status: "ROLLBACK",
43
- }),
44
- getBundleById: vi.fn<HandlerAPI<TestContext>["getBundleById"]>(),
45
- getBundles: vi.fn<HandlerAPI<TestContext>["getBundles"]>(),
46
- getChannels: vi
47
- .fn<HandlerAPI<TestContext>["getChannels"]>()
48
- .mockResolvedValue(["production"]),
49
- insertBundle: vi.fn<HandlerAPI<TestContext>["insertBundle"]>(),
50
- updateBundleById: vi.fn<HandlerAPI<TestContext>["updateBundleById"]>(),
51
- deleteBundleById: vi.fn<HandlerAPI<TestContext>["deleteBundleById"]>(),
52
- }) satisfies HandlerAPI<TestContext>;
53
-
54
- const createManagementHandler = (
55
- api: HandlerAPI<TestContext>,
56
- routes: Partial<HandlerRoutes> = {},
57
- ) =>
58
- createHandler(api, {
59
- basePath: "/hot-updater",
60
- routes: {
61
- updateCheck: true,
62
- bundles: true,
63
- ...routes,
64
- },
65
- });
66
-
67
- describe("createHandler", () => {
68
- it("supports the app-version route without a cohort segment", async () => {
69
- const api = createApi();
70
- const handler = createHandler(api, { basePath: "/hot-updater" });
71
-
72
- const response = await handler(
73
- new Request(
74
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
75
- ),
76
- {
77
- env: {
78
- tenantId: "tenant-a",
79
- },
80
- },
81
- );
82
-
83
- expect(response.status).toBe(200);
84
- expect(api.getAppUpdateInfo).toHaveBeenCalledWith(
85
- {
86
- _updateStrategy: "appVersion",
87
- appVersion: "1.0.0",
88
- bundleId: "default",
89
- channel: "production",
90
- cohort: undefined,
91
- minBundleId: "default",
92
- platform: "ios",
93
- },
94
- {
95
- env: {
96
- tenantId: "tenant-a",
97
- },
98
- },
99
- );
100
- });
101
-
102
- it("keeps legacy no-update responses as null when SDK version is missing", async () => {
103
- const api = createApi();
104
- api.getAppUpdateInfo.mockResolvedValueOnce(null);
105
- const handler = createHandler(api, { basePath: "/hot-updater" });
106
-
107
- const response = await handler(
108
- new Request(
109
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
110
- ),
111
- );
112
-
113
- expect(response.status).toBe(200);
114
- await expect(response.json()).resolves.toBeNull();
115
- });
116
-
117
- it("returns UP_TO_DATE for no-update responses from SDK-versioned clients", async () => {
118
- const api = createApi();
119
- api.getAppUpdateInfo.mockResolvedValueOnce(null);
120
- const handler = createHandler(api, { basePath: "/hot-updater" });
121
-
122
- const response = await handler(
123
- new Request(
124
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
125
- {
126
- headers: {
127
- "Hot-Updater-SDK-Version": NEXT_SDK_VERSION_FOR_TEST,
128
- },
129
- },
130
- ),
131
- );
14
+ describe("createHandlers client routes", () => {
15
+ it("reports the v1 infrastructure generation", async () => {
16
+ const handler = createHandlers(createApi()).client;
17
+ const response = await handler(new Request("http://localhost/version"));
132
18
 
133
19
  expect(response.status).toBe(200);
134
20
  await expect(response.json()).resolves.toEqual({
135
- status: "UP_TO_DATE",
136
- });
137
- });
138
-
139
- it("keeps no-update responses as null for unsupported SDK versions", async () => {
140
- const api = createApi();
141
- api.getAppUpdateInfo.mockResolvedValueOnce(null);
142
- const handler = createHandler(api, { basePath: "/hot-updater" });
143
-
144
- const response = await handler(
145
- new Request(
146
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
147
- {
148
- headers: {
149
- "Hot-Updater-SDK-Version": CURRENT_PACKAGE_SDK_VERSION,
150
- },
151
- },
152
- ),
153
- );
154
-
155
- expect(response.status).toBe(200);
156
- await expect(response.json()).resolves.toBeNull();
157
- });
158
-
159
- it("keeps no-update responses as null for invalid SDK versions", async () => {
160
- const api = createApi();
161
- api.getAppUpdateInfo.mockResolvedValueOnce(null);
162
- const handler = createHandler(api, { basePath: "/hot-updater" });
163
-
164
- const response = await handler(
165
- new Request(
166
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
167
- {
168
- headers: {
169
- "Hot-Updater-SDK-Version": "invalid",
170
- },
171
- },
172
- ),
173
- );
174
-
175
- expect(response.status).toBe(200);
176
- await expect(response.json()).resolves.toBeNull();
177
- });
178
-
179
- it("supports the fingerprint route without a cohort segment", async () => {
180
- const api = createApi();
181
- const handler = createHandler(api, { basePath: "/hot-updater" });
182
-
183
- const response = await handler(
184
- new Request(
185
- "http://localhost/hot-updater/fingerprint/android/fingerprint-123/production/default/default",
186
- ),
187
- );
188
-
189
- expect(response.status).toBe(200);
190
- expect(api.getAppUpdateInfo).toHaveBeenCalledWith(
191
- {
192
- _updateStrategy: "fingerprint",
193
- bundleId: "default",
194
- channel: "production",
195
- cohort: undefined,
196
- fingerprintHash: "fingerprint-123",
197
- minBundleId: "default",
198
- platform: "android",
199
- },
200
- undefined,
201
- );
202
- });
203
-
204
- it("keeps version mounted when bundle routes are disabled", async () => {
205
- const api = createApi();
206
- const handler = createHandler(api, {
207
- basePath: "/hot-updater",
208
- routes: {
209
- updateCheck: true,
210
- bundles: false,
211
- },
212
- });
213
-
214
- const versionResponse = await handler(
215
- new Request("http://localhost/hot-updater/version"),
216
- );
217
- const bundlesResponse = await handler(
218
- new Request("http://localhost/hot-updater/api/bundles"),
219
- );
220
- const updateResponse = await handler(
221
- new Request(
222
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
223
- ),
224
- );
225
-
226
- expect(versionResponse.status).toBe(200);
227
- await expect(versionResponse.json()).resolves.toEqual({
21
+ infrastructureGeneration: HOT_UPDATER_INFRASTRUCTURE_GENERATION,
228
22
  version: HOT_UPDATER_SERVER_VERSION,
229
23
  });
230
- expect(bundlesResponse.status).toBe(404);
231
- expect(updateResponse.status).toBe(200);
232
- });
233
-
234
- it("does not mount bundle routes by default", async () => {
235
- const api = createApi();
236
- const handler = createHandler(api, { basePath: "/hot-updater" });
237
-
238
- const versionResponse = await handler(
239
- new Request("http://localhost/hot-updater/version"),
240
- );
241
- const bundlesResponse = await handler(
242
- new Request("http://localhost/hot-updater/api/bundles"),
243
- );
244
- const updateResponse = await handler(
245
- new Request(
246
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
247
- ),
248
- );
249
-
250
- expect(versionResponse.status).toBe(200);
251
- expect(bundlesResponse.status).toBe(404);
252
- expect(updateResponse.status).toBe(200);
253
- });
254
-
255
- it("mounts bundle routes when explicitly enabled", async () => {
256
- const api = createApi();
257
- api.getBundles.mockResolvedValueOnce({
258
- data: [],
259
- pagination: {
260
- total: 0,
261
- hasNextPage: false,
262
- hasPreviousPage: false,
263
- currentPage: 1,
264
- totalPages: 0,
265
- },
266
- });
267
- const handler = createHandler(api, {
268
- basePath: "/hot-updater",
269
- routes: {
270
- updateCheck: true,
271
- bundles: true,
272
- },
273
- });
274
-
275
- const response = await handler(
276
- new Request("http://localhost/hot-updater/api/bundles"),
277
- );
278
-
279
- expect(response.status).toBe(200);
280
- expect(api.getBundles).toHaveBeenCalledWith(
281
- {
282
- cursor: undefined,
283
- limit: 50,
284
- page: undefined,
285
- where: {},
286
- },
287
- undefined,
288
- );
289
- });
290
-
291
- it("keeps update-check routes mounted for partial runtime route config", async () => {
292
- const api = createApi();
293
- const handler = createHandler(api, {
294
- basePath: "/hot-updater",
295
- routes: JSON.parse('{"bundles":true}') as HandlerRoutes,
296
- });
297
-
298
- const updateResponse = await handler(
299
- new Request(
300
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
301
- ),
302
- );
303
-
304
- expect(updateResponse.status).toBe(200);
305
24
  });
306
25
 
307
- it("keeps the version route mounted when update-check routes are disabled", async () => {
308
- const api = createApi();
309
- const handler = createHandler(api, {
310
- basePath: "/hot-updater",
311
- routes: {
312
- updateCheck: false,
313
- bundles: false,
314
- },
315
- });
26
+ it.each([
27
+ "/app-version/ios/1.0.0/production/default/default",
28
+ "/fingerprint/android/fingerprint-123/production/default/default",
29
+ ])("does not expose the v0 route %s", async (path) => {
30
+ const handler = createHandlers(createApi()).client;
316
31
 
317
- const versionResponse = await handler(
318
- new Request("http://localhost/hot-updater/version"),
319
- );
320
- const bundlesResponse = await handler(
321
- new Request("http://localhost/hot-updater/api/bundles"),
322
- );
323
- const updateResponse = await handler(
324
- new Request(
325
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
326
- ),
327
- );
32
+ const response = await handler(new Request(`http://localhost${path}`));
328
33
 
329
- expect(versionResponse.status).toBe(200);
330
- await expect(versionResponse.json()).resolves.toEqual({
331
- version: HOT_UPDATER_SERVER_VERSION,
332
- });
333
- expect(bundlesResponse.status).toBe(404);
334
- expect(updateResponse.status).toBe(404);
34
+ expect(response.status).toBe(404);
335
35
  });
336
36
 
337
- it("can mount bundle routes without update-check routes", async () => {
338
- const api = createApi();
339
- const handler = createManagementHandler(api, { updateCheck: false });
37
+ it.each([
38
+ "/v2/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0",
39
+ "/v2/artifacts/target-bundle/from/current-bundle",
40
+ ])("does not expose the version-prefixed route %s", async (path) => {
41
+ const handler = createHandlers(createApi()).client;
340
42
 
341
- const versionResponse = await handler(
342
- new Request("http://localhost/hot-updater/version"),
343
- );
344
- const channelsResponse = await handler(
345
- new Request("http://localhost/hot-updater/api/bundles/channels"),
346
- );
347
- const updateResponse = await handler(
348
- new Request(
349
- "http://localhost/hot-updater/app-version/ios/1.0.0/production/default/default",
350
- ),
351
- );
43
+ const response = await handler(new Request(`http://localhost${path}`));
352
44
 
353
- expect(versionResponse.status).toBe(200);
354
- await expect(versionResponse.json()).resolves.toEqual({
355
- version: HOT_UPDATER_SERVER_VERSION,
356
- });
357
- expect(channelsResponse.status).toBe(200);
358
- await expect(channelsResponse.json()).resolves.toEqual({
359
- data: {
360
- channels: ["production"],
361
- },
362
- });
363
- expect(api.getChannels).toHaveBeenCalledWith(undefined);
364
- expect(updateResponse.status).toBe(404);
45
+ expect(response.status).toBe(404);
365
46
  });
366
47
 
367
- it("returns paginated bundle results in the response body", async () => {
368
- const api = createApi();
369
- api.getBundles.mockResolvedValue({
370
- data: [testBundle],
371
- pagination: {
372
- total: 51,
373
- hasNextPage: true,
374
- hasPreviousPage: true,
375
- currentPage: 6,
376
- totalPages: 26,
377
- },
378
- });
379
- const handler = createManagementHandler(api);
48
+ it("does not match the admin mount namespace", async () => {
49
+ const handler = createHandlers(createApi()).client;
380
50
 
381
51
  const response = await handler(
382
- new Request(
383
- "http://localhost/hot-updater/api/bundles?channel=production&platform=ios&limit=2",
384
- ),
52
+ new Request("http://localhost/admin/channels"),
385
53
  );
386
54
 
387
- expect(response.status).toBe(200);
388
- expect(response.headers.get("X-Total-Count")).toBeNull();
389
- await expect(response.json()).resolves.toEqual({
390
- data: [testBundle],
391
- pagination: {
392
- total: 51,
393
- hasNextPage: true,
394
- hasPreviousPage: true,
395
- currentPage: 6,
396
- totalPages: 26,
397
- },
398
- });
399
- expect(api.getBundles).toHaveBeenCalledWith(
400
- {
401
- where: {
402
- channel: "production",
403
- platform: "ios",
404
- },
405
- limit: 2,
406
- page: undefined,
407
- },
408
- undefined,
409
- );
55
+ expect(response.status).toBe(404);
410
56
  });
411
57
 
412
- it("passes advanced bundle filters through to getBundles", async () => {
413
- const api = createApi();
414
- api.getBundles.mockResolvedValue({
415
- data: [testBundle],
416
- pagination: {
417
- total: 1,
418
- hasNextPage: false,
419
- hasPreviousPage: false,
420
- currentPage: 1,
421
- totalPages: 1,
422
- },
423
- });
424
- const handler = createManagementHandler(api);
58
+ it("returns the stored Catalog identity without accepting identity parameters", async () => {
59
+ const catalog = {
60
+ catalogId: "project-a",
61
+ catalogHash: "sha256:catalog",
62
+ fallbackPolicy: "BUILTIN_IF_ACTIVE_INELIGIBLE",
63
+ generation: 1,
64
+ releases: [],
65
+ schemaVersion: 1,
66
+ scopeKey: "v1:fingerprint:android:cHJvZHVjdGlvbg:fingerprint-123",
67
+ } satisfies ReleaseCatalog;
68
+ const getReleaseCatalog = vi
69
+ .fn<NonNullable<HandlerAPI["getReleaseCatalog"]>>()
70
+ .mockResolvedValue(catalog);
71
+ const handler = createHandlers({
72
+ ...createApi(),
73
+ getReleaseCatalog,
74
+ }).client;
75
+ const url =
76
+ "http://localhost/release-catalogs/fingerprint/android/" +
77
+ "cHJvZHVjdGlvbg/fingerprint-123";
425
78
 
426
- const response = await handler(
427
- new Request(
428
- "http://localhost/hot-updater/api/bundles?channel=production&platform=ios&enabled=true&idLt=bundle-9&targetAppVersion=1.0.x&targetAppVersionNotNull=true&fingerprintHash=null&limit=5",
429
- ),
430
- );
79
+ const response = await handler(new Request(url));
431
80
 
432
81
  expect(response.status).toBe(200);
433
- expect(api.getBundles).toHaveBeenCalledWith(
434
- {
435
- where: {
436
- channel: "production",
437
- platform: "ios",
438
- enabled: true,
439
- id: {
440
- lt: "bundle-9",
82
+ await expect(response.json()).resolves.toMatchObject({
83
+ catalogId: "project-a",
84
+ });
85
+ expect(getReleaseCatalog).toHaveBeenCalledWith({
86
+ channelKey: "cHJvZHVjdGlvbg",
87
+ fingerprintHash: "fingerprint-123",
88
+ platform: "android",
89
+ strategy: "FINGERPRINT",
90
+ });
91
+
92
+ const legacyAuthorityPath = await handler(
93
+ new Request(url.replace("/android/", "/project-a/android/")),
94
+ );
95
+ expect(legacyAuthorityPath.status).toBe(404);
96
+ expect(getReleaseCatalog).toHaveBeenCalledOnce();
97
+ });
98
+
99
+ it("serves client-relative storage paths under a framework mount", async () => {
100
+ const api = {
101
+ ...createApi(),
102
+ getArtifactInfo: vi.fn().mockResolvedValue({
103
+ changedAssets: {
104
+ "index.bundle": {
105
+ file: {
106
+ url: "/storage/asset-token/asset-signature",
107
+ },
108
+ fileHash: "asset-hash",
109
+ patch: {
110
+ algorithm: "bsdiff",
111
+ baseBundleId: "bundle-1",
112
+ baseFileHash: "base-hash",
113
+ patchFileHash: "patch-hash",
114
+ patchUrl: "/storage/patch-token/patch-signature",
115
+ },
441
116
  },
442
- targetAppVersion: "1.0.x",
443
- targetAppVersionNotNull: true,
444
- fingerprintHash: null,
445
- },
446
- limit: 5,
447
- page: undefined,
448
- },
449
- undefined,
450
- );
451
- });
452
-
453
- it("passes cursor pagination params through to getBundles", async () => {
454
- const api = createApi();
455
- api.getBundles.mockResolvedValue({
456
- data: [testBundle],
457
- pagination: {
458
- total: 51,
459
- hasNextPage: true,
460
- hasPreviousPage: true,
461
- currentPage: 2,
462
- totalPages: 26,
463
- nextCursor: "bundle-1",
464
- previousCursor: "bundle-9",
465
- },
466
- });
467
- const handler = createManagementHandler(api);
468
-
469
- const response = await handler(
470
- new Request(
471
- "http://localhost/hot-updater/api/bundles?channel=production&limit=20&after=bundle-20",
472
- ),
473
- );
474
-
475
- expect(response.status).toBe(200);
476
- expect(api.getBundles).toHaveBeenCalledWith(
477
- {
478
- where: {
479
- channel: "production",
480
117
  },
481
- limit: 20,
482
- page: undefined,
483
- cursor: {
484
- after: "bundle-20",
485
- before: undefined,
486
- },
487
- },
118
+ fileHash: "archive-hash",
119
+ fileUrl: "/storage/archive-token/archive-signature",
120
+ manifestFileHash: "manifest-hash",
121
+ manifestUrl: "/storage/manifest-token/manifest-signature",
122
+ }),
123
+ };
124
+ const handlers = createHotUpdaterHandlers(
125
+ api,
488
126
  undefined,
489
- );
490
- });
491
-
492
- it("supports cursor pagination without a legacy offset query param", async () => {
493
- const api = createApi();
494
- api.getBundles.mockResolvedValue({
495
- data: [testBundle],
496
- pagination: {
497
- total: 1,
498
- hasNextPage: false,
499
- hasPreviousPage: false,
500
- currentPage: 1,
501
- totalPages: 1,
502
- nextCursor: null,
503
- previousCursor: null,
504
- },
505
- });
506
- const handler = createManagementHandler(api);
507
-
508
- const response = await handler(
509
- new Request(
510
- "http://localhost/hot-updater/api/bundles?channel=production&limit=20&after=bundle-20",
511
- ),
512
- );
513
-
514
- expect(response.status).toBe(200);
515
- expect(api.getBundles).toHaveBeenCalledWith(
516
- {
517
- where: {
518
- channel: "production",
519
- },
520
- limit: 20,
521
- page: undefined,
522
- cursor: {
523
- after: "bundle-20",
524
- before: undefined,
525
- },
526
- },
527
127
  undefined,
128
+ async () => new Response("bundle archive"),
528
129
  );
529
- });
130
+ const app = new Hono();
131
+ app.mount("/hot-updater", handlers.client);
530
132
 
531
- it("returns 400 when bundle list requests still send offset pagination", async () => {
532
- const api = createApi();
533
- const handler = createManagementHandler(api);
534
-
535
- const response = await handler(
536
- new Request(
537
- "http://localhost/hot-updater/api/bundles?limit=20&offset=40",
538
- ),
539
- );
540
-
541
- expect(response.status).toBe(400);
542
- await expect(response.json()).resolves.toEqual({
543
- error:
544
- "The 'offset' query parameter has been removed. Use 'after' or 'before' cursor pagination instead.",
545
- });
546
- expect(api.getBundles).not.toHaveBeenCalled();
547
- });
548
-
549
- it("passes page-aligned pagination params through to getBundles", async () => {
550
- const api = createApi();
551
- api.getBundles.mockResolvedValue({
552
- data: [testBundle],
553
- pagination: {
554
- total: 121,
555
- hasNextPage: true,
556
- hasPreviousPage: true,
557
- currentPage: 2,
558
- totalPages: 7,
559
- nextCursor: "bundle-1",
560
- previousCursor: "bundle-9",
561
- },
562
- });
563
- const handler = createManagementHandler(api);
564
-
565
- const response = await handler(
566
- new Request(
567
- "http://localhost/hot-updater/api/bundles?channel=production&limit=20&page=2&after=bundle-20",
568
- ),
133
+ const response = await app.request(
134
+ "/hot-updater/artifacts/bundle-2/from/bundle-1",
569
135
  );
570
136
 
571
137
  expect(response.status).toBe(200);
572
- expect(api.getBundles).toHaveBeenCalledWith(
573
- {
574
- where: {
575
- channel: "production",
576
- },
577
- limit: 20,
578
- page: 2,
579
- cursor: {
580
- after: "bundle-20",
581
- before: undefined,
138
+ const info = (await response.json()) as {
139
+ changedAssets: Record<
140
+ string,
141
+ { file: { url: string }; patch: { patchUrl: string } }
142
+ >;
143
+ fileUrl: string;
144
+ manifestUrl: string;
145
+ };
146
+ expect(info).toMatchObject({
147
+ changedAssets: {
148
+ "index.bundle": {
149
+ file: { url: "/storage/asset-token/asset-signature" },
150
+ patch: { patchUrl: "/storage/patch-token/patch-signature" },
582
151
  },
583
152
  },
584
- undefined,
585
- );
586
- });
587
-
588
- it("returns 400 when bundle list requests send an invalid page", async () => {
589
- const api = createApi();
590
- const handler = createManagementHandler(api);
591
-
592
- const response = await handler(
593
- new Request("http://localhost/hot-updater/api/bundles?limit=20&page=0"),
594
- );
595
-
596
- expect(response.status).toBe(400);
597
- await expect(response.json()).resolves.toEqual({
598
- error: "The 'page' query parameter must be a positive integer.",
153
+ fileUrl: "/storage/archive-token/archive-signature",
154
+ manifestUrl: "/storage/manifest-token/manifest-signature",
599
155
  });
600
- expect(api.getBundles).not.toHaveBeenCalled();
601
- });
602
-
603
- it("returns 400 when bundle list limit exceeds the maximum", async () => {
604
- const api = createApi();
605
- const handler = createManagementHandler(api);
606
156
 
607
- const response = await handler(
608
- new Request("http://localhost/hot-updater/api/bundles?limit=101"),
609
- );
610
-
611
- expect(response.status).toBe(400);
612
- await expect(response.json()).resolves.toEqual({
613
- error:
614
- "The 'limit' query parameter must be a positive integer between 1 and 100.",
615
- });
616
- expect(api.getBundles).not.toHaveBeenCalled();
157
+ const download = await app.request(`/hot-updater${info.fileUrl}`);
158
+ expect(download.status).toBe(200);
159
+ await expect(download.text()).resolves.toBe("bundle archive");
617
160
  });
618
161
 
619
- it("returns 400 when the platform route parameter is invalid", async () => {
620
- const api = createApi();
621
- const handler = createHandler(api, { basePath: "/hot-updater" });
162
+ it("does not expose provider errors from the public client handler", async () => {
163
+ const api = {
164
+ ...createApi(),
165
+ getReleaseCatalog: vi
166
+ .fn()
167
+ .mockRejectedValue(new Error("private database connection details")),
168
+ };
169
+ const consoleError = vi
170
+ .spyOn(console, "error")
171
+ .mockImplementation(() => {});
172
+ const handler = createHandlers(api).client;
622
173
 
623
174
  const response = await handler(
624
175
  new Request(
625
- "http://localhost/hot-updater/app-version/web/1.0.0/production/default/default",
176
+ "http://localhost/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0",
626
177
  ),
627
178
  );
628
179
 
180
+ expect(response.status).toBe(500);
629
181
  await expect(response.json()).resolves.toEqual({
630
- error: "Invalid platform: web. Expected 'ios' or 'android'.",
182
+ error: "Internal server error",
631
183
  });
632
- expect(response.status).toBe(400);
633
- expect(api.getAppUpdateInfo).not.toHaveBeenCalled();
184
+ expect(consoleError).toHaveBeenCalledWith(
185
+ "Hot Updater handler error:",
186
+ expect.any(Error),
187
+ );
188
+ consoleError.mockRestore();
634
189
  });
635
190
  });