@hot-updater/server 0.36.7 → 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,55 +1,109 @@
1
- import type {
2
- DatabasePlugin,
3
- HotUpdaterContext,
4
- RuntimeStoragePlugin,
1
+ import {
2
+ assertStorageOperations,
3
+ type StoragePlugin,
5
4
  } from "@hot-updater/plugin-core";
6
- import { assertRuntimeStoragePlugin } from "@hot-updater/plugin-core";
7
5
 
8
- import { createPluginDatabaseCore } from "./db/pluginCore";
6
+ import { createAnalyticsProvider } from "./analytics/bounded/provider";
7
+ import type { AnalyticsProvider } from "./analytics/types";
8
+ import {
9
+ authenticateApiKey,
10
+ createApiKeyManagement,
11
+ normalizeApiKeyHeaderName,
12
+ } from "./apiKeys";
13
+ import type { ApiKeyManagementAPI } from "./apiKeys";
14
+ import { createDatabasePluginCore } from "./db/databasePluginCore";
9
15
  import { createSchemaReadinessChecker } from "./db/schemaReadiness";
10
16
  import {
11
- type DatabaseAdapter,
12
17
  type DatabaseAdapterCapabilities,
13
18
  type DatabaseAPI,
19
+ type DatabasePlugin,
14
20
  isDatabasePlugin,
15
- isDatabasePluginFactory,
16
- type StoragePluginFactory,
17
21
  } from "./db/types";
18
- import { createHandler, type HandlerRoutes } from "./handler";
19
- import { normalizeBasePath } from "./route";
22
+ import { createHotUpdaterHandlers, type HotUpdaterHandlers } from "./handler";
20
23
  import { createStorageAccess } from "./storageAccess";
21
24
 
22
- export type RuntimeHotUpdaterAPI<TContext = unknown> = DatabaseAPI<TContext> & {
23
- readonly basePath: string;
24
- readonly handler: (
25
- request: Request,
26
- context?: HotUpdaterContext<TContext>,
27
- ) => Promise<Response>;
25
+ export type RuntimeHotUpdaterAPI = DatabaseAPI & {
26
+ readonly handlers: HotUpdaterHandlers;
28
27
  readonly adapterName: string;
28
+ /**
29
+ * Built-in Analytics provider. Client ingestion and admin query routes are
30
+ * always mounted; React Native clients independently opt into lifecycle
31
+ * reporting with `HotUpdater.init({ analytics: true })`.
32
+ */
33
+ readonly analytics: AnalyticsProvider;
34
+ /**
35
+ * In-process API key lifecycle operations for trusted server tooling.
36
+ * Creation returns the plaintext once; list and revoke expose only metadata.
37
+ * This capability is never mounted on the client or admin HTTP handlers.
38
+ */
39
+ readonly apiKeys: ApiKeyManagementAPI;
29
40
  };
30
41
 
31
- export type HotUpdaterAPI<TContext = unknown> = RuntimeHotUpdaterAPI<TContext>;
42
+ export type HotUpdaterAPI = RuntimeHotUpdaterAPI;
32
43
 
33
- export interface CreateHotUpdaterOptions<TContext = unknown> {
34
- readonly database: DatabaseAdapter<TContext>;
35
- readonly storages?: readonly (
36
- | RuntimeStoragePlugin<TContext>
37
- | StoragePluginFactory<TContext>
38
- )[];
44
+ export type ClientAccessPolicy =
45
+ | {
46
+ /**
47
+ * Leaves Release Catalog, artifact, and Analytics ingestion routes
48
+ * publicly accessible without a client credential.
49
+ */
50
+ readonly type: "public";
51
+ }
52
+ | {
53
+ /**
54
+ * Requires a key registered in `database.models.apiKeys` for
55
+ * Release Catalog, artifact, and Analytics ingestion requests.
56
+ */
57
+ readonly type: "api-key";
58
+ /**
59
+ * Request header containing the API key. Defaults to
60
+ * `x-api-key`. Clients must send the same header; Release Catalog
61
+ * responses include it in `Vary` to preserve cache isolation.
62
+ */
63
+ readonly headerName?: string;
64
+ };
65
+
66
+ export interface CreateHotUpdaterOptions {
67
+ readonly database: DatabasePlugin;
39
68
  /**
40
- * @deprecated Use `storages` instead. This field will be removed in a future version.
69
+ * Required client-route access policy. This choice is explicit so a server
70
+ * cannot accidentally change between public and authenticated OTA access.
71
+ * `/version`, storage downloads, and admin-handler routes are unaffected.
41
72
  */
42
- readonly storagePlugins?: readonly (
43
- | RuntimeStoragePlugin<TContext>
44
- | StoragePluginFactory<TContext>
45
- )[];
46
- readonly basePath?: string;
47
- readonly cwd?: string;
48
- readonly routes?: HandlerRoutes;
73
+ readonly clientAccess: ClientAccessPolicy;
74
+ /** Storage implementations used to read provider-specific storage URIs. */
75
+ readonly storage?: readonly StoragePlugin[];
49
76
  }
50
77
 
51
- type PluginDatabaseCore<TContext> = {
52
- readonly api: DatabaseAPI<TContext>;
78
+ const normalizeClientAccess = (
79
+ value: unknown,
80
+ ):
81
+ | { readonly type: "public" }
82
+ | {
83
+ readonly type: "api-key";
84
+ readonly headerName: string;
85
+ } => {
86
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
87
+ throw new TypeError("clientAccess must be an object.");
88
+ }
89
+ const policy = value as {
90
+ readonly headerName?: unknown;
91
+ readonly type?: unknown;
92
+ };
93
+ if (policy.type === "public") return { type: "public" };
94
+ if (policy.type === "api-key") {
95
+ return {
96
+ headerName: normalizeApiKeyHeaderName(policy.headerName),
97
+ type: "api-key",
98
+ };
99
+ }
100
+ throw new TypeError(
101
+ 'clientAccess.type must be either "public" or "api-key".',
102
+ );
103
+ };
104
+
105
+ type DatabasePluginCore = {
106
+ readonly api: DatabaseAPI;
53
107
  readonly adapterName: string;
54
108
  readonly createMigrator: () => never;
55
109
  readonly generateSchema: () => never;
@@ -59,110 +113,120 @@ export const hotUpdaterCoreMetadata = Symbol.for(
59
113
  "@hot-updater/server/core-metadata",
60
114
  );
61
115
 
62
- export type HotUpdaterCoreMetadata<TContext = unknown> = {
116
+ export type HotUpdaterCoreMetadata = {
63
117
  readonly adapterCapabilities: DatabaseAdapterCapabilities;
64
- readonly core: PluginDatabaseCore<TContext>;
118
+ readonly core: DatabasePluginCore;
65
119
  };
66
120
 
67
- export type HotUpdaterCore<TContext = unknown> = {
68
- readonly api: RuntimeHotUpdaterAPI<TContext>;
121
+ export type HotUpdaterCore = {
122
+ readonly api: RuntimeHotUpdaterAPI;
69
123
  readonly adapterCapabilities: DatabaseAdapterCapabilities;
70
- readonly core: PluginDatabaseCore<TContext>;
124
+ readonly core: DatabasePluginCore;
71
125
  };
72
126
 
73
- export function getHotUpdaterCoreMetadata<TContext = unknown>(
74
- hotUpdater: RuntimeHotUpdaterAPI<TContext>,
75
- ): HotUpdaterCoreMetadata<TContext> | undefined {
127
+ export function getHotUpdaterCoreMetadata(
128
+ hotUpdater: RuntimeHotUpdaterAPI,
129
+ ): HotUpdaterCoreMetadata | undefined {
76
130
  return (
77
- hotUpdater as RuntimeHotUpdaterAPI<TContext> & {
78
- readonly [hotUpdaterCoreMetadata]?: HotUpdaterCoreMetadata<TContext>;
131
+ hotUpdater as RuntimeHotUpdaterAPI & {
132
+ readonly [hotUpdaterCoreMetadata]?: HotUpdaterCoreMetadata;
79
133
  }
80
134
  )[hotUpdaterCoreMetadata];
81
135
  }
82
136
 
83
- export function createHotUpdaterCore<TContext = unknown>(
84
- options: CreateHotUpdaterOptions<TContext>,
85
- ): HotUpdaterCore<TContext> {
137
+ export function createHotUpdaterCore(
138
+ options: CreateHotUpdaterOptions,
139
+ ): HotUpdaterCore {
140
+ for (const key of ["authorityId", "catalogId"]) {
141
+ if (Object.hasOwn(options, key)) {
142
+ throw new TypeError(
143
+ `Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`,
144
+ );
145
+ }
146
+ }
86
147
  const database = options.database;
87
- const basePath = normalizeBasePath(options.basePath ?? "/api");
88
- const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map(
89
- (plugin) => {
90
- const storagePlugin = typeof plugin === "function" ? plugin() : plugin;
91
- assertRuntimeStoragePlugin(storagePlugin);
92
- return storagePlugin;
93
- },
94
- );
95
- const { readStorageText, resolveFileUrl } =
148
+ const storagePlugins = (options.storage ?? []).map((storage) => {
149
+ assertStorageOperations(storage, ["get", "getDownloadUrl"]);
150
+ return storage;
151
+ });
152
+ const { downloadStorageObject, readStorageText, resolveFileUrl } =
96
153
  createStorageAccess(storagePlugins);
154
+ const adapterCapabilities: DatabaseAdapterCapabilities = database;
97
155
 
98
- if (!isDatabasePluginFactory(database) && !isDatabasePlugin(database)) {
156
+ if (!isDatabasePlugin(database)) {
99
157
  throw new Error("@hot-updater/server only supports database plugins.");
100
158
  }
101
159
 
102
- const adapterCapabilities = database as DatabaseAdapterCapabilities;
103
- const plugin: DatabasePlugin<TContext> = isDatabasePluginFactory(database)
104
- ? database()
105
- : database;
160
+ const plugin: DatabasePlugin = database;
106
161
  const adapterName = adapterCapabilities.adapterName ?? plugin.name;
107
162
  const assertSchemaReady = createSchemaReadinessChecker(
108
163
  adapterName,
109
164
  adapterCapabilities.createMigrator,
110
165
  );
111
- const core = createPluginDatabaseCore<TContext>(
112
- () => plugin,
113
- resolveFileUrl,
114
- isDatabasePluginFactory(database)
166
+ const core = createDatabasePluginCore(plugin, resolveFileUrl, {
167
+ beforeOperation: assertSchemaReady,
168
+ readStorageText,
169
+ });
170
+ const clientAccess = normalizeClientAccess(options.clientAccess);
171
+ const analytics = createAnalyticsProvider({
172
+ async append(row) {
173
+ await assertSchemaReady();
174
+ return plugin.models.analytics.append(row);
175
+ },
176
+ async scan(input) {
177
+ await assertSchemaReady();
178
+ return plugin.models.analytics.scan(input);
179
+ },
180
+ });
181
+ const apiKeys = createApiKeyManagement({
182
+ apiKeys: plugin.models.apiKeys,
183
+ beforeOperation: assertSchemaReady,
184
+ });
185
+
186
+ const handlers = createHotUpdaterHandlers(
187
+ core.api,
188
+ analytics,
189
+ clientAccess.type === "api-key"
115
190
  ? {
116
- createMutationPlugin: () => database(),
117
- beforeOperation: assertSchemaReady,
118
- readStorageText,
191
+ authenticate: (request) =>
192
+ authenticateApiKey({
193
+ apiKeys: plugin.models.apiKeys,
194
+ beforeLookup: assertSchemaReady,
195
+ headerName: clientAccess.headerName,
196
+ request,
197
+ }),
198
+ headerName: clientAccess.headerName,
119
199
  }
120
- : { beforeOperation: assertSchemaReady, readStorageText },
200
+ : undefined,
201
+ downloadStorageObject,
121
202
  );
122
203
 
123
- const internalHandler = createHandler(core.api, {
124
- basePath,
125
- routes: options.routes,
126
- });
127
-
128
- // Some framework adapters strip the mounted base path or pass extra
129
- // bindings/execution context arguments. Ignore those extras here so the
130
- // handler can still be mounted directly as a plain Request handler.
131
- const handler: RuntimeHotUpdaterAPI<TContext>["handler"] = (
132
- request,
133
- context,
134
- ...extraArgs: unknown[]
135
- ) => {
136
- if (extraArgs.length > 0) {
137
- return internalHandler(request);
138
- }
139
-
140
- return internalHandler(request, context);
141
- };
142
-
143
- const api = {
144
- basePath,
145
- adapterName: adapterCapabilities.adapterName ?? core.adapterName,
146
- handler,
147
- };
148
- Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
204
+ const api: RuntimeHotUpdaterAPI = Object.assign(
205
+ {
206
+ adapterName: adapterCapabilities.adapterName ?? core.adapterName,
207
+ analytics,
208
+ apiKeys,
209
+ handlers,
210
+ },
211
+ core.api,
212
+ );
149
213
  Object.defineProperty(api, hotUpdaterCoreMetadata, {
150
214
  enumerable: false,
151
215
  value: {
152
216
  adapterCapabilities,
153
217
  core,
154
- } satisfies HotUpdaterCoreMetadata<TContext>,
218
+ } satisfies HotUpdaterCoreMetadata,
155
219
  });
156
220
 
157
221
  return {
158
- api: api as RuntimeHotUpdaterAPI<TContext>,
222
+ api,
159
223
  adapterCapabilities,
160
224
  core,
161
225
  };
162
226
  }
163
227
 
164
- export function createHotUpdater<TContext = unknown>(
165
- options: CreateHotUpdaterOptions<TContext>,
166
- ): RuntimeHotUpdaterAPI<TContext> {
228
+ export function createHotUpdater(
229
+ options: CreateHotUpdaterOptions,
230
+ ): RuntimeHotUpdaterAPI {
167
231
  return createHotUpdaterCore(options).api;
168
232
  }
@@ -0,0 +1,164 @@
1
+ import type { Bundle } from "@hot-updater/core";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ BundleRowHydrationError,
6
+ bundleToPatchRows,
7
+ bundleToRow,
8
+ rowsToBundles,
9
+ type BundlePatchRow,
10
+ type BundleRow,
11
+ } from "./bundleRows";
12
+
13
+ const createBundle = (id: string): Bundle => ({
14
+ id,
15
+ platform: "ios",
16
+ fileHash: `hash-${id}`,
17
+ gitCommitHash: null,
18
+ storageUri: `s3://bucket/${id}.zip`,
19
+ archiveByteSize: 3_000_000_001,
20
+ });
21
+
22
+ const toRow = (bundle: Bundle): BundleRow => bundleToRow(bundle);
23
+
24
+ const createPatchRow = (
25
+ id: string,
26
+ bundleId: string,
27
+ baseBundleId: string,
28
+ orderIndex: number,
29
+ ): BundlePatchRow => ({
30
+ id,
31
+ bundle_id: bundleId,
32
+ base_bundle_id: baseBundleId,
33
+ base_file_hash: `base-hash-${id}`,
34
+ patch_file_hash: `patch-hash-${id}`,
35
+ patch_storage_uri: `s3://bucket/${id}.patch`,
36
+ byte_size: 3_000_000_002,
37
+ order_index: orderIndex,
38
+ });
39
+
40
+ describe("bundle row conversion", () => {
41
+ it("round-trips bundle fields without embedding patch artifacts in the bundle row", () => {
42
+ const baseBundle = createBundle("base");
43
+ const bundle: Bundle = {
44
+ ...createBundle("target"),
45
+ metadata: { app_version: "1.0.0" },
46
+ manifestStorageUri: "s3://bucket/manifest.json",
47
+ manifestFileHash: "manifest-hash",
48
+ assetBaseStorageUri: "s3://bucket/assets",
49
+ patches: [
50
+ {
51
+ baseBundleId: baseBundle.id,
52
+ baseFileHash: "base-hash",
53
+ byteSize: 3_000_000_002,
54
+ patchFileHash: "patch-hash",
55
+ patchStorageUri: "s3://bucket/target.patch",
56
+ },
57
+ ],
58
+ };
59
+
60
+ const row = toRow(bundle);
61
+ const patchRows = bundleToPatchRows(bundle);
62
+ const hydratedBundles = rowsToBundles([row], patchRows, [
63
+ toRow(baseBundle),
64
+ ]);
65
+
66
+ expect(row).not.toHaveProperty("patches");
67
+ expect(row).not.toHaveProperty("patch_file_hash");
68
+ expect(row).not.toHaveProperty("channel");
69
+ expect(row).not.toHaveProperty("enabled");
70
+ expect(hydratedBundles).toHaveLength(1);
71
+ expect(hydratedBundles[0]).toEqual(bundle);
72
+ });
73
+
74
+ it("orders multiple patches deterministically", () => {
75
+ const oldest = createBundle("oldest");
76
+ const newest = createBundle("newest");
77
+ const target = createBundle("target");
78
+ const rows: readonly BundleRow[] = [toRow(target)];
79
+ const later = createPatchRow("later", target.id, newest.id, 1);
80
+ const firstById = createPatchRow("a-first", target.id, oldest.id, 0);
81
+ const secondById = createPatchRow("z-second", target.id, newest.id, 0);
82
+
83
+ const [hydrated] = rowsToBundles(
84
+ rows,
85
+ [later, secondById, firstById],
86
+ [toRow(oldest), toRow(newest)],
87
+ );
88
+
89
+ expect(hydrated?.patches?.map((patch) => patch.patchFileHash)).toEqual([
90
+ firstById.patch_file_hash,
91
+ secondById.patch_file_hash,
92
+ later.patch_file_hash,
93
+ ]);
94
+ });
95
+
96
+ it("hydrates an empty patch set", () => {
97
+ const bundle = createBundle("without-patches");
98
+
99
+ const [hydrated] = rowsToBundles([toRow(bundle)], [], []);
100
+
101
+ expect(hydrated).toMatchObject({ patches: [] });
102
+ });
103
+
104
+ it("preserves nested metadata at the row boundary", () => {
105
+ const row: BundleRow = {
106
+ ...toRow(createBundle("nested-metadata")),
107
+ metadata: { release: { flags: [true, null, 3] } },
108
+ };
109
+
110
+ const [hydrated] = rowsToBundles([row], [], []);
111
+
112
+ expect(hydrated).toMatchObject({
113
+ metadata: { release: { flags: [true, null, 3] } },
114
+ });
115
+ });
116
+
117
+ it("rejects duplicate patch ids during aggregate hydration", () => {
118
+ const base = createBundle("base");
119
+ const target = createBundle("target");
120
+ const patch = createPatchRow("duplicate", target.id, base.id, 0);
121
+ const duplicate = { ...patch, base_file_hash: "different-hash" };
122
+
123
+ const hydrate = () =>
124
+ rowsToBundles([toRow(target)], [patch, duplicate], [toRow(base)]);
125
+
126
+ expect(hydrate).toThrowError(BundleRowHydrationError);
127
+ expect(hydrate).toThrowError(
128
+ expect.objectContaining({
129
+ reason: "duplicate_patch_id",
130
+ patchId: patch.id,
131
+ }),
132
+ );
133
+ });
134
+
135
+ it("rejects a patch whose owner bundle row is absent", () => {
136
+ const base = createBundle("base");
137
+ const patch = createPatchRow("orphan-owner", "missing", base.id, 0);
138
+
139
+ const hydrate = () => rowsToBundles([], [patch], [toRow(base)]);
140
+
141
+ expect(hydrate).toThrowError(
142
+ expect.objectContaining({
143
+ reason: "orphan_patch_owner",
144
+ patchId: patch.id,
145
+ bundleId: patch.bundle_id,
146
+ }),
147
+ );
148
+ });
149
+
150
+ it("rejects a patch whose base bundle row is absent", () => {
151
+ const target = createBundle("target");
152
+ const patch = createPatchRow("orphan-base", target.id, "missing", 0);
153
+
154
+ const hydrate = () => rowsToBundles([toRow(target)], [patch], []);
155
+
156
+ expect(hydrate).toThrowError(
157
+ expect.objectContaining({
158
+ reason: "orphan_patch_base",
159
+ patchId: patch.id,
160
+ bundleId: patch.base_bundle_id,
161
+ }),
162
+ );
163
+ });
164
+ });
@@ -1,140 +1,10 @@
1
- import type { Bundle, BundlePatchArtifact, Platform } from "@hot-updater/core";
2
- import {
3
- DEFAULT_ROLLOUT_COHORT_COUNT,
4
- getAssetBaseStorageUri,
5
- getBundlePatches,
6
- getManifestFileHash,
7
- getManifestStorageUri,
8
- stripBundleArtifactMetadata,
9
- } from "@hot-updater/core";
10
-
11
- import { parseBundleMetadata } from "./updateArtifacts";
12
-
13
- export type BundleRow = {
14
- readonly id: string;
15
- readonly platform: string;
16
- readonly should_force_update: unknown;
17
- readonly enabled: unknown;
18
- readonly file_hash: string;
19
- readonly git_commit_hash: string | null;
20
- readonly message: string | null;
21
- readonly channel: string;
22
- readonly storage_uri: string;
23
- readonly target_app_version: string | null;
24
- readonly fingerprint_hash: string | null;
25
- readonly metadata?: unknown;
26
- readonly manifest_storage_uri?: string | null;
27
- readonly manifest_file_hash?: string | null;
28
- readonly asset_base_storage_uri?: string | null;
29
- readonly rollout_cohort_count?: number | null;
30
- readonly target_cohorts?: unknown;
31
- };
32
-
33
- export type BundlePatchRow = {
34
- readonly id: string;
35
- readonly bundle_id: string;
36
- readonly base_bundle_id: string;
37
- readonly base_file_hash: string;
38
- readonly patch_file_hash: string;
39
- readonly patch_storage_uri: string;
40
- readonly order_index?: number | null;
41
- };
42
-
43
- const parseTargetCohorts = (value: unknown): string[] | null => {
44
- if (!value) return null;
45
- if (Array.isArray(value)) {
46
- return value.filter((item): item is string => typeof item === "string");
47
- }
48
- if (typeof value !== "string") return null;
49
- try {
50
- const parsed = JSON.parse(value) as unknown;
51
- return Array.isArray(parsed)
52
- ? parsed.filter((item): item is string => typeof item === "string")
53
- : null;
54
- } catch {
55
- return null;
56
- }
57
- };
58
-
59
- const buildBundlePatchId = (bundleId: string, baseBundleId: string) =>
60
- `${bundleId}:${baseBundleId}`;
61
-
62
- export const bundleToRow = (bundle: Bundle): BundleRow => ({
63
- id: bundle.id,
64
- platform: bundle.platform,
65
- should_force_update: bundle.shouldForceUpdate,
66
- enabled: bundle.enabled,
67
- file_hash: bundle.fileHash,
68
- git_commit_hash: bundle.gitCommitHash,
69
- message: bundle.message,
70
- channel: bundle.channel,
71
- storage_uri: bundle.storageUri,
72
- target_app_version: bundle.targetAppVersion,
73
- fingerprint_hash: bundle.fingerprintHash,
74
- metadata: stripBundleArtifactMetadata(bundle.metadata) ?? {},
75
- manifest_storage_uri: getManifestStorageUri(bundle),
76
- manifest_file_hash: getManifestFileHash(bundle),
77
- asset_base_storage_uri: getAssetBaseStorageUri(bundle),
78
- rollout_cohort_count:
79
- bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
80
- target_cohorts: bundle.targetCohorts ?? null,
81
- });
82
-
83
- export const bundleToPatchRows = (bundle: Bundle): BundlePatchRow[] =>
84
- getBundlePatches(bundle).map((patch, index) => ({
85
- id: buildBundlePatchId(bundle.id, patch.baseBundleId),
86
- bundle_id: bundle.id,
87
- base_bundle_id: patch.baseBundleId,
88
- base_file_hash: patch.baseFileHash,
89
- patch_file_hash: patch.patchFileHash,
90
- patch_storage_uri: patch.patchStorageUri,
91
- order_index: index,
92
- }));
93
-
94
- const mapPatchRowToPatch = (record: BundlePatchRow): BundlePatchArtifact => ({
95
- baseBundleId: record.base_bundle_id,
96
- baseFileHash: record.base_file_hash,
97
- patchFileHash: record.patch_file_hash,
98
- patchStorageUri: record.patch_storage_uri,
99
- });
100
-
101
- export const rowToBundle = (
102
- record: BundleRow,
103
- patchRecords: readonly BundlePatchRow[] = [],
104
- ): Bundle => {
105
- const patches = patchRecords
106
- .slice()
107
- .sort(
108
- (left, right) =>
109
- (left.order_index ?? 0) - (right.order_index ?? 0) ||
110
- left.base_bundle_id.localeCompare(right.base_bundle_id),
111
- )
112
- .map(mapPatchRowToPatch);
113
- const primaryPatch = patches[0] ?? null;
114
-
115
- return {
116
- id: record.id,
117
- platform: record.platform as Platform,
118
- shouldForceUpdate: Boolean(record.should_force_update),
119
- enabled: Boolean(record.enabled),
120
- fileHash: record.file_hash,
121
- gitCommitHash: record.git_commit_hash ?? null,
122
- message: record.message ?? null,
123
- channel: record.channel,
124
- storageUri: record.storage_uri,
125
- targetAppVersion: record.target_app_version ?? null,
126
- fingerprintHash: record.fingerprint_hash ?? null,
127
- metadata: parseBundleMetadata(record.metadata),
128
- manifestStorageUri: record.manifest_storage_uri ?? null,
129
- manifestFileHash: record.manifest_file_hash ?? null,
130
- assetBaseStorageUri: record.asset_base_storage_uri ?? null,
131
- patches,
132
- patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
133
- patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
134
- patchFileHash: primaryPatch?.patchFileHash ?? null,
135
- patchStorageUri: primaryPatch?.patchStorageUri ?? null,
136
- rolloutCohortCount:
137
- record.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
138
- targetCohorts: parseTargetCohorts(record.target_cohorts),
139
- };
140
- };
1
+ export {
2
+ BundleRowHydrationError,
3
+ BundleRowHydrationErrorReason,
4
+ bundleToPatchRows,
5
+ bundleToRow,
6
+ rowToBundle,
7
+ rowsToBundles,
8
+ } from "@hot-updater/plugin-core";
9
+
10
+ export type { BundlePatchRow, BundleRow } from "@hot-updater/plugin-core";