@hot-updater/server 0.36.7 → 1.0.0-rc.1

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/apiKeys.cjs +139 -0
  44. package/dist/apiKeys.d.cts +50 -0
  45. package/dist/apiKeys.d.mts +50 -0
  46. package/dist/apiKeys.mjs +133 -0
  47. package/dist/createHotUpdaterCore.cjs +48 -26
  48. package/dist/createHotUpdaterCore.d.cts +49 -17
  49. package/dist/createHotUpdaterCore.d.mts +49 -17
  50. package/dist/createHotUpdaterCore.mjs +51 -29
  51. package/dist/db/createBundleDiff.cjs +45 -68
  52. package/dist/db/createBundleDiff.d.cts +3 -3
  53. package/dist/db/createBundleDiff.d.mts +3 -3
  54. package/dist/db/createBundleDiff.mjs +47 -68
  55. package/dist/db/databasePluginCore.cjs +138 -0
  56. package/dist/db/databasePluginCore.mjs +137 -0
  57. package/dist/db/fixedMigrator.cjs +36 -93
  58. package/dist/db/fixedMigrator.mjs +36 -93
  59. package/dist/db/fixedMigratorMongo.cjs +319 -0
  60. package/dist/db/fixedMigratorMongo.mjs +319 -0
  61. package/dist/db/fixedMigratorShared.cjs +23 -0
  62. package/dist/db/fixedMigratorShared.mjs +18 -0
  63. package/dist/db/mongoMigrationExecution.cjs +11 -0
  64. package/dist/db/mongoMigrationExecution.mjs +11 -0
  65. package/dist/db/releaseCatalog.cjs +66 -0
  66. package/dist/db/releaseCatalog.d.cts +17 -0
  67. package/dist/db/releaseCatalog.d.mts +17 -0
  68. package/dist/db/releaseCatalog.mjs +64 -0
  69. package/dist/db/schema/mongodb.cjs +14 -6
  70. package/dist/db/schema/mongodb.mjs +14 -6
  71. package/dist/db/schema/registry.cjs +0 -2
  72. package/dist/db/schema/registry.mjs +1 -2
  73. package/dist/db/schema/sql.cjs +31 -14
  74. package/dist/db/schema/sql.mjs +32 -13
  75. package/dist/db/schema/sqlOperations.cjs +4 -4
  76. package/dist/db/schema/sqlOperations.mjs +5 -5
  77. package/dist/db/schemaGenerators.cjs +52 -18
  78. package/dist/db/schemaGenerators.mjs +52 -18
  79. package/dist/db/schemaReadiness.cjs +4 -3
  80. package/dist/db/schemaReadiness.mjs +4 -3
  81. package/dist/db/sqlMigrationExecution.cjs +67 -0
  82. package/dist/db/sqlMigrationExecution.mjs +66 -0
  83. package/dist/db/types.cjs +2 -6
  84. package/dist/db/types.d.cts +60 -16
  85. package/dist/db/types.d.mts +60 -16
  86. package/dist/db/types.mjs +3 -6
  87. package/dist/db/updateArtifacts.cjs +114 -60
  88. package/dist/db/updateArtifacts.mjs +117 -62
  89. package/dist/handler.cjs +108 -282
  90. package/dist/handler.d.cts +3 -46
  91. package/dist/handler.d.mts +3 -46
  92. package/dist/handler.mjs +107 -281
  93. package/dist/handlerBundleRoutes.cjs +155 -0
  94. package/dist/handlerBundleRoutes.mjs +155 -0
  95. package/dist/handlerErrors.cjs +9 -0
  96. package/dist/handlerErrors.mjs +9 -0
  97. package/dist/handlerParameters.cjs +39 -0
  98. package/dist/handlerParameters.mjs +34 -0
  99. package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
  100. package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
  101. package/dist/handlerReleaseManagementRoutes.cjs +125 -0
  102. package/dist/handlerReleaseManagementRoutes.mjs +124 -0
  103. package/dist/handlerTypes.d.cts +63 -0
  104. package/dist/handlerTypes.d.mts +63 -0
  105. package/dist/handlerVersionRoutes.cjs +10 -0
  106. package/dist/handlerVersionRoutes.d.cts +4 -0
  107. package/dist/handlerVersionRoutes.d.mts +4 -0
  108. package/dist/handlerVersionRoutes.mjs +9 -0
  109. package/dist/index.cjs +10 -1
  110. package/dist/index.d.cts +9 -3
  111. package/dist/index.d.mts +9 -3
  112. package/dist/index.mjs +5 -2
  113. package/dist/insights/bounded/activeOverview.cjs +87 -0
  114. package/dist/insights/bounded/activeOverview.mjs +85 -0
  115. package/dist/insights/bounded/installationSearch.cjs +42 -0
  116. package/dist/insights/bounded/installationSearch.mjs +42 -0
  117. package/dist/insights/bounded/persistence.cjs +39 -0
  118. package/dist/insights/bounded/persistence.mjs +38 -0
  119. package/dist/insights/bounded/provider.cjs +170 -0
  120. package/dist/insights/bounded/provider.d.cts +7 -0
  121. package/dist/insights/bounded/provider.d.mts +7 -0
  122. package/dist/insights/bounded/provider.mjs +170 -0
  123. package/dist/insights/bounded/scan.cjs +144 -0
  124. package/dist/insights/bounded/scan.mjs +138 -0
  125. package/dist/insights/domain.d.cts +121 -0
  126. package/dist/insights/domain.d.mts +121 -0
  127. package/dist/insights/errors.cjs +22 -0
  128. package/dist/insights/errors.mjs +20 -0
  129. package/dist/insights/eventInput.cjs +111 -0
  130. package/dist/insights/eventInput.mjs +111 -0
  131. package/dist/insights/persistence.d.cts +6 -0
  132. package/dist/insights/persistence.d.mts +6 -0
  133. package/dist/insights/queryInput.cjs +58 -0
  134. package/dist/insights/queryInput.mjs +55 -0
  135. package/dist/insights/routes.cjs +63 -0
  136. package/dist/insights/routes.mjs +61 -0
  137. package/dist/insights/types.d.cts +17 -0
  138. package/dist/insights/types.d.mts +17 -0
  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/apiKeys.integration.spec.ts +192 -0
  188. package/src/apiKeys.spec.ts +162 -0
  189. package/src/apiKeys.ts +220 -0
  190. package/src/createHotUpdaterCore.ts +164 -100
  191. package/src/db/bundleRows.spec.ts +164 -0
  192. package/src/db/bundleRows.ts +10 -140
  193. package/src/db/createBundleDiff.spec.ts +172 -127
  194. package/src/db/createBundleDiff.ts +91 -110
  195. package/src/db/databasePluginCore.spec.ts +62 -0
  196. package/src/db/databasePluginCore.testFixtures.ts +76 -0
  197. package/src/db/databasePluginCore.ts +185 -0
  198. package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
  199. package/src/db/fixedMigrator.spec.ts +184 -42
  200. package/src/db/fixedMigrator.ts +64 -205
  201. package/src/db/fixedMigratorMongo.spec.ts +192 -0
  202. package/src/db/fixedMigratorMongo.ts +425 -0
  203. package/src/db/fixedMigratorShared.ts +36 -0
  204. package/src/db/hotUpdaterSchema.ts +0 -1
  205. package/src/db/index.spec.ts +333 -1025
  206. package/src/db/mongoMigrationExecution.ts +23 -0
  207. package/src/db/node-sqlite.d.ts +25 -0
  208. package/src/db/releaseCatalog.ts +148 -0
  209. package/src/db/schema/mongodb.ts +20 -6
  210. package/src/db/schema/sql.ts +52 -11
  211. package/src/db/schema/sqlOperations.ts +5 -4
  212. package/src/db/schemaGenerators.ts +107 -23
  213. package/src/db/schemaReadiness.ts +2 -1
  214. package/src/db/sqlMigrationExecution.spec.ts +65 -0
  215. package/src/db/sqlMigrationExecution.ts +108 -0
  216. package/src/db/types.spec.ts +55 -0
  217. package/src/db/types.ts +129 -65
  218. package/src/db/updateArtifacts.spec.ts +359 -0
  219. package/src/db/updateArtifacts.ts +290 -126
  220. package/src/handler-standalone.integration.spec.ts +401 -364
  221. package/src/handler.spec.ts +137 -582
  222. package/src/handler.testFixtures.ts +30 -0
  223. package/src/handler.ts +218 -616
  224. package/src/handlerAuth.integration.spec.ts +33 -0
  225. package/src/handlerBundleRoutes.ts +259 -0
  226. package/src/handlerErrors.ts +7 -0
  227. package/src/handlerManagement.spec.ts +262 -0
  228. package/src/handlerParameters.ts +105 -0
  229. package/src/handlerReleaseCatalogRoutes.ts +154 -0
  230. package/src/handlerReleaseManagementRoutes.ts +213 -0
  231. package/src/handlerTypes.ts +96 -0
  232. package/src/handlerVersionRoutes.ts +12 -0
  233. package/src/index.ts +22 -2
  234. package/src/insights/bounded/activeOverview.spec.ts +189 -0
  235. package/src/insights/bounded/activeOverview.ts +148 -0
  236. package/src/insights/bounded/installationSearch.spec.ts +130 -0
  237. package/src/insights/bounded/installationSearch.ts +69 -0
  238. package/src/insights/bounded/persistence.ts +43 -0
  239. package/src/insights/bounded/provider.spec.ts +219 -0
  240. package/src/insights/bounded/provider.ts +229 -0
  241. package/src/insights/bounded/scan.ts +235 -0
  242. package/src/insights/domain.ts +139 -0
  243. package/src/insights/errors.ts +18 -0
  244. package/src/insights/eventInput.ts +168 -0
  245. package/src/insights/insights.integration.spec.ts +144 -0
  246. package/src/insights/persistence.ts +10 -0
  247. package/src/insights/queryInput.ts +102 -0
  248. package/src/insights/routes.ts +123 -0
  249. package/src/insights/types.ts +44 -0
  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
package/src/node.ts CHANGED
@@ -14,16 +14,13 @@ interface NodeRequest {
14
14
  interface NodeResponse {
15
15
  status(code: number): NodeResponse;
16
16
  setHeader(name: string, value: string | string[]): void;
17
- send(body: string): void;
17
+ send(body: string | Uint8Array): void;
18
18
  end(): void;
19
19
  [key: string]: unknown;
20
20
  }
21
21
 
22
- type HandlerHotUpdaterAPI = {
23
- readonly handler: (request: Request) => Promise<Response>;
24
- };
25
-
26
22
  export { HOT_UPDATER_SERVER_VERSION } from "./version";
23
+ export { HOT_UPDATER_INFRASTRUCTURE_GENERATION } from "./handlerVersionRoutes";
27
24
 
28
25
  /**
29
26
  * Converts a Hot Updater handler to a Node.js-compatible middleware
@@ -36,22 +33,28 @@ export { HOT_UPDATER_SERVER_VERSION } from "./version";
36
33
  *
37
34
  * const app = express();
38
35
  *
39
- * // Mount middleware
40
- * app.use(express.json());
41
- *
42
- * // Mount hot-updater handler
43
- * app.all("/hot-updater/*", toNodeHandler(hotUpdater));
36
+ * app.use(
37
+ * "/hot-updater/admin",
38
+ * bearerMiddleware,
39
+ * express.json({ limit: "1mb" }),
40
+ * toNodeHandler(hotUpdater.handlers.admin),
41
+ * );
42
+ * app.use(
43
+ * "/hot-updater",
44
+ * express.json({ limit: "1mb" }),
45
+ * toNodeHandler(hotUpdater.handlers.client),
46
+ * );
44
47
  * ```
45
48
  */
46
49
  export function toNodeHandler(
47
- hotUpdater: HandlerHotUpdaterAPI,
50
+ handler: HotUpdaterHandler,
48
51
  ): (req: any, res: any, next?: any) => Promise<void> {
49
52
  return async (req: NodeRequest, res: NodeResponse) => {
50
53
  try {
51
54
  // Build full URL
52
55
  const protocol = req.protocol || "http";
53
56
  const host = req.get?.("host") || "localhost";
54
- const url = `${protocol}://${host}${req.url || "/"}`;
57
+ const url = `${protocol}://${host}${req.url ?? "/"}`;
55
58
 
56
59
  // Convert headers to Web Headers
57
60
  const headers = new Headers();
@@ -80,8 +83,7 @@ export function toNodeHandler(
80
83
  body,
81
84
  });
82
85
 
83
- // Call hot-updater handler
84
- const response = await hotUpdater.handler(webRequest);
86
+ const response = await handler(webRequest);
85
87
 
86
88
  // Set status code
87
89
  res.status(response.status);
@@ -92,9 +94,9 @@ export function toNodeHandler(
92
94
  });
93
95
 
94
96
  // Send response body
95
- const text = await response.text();
96
- if (text) {
97
- res.send(text);
97
+ const responseBody = Buffer.from(await response.arrayBuffer());
98
+ if (responseBody.length > 0) {
99
+ res.send(responseBody);
98
100
  } else {
99
101
  res.end();
100
102
  }
@@ -106,3 +108,4 @@ export function toNodeHandler(
106
108
  }
107
109
  };
108
110
  }
111
+ import type { HotUpdaterHandler } from "./handlerTypes";
@@ -0,0 +1,238 @@
1
+ import { encodeChannelKey } from "@hot-updater/core";
2
+ import { commitReleaseCatalogMutation } from "@hot-updater/plugin-core";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { createInMemoryDatabasePlugin } from "../../test-utils/test/inMemoryDatabasePlugin";
6
+ import { registerApiKey } from "./apiKeys";
7
+ import { createHotUpdater } from "./index";
8
+
9
+ const API_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
10
+
11
+ const channelKey = encodeChannelKey("production");
12
+ const scopeKey = `v1:app-version:ios:${channelKey}`;
13
+
14
+ const createCatalogDatabase = async () => {
15
+ const database = createInMemoryDatabasePlugin();
16
+ const channel = { id: "channel-production", name: "production" };
17
+ await database.models.channels.insert({
18
+ onConflict: "returnExisting",
19
+ row: channel,
20
+ });
21
+ const bundleId = "00000000-0000-7001-8000-000000000001";
22
+ await database.commit({
23
+ changes: [
24
+ {
25
+ model: "bundles",
26
+ operation: "insert",
27
+ row: {
28
+ asset_base_storage_uri: null,
29
+ archive_byte_size: 3_000_000_001,
30
+ file_hash: "bundle-hash",
31
+ git_commit_hash: null,
32
+ id: bundleId,
33
+ manifest_file_hash: null,
34
+ manifest_storage_uri: null,
35
+ metadata: {},
36
+ platform: "ios",
37
+ storage_uri: "storage://bundle.zip",
38
+ },
39
+ },
40
+ ],
41
+ });
42
+ await commitReleaseCatalogMutation({
43
+ database,
44
+ mutation: {
45
+ operation: "insert",
46
+ row: {
47
+ bundle_id: bundleId,
48
+ channel_id: channel.id,
49
+ created_at_ms: 1,
50
+ enabled: true,
51
+ fingerprint_hash: null,
52
+ id: "00000000-0000-7000-8000-000000000001",
53
+ kind: "BUNDLE",
54
+ message: "Stable Release",
55
+ operation: "DEPLOY",
56
+ platform: "ios",
57
+ revision: 1,
58
+ rollout_cohort_count: 1000,
59
+ scope_key: scopeKey,
60
+ should_force_update: false,
61
+ source_release_id: null,
62
+ strategy: "APP_VERSION",
63
+ target_app_version: ">=1.0.0 <2.0.0",
64
+ target_cohorts: [],
65
+ updated_at_ms: 1,
66
+ },
67
+ },
68
+ scope: {
69
+ channelId: channel.id,
70
+ channelName: channel.name,
71
+ fingerprintHash: null,
72
+ platform: "ios",
73
+ scopeKey,
74
+ strategy: "APP_VERSION",
75
+ },
76
+ updatedAtMs: 1,
77
+ });
78
+ return database;
79
+ };
80
+
81
+ describe("Release catalog routes", () => {
82
+ it("serves persisted Catalog identity without configuration and keeps it across server restarts", async () => {
83
+ const database = await createCatalogDatabase();
84
+ const storedCatalog =
85
+ await database.models.releaseCatalogs.findByScopeKey(scopeKey);
86
+ const hotUpdater = createHotUpdater({
87
+ database,
88
+ clientAccess: { type: "public" },
89
+ });
90
+ const catalogRead = vi.spyOn(
91
+ database.models.releaseCatalogs,
92
+ "findByScopeKey",
93
+ );
94
+ const url =
95
+ `https://updates.example.com/release-catalogs/app-version/` +
96
+ `ios/${channelKey}/1.5.0`;
97
+
98
+ const response = await hotUpdater.handlers.client(new Request(url));
99
+ expect(response.status).toBe(200);
100
+ expect(response.headers.get("cache-control")).toBe(
101
+ "public, max-age=0, s-maxage=5",
102
+ );
103
+ expect(response.headers.get("content-type")).toBe(
104
+ "application/vnd.hot-updater.release-catalog+json; version=1",
105
+ );
106
+ const body = await response.json();
107
+ expect(body).toMatchObject({
108
+ catalogId: storedCatalog!.catalog_id,
109
+ fallbackPolicy: "BUILTIN_IF_ACTIVE_INELIGIBLE",
110
+ generation: 1,
111
+ releases: [
112
+ {
113
+ bundleId: "00000000-0000-7001-8000-000000000001",
114
+ kind: "BUNDLE",
115
+ message: "Stable Release",
116
+ shouldForceUpdate: false,
117
+ },
118
+ ],
119
+ rollbackReleases: [
120
+ {
121
+ bundleId: "00000000-0000-7001-8000-000000000001",
122
+ kind: "BUNDLE",
123
+ },
124
+ ],
125
+ scopeKey,
126
+ });
127
+
128
+ const etag = response.headers.get("etag");
129
+ expect(etag).toMatch(/^"sha256:[0-9a-f]{64}"$/);
130
+ const revalidated = await hotUpdater.handlers.client(
131
+ new Request(url, { headers: { "if-none-match": etag! } }),
132
+ );
133
+ expect(revalidated.status).toBe(304);
134
+ expect(await revalidated.text()).toBe("");
135
+ expect(catalogRead).toHaveBeenCalledOnce();
136
+
137
+ const nonCanonicalVersion = await hotUpdater.handlers.client(
138
+ new Request(url.replace("/1.5.0", "/v1.5")),
139
+ );
140
+ expect(nonCanonicalVersion.status).toBe(400);
141
+ expect(nonCanonicalVersion.headers.get("cache-control")).toBe(
142
+ "private, no-store",
143
+ );
144
+ expect(catalogRead).toHaveBeenCalledOnce();
145
+
146
+ const invalidPlatform = await hotUpdater.handlers.client(
147
+ new Request(url.replace("/ios/", "/windows/")),
148
+ );
149
+ expect(invalidPlatform.status).toBe(400);
150
+ expect(catalogRead).toHaveBeenCalledOnce();
151
+
152
+ const otherChannel = await hotUpdater.handlers.client(
153
+ new Request(
154
+ url.replace(`/${channelKey}/`, `/${encodeChannelKey("beta")}/`),
155
+ ),
156
+ );
157
+ expect(otherChannel.status).toBe(404);
158
+ expect(catalogRead).toHaveBeenCalledTimes(2);
159
+
160
+ const legacyAuthorityPath = await hotUpdater.handlers.client(
161
+ new Request(
162
+ `https://updates.example.com/release-catalogs/app-version/` +
163
+ `old-authority/ios/${channelKey}/1.5.0`,
164
+ ),
165
+ );
166
+ expect(legacyAuthorityPath.status).toBe(404);
167
+ expect(legacyAuthorityPath.headers.get("cache-control")).toBe(
168
+ "private, no-store",
169
+ );
170
+ expect(catalogRead).toHaveBeenCalledTimes(2);
171
+ const commit = vi.spyOn(database, "commit");
172
+ const restarted = createHotUpdater({
173
+ database,
174
+ clientAccess: { type: "public" },
175
+ });
176
+ const relocated = await restarted.handlers.client(
177
+ new Request(url.replace("updates.example.com", "new.example.com")),
178
+ );
179
+ expect(await relocated.json()).toEqual(body);
180
+ expect(commit).not.toHaveBeenCalled();
181
+ });
182
+
183
+ it("singleflights concurrent cold requests into one exact catalog read", async () => {
184
+ const database = await createCatalogDatabase();
185
+ const catalogRead = vi.spyOn(
186
+ database.models.releaseCatalogs,
187
+ "findByScopeKey",
188
+ );
189
+ const hotUpdater = createHotUpdater({
190
+ database,
191
+ clientAccess: { type: "public" },
192
+ });
193
+ const url =
194
+ `https://updates.example.com/release-catalogs/app-version/` +
195
+ `ios/${channelKey}/1.5.0`;
196
+
197
+ const responses = await Promise.all(
198
+ Array.from({ length: 100 }, () =>
199
+ hotUpdater.handlers.client(new Request(url)),
200
+ ),
201
+ );
202
+
203
+ expect(responses.every(({ status }) => status === 200)).toBe(true);
204
+ expect(catalogRead).toHaveBeenCalledOnce();
205
+ });
206
+
207
+ it("varies authenticated catalog responses by the configured header", async () => {
208
+ const database = await createCatalogDatabase();
209
+ await registerApiKey({
210
+ apiKeys: database.models.apiKeys,
211
+ apiKey: API_KEY,
212
+ name: "App",
213
+ });
214
+ const hotUpdater = createHotUpdater({
215
+ clientAccess: {
216
+ headerName: "X-Hot-Updater-Key",
217
+ type: "api-key",
218
+ },
219
+ database,
220
+ });
221
+ const url =
222
+ `https://updates.example.com/release-catalogs/app-version/` +
223
+ `ios/${channelKey}/1.5.0`;
224
+
225
+ const unauthorized = await hotUpdater.handlers.client(
226
+ new Request(url, { headers: { "x-api-key": API_KEY } }),
227
+ );
228
+ expect(unauthorized.status).toBe(401);
229
+
230
+ const response = await hotUpdater.handlers.client(
231
+ new Request(url, { headers: { "x-hot-updater-key": API_KEY } }),
232
+ );
233
+ expect(response.status).toBe(200);
234
+ expect(response.headers.get("vary")).toBe(
235
+ "Accept-Encoding, x-hot-updater-key",
236
+ );
237
+ });
238
+ });