@hot-updater/server 1.0.4 → 1.0.6

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.
@@ -31,6 +31,8 @@ declare const createCoreApi: (db: CoreDatabase, storage: CoreStorage, options?:
31
31
  deleteBundles: (ids: readonly string[]) => Promise<void>;
32
32
  getReleaseCatalog(input: import("@hot-updater/plugin-core").ReleaseCatalogRequest): Promise<import("@hot-updater/protocol").ReleaseCatalog | null>;
33
33
  getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 1): Promise<import("@hot-updater/protocol").ArtifactInfo | null>;
34
+ getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 2): Promise<import("@hot-updater/protocol").ArtifactInfoV2 | null>;
35
+ getArtifactFiles(bundleId: string, paths: readonly string[]): Promise<import("@hot-updater/protocol").ArtifactFiles | null>;
34
36
  getBundle(id: string): Promise<import("@hot-updater/plugin-core").BundleDetail | null>;
35
37
  countBundleChildren(baseBundleIds: readonly string[]): Promise<Record<string, number>>;
36
38
  listPatchesFromBase(baseBundleId: string, input: import("@hot-updater/plugin-core").KeysetInput): Promise<import("@hot-updater/plugin-core").BundlePatchRow[]>;
@@ -31,6 +31,8 @@ declare const createCoreApi: (db: CoreDatabase, storage: CoreStorage, options?:
31
31
  deleteBundles: (ids: readonly string[]) => Promise<void>;
32
32
  getReleaseCatalog(input: import("@hot-updater/plugin-core").ReleaseCatalogRequest): Promise<import("@hot-updater/protocol").ReleaseCatalog | null>;
33
33
  getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 1): Promise<import("@hot-updater/protocol").ArtifactInfo | null>;
34
+ getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 2): Promise<import("@hot-updater/protocol").ArtifactInfoV2 | null>;
35
+ getArtifactFiles(bundleId: string, paths: readonly string[]): Promise<import("@hot-updater/protocol").ArtifactFiles | null>;
34
36
  getBundle(id: string): Promise<import("@hot-updater/plugin-core").BundleDetail | null>;
35
37
  countBundleChildren(baseBundleIds: readonly string[]): Promise<Record<string, number>>;
36
38
  listPatchesFromBase(baseBundleId: string, input: import("@hot-updater/plugin-core").KeysetInput): Promise<import("@hot-updater/plugin-core").BundlePatchRow[]>;
@@ -106,20 +106,34 @@ const createCoreReads = (db, storage) => {
106
106
  return require_releaseCatalog.projectReleaseCatalogRow(row === null ? null : toCatalogRow(row), input, scopeKey);
107
107
  },
108
108
  /** Artifact resolution: one batch read of both bundles and one unique read of their patch. */
109
- async getArtifactInfo(targetBundleId, currentBundleId, artifactProtocolVersion) {
109
+ getArtifactInfo: (async (targetBundleId, currentBundleId, artifactProtocolVersion) => {
110
110
  const { readStorageText } = storage;
111
- if (artifactProtocolVersion !== _hot_updater_protocol.ARTIFACT_PROTOCOL_VERSION || readStorageText === void 0) return null;
111
+ if (artifactProtocolVersion !== _hot_updater_protocol.ARTIFACT_PROTOCOL_VERSION && artifactProtocolVersion !== _hot_updater_protocol.ARTIFACT_FILES_PROTOCOL_VERSION || readStorageText === void 0) return null;
112
112
  const withCurrent = currentBundleId !== _hot_updater_protocol.NIL_UUID;
113
113
  const [[target, current], patch] = await Promise.all([db.findByKeys("bundles", withCurrent ? [{ id: targetBundleId }, { id: currentBundleId }] : [{ id: targetBundleId }]), withCurrent ? db.findOne("bundle_patches", {
114
114
  bundle_id: targetBundleId,
115
115
  base_bundle_id: currentBundleId
116
116
  }) : null]);
117
117
  if (target === null || target === void 0) return null;
118
- return require_updateArtifacts.resolveManifestArtifacts({
118
+ const input = {
119
119
  currentBundle: current ? (0, _hot_updater_plugin_core.rowToBundle)(toBundleRow(current)) : null,
120
120
  readStorageText,
121
121
  resolveFileUrl: storage.resolveFileUrl,
122
122
  targetBundle: (0, _hot_updater_plugin_core.rowToBundle)(toBundleRow(target), patch === null ? [] : [toPatchRow(patch)])
123
+ };
124
+ return artifactProtocolVersion === _hot_updater_protocol.ARTIFACT_FILES_PROTOCOL_VERSION ? require_updateArtifacts.resolveManifestArtifactsV2(input) : require_updateArtifacts.resolveManifestArtifacts(input);
125
+ }),
126
+ /** A Bundle's file URLs: one point read of the bundle and its manifest. */
127
+ async getArtifactFiles(bundleId, paths) {
128
+ const { readStorageText } = storage;
129
+ if (readStorageText === void 0) return null;
130
+ const row = await db.findOne("bundles", { id: bundleId });
131
+ if (row === null) return null;
132
+ return require_updateArtifacts.resolveManifestArtifactFiles({
133
+ paths,
134
+ readStorageText,
135
+ resolveFileUrl: storage.resolveFileUrl,
136
+ targetBundle: (0, _hot_updater_plugin_core.rowToBundle)(toBundleRow(row))
123
137
  });
124
138
  },
125
139
  /** A bundle, its patches, and how many patches start from it. */
@@ -1,8 +1,8 @@
1
1
  import { projectReleaseCatalogRow, releaseCatalogScopeKeyOf } from "../db/releaseCatalog.mjs";
2
- import { resolveManifestArtifacts } from "../db/updateArtifacts.mjs";
2
+ import { resolveManifestArtifactFiles, resolveManifestArtifacts, resolveManifestArtifactsV2 } from "../db/updateArtifacts.mjs";
3
3
  import { baseBundleIdsOf } from "./baseCandidates.mjs";
4
4
  import { parseBaseCandidateKey, rowToBundle } from "@hot-updater/plugin-core";
5
- import { ARTIFACT_PROTOCOL_VERSION, NIL_UUID } from "@hot-updater/protocol";
5
+ import { ARTIFACT_FILES_PROTOCOL_VERSION, ARTIFACT_PROTOCOL_VERSION, NIL_UUID } from "@hot-updater/protocol";
6
6
  //#region src/core/reads.ts
7
7
  const PAGE = 500;
8
8
  const pick = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
@@ -106,20 +106,34 @@ const createCoreReads = (db, storage) => {
106
106
  return projectReleaseCatalogRow(row === null ? null : toCatalogRow(row), input, scopeKey);
107
107
  },
108
108
  /** Artifact resolution: one batch read of both bundles and one unique read of their patch. */
109
- async getArtifactInfo(targetBundleId, currentBundleId, artifactProtocolVersion) {
109
+ getArtifactInfo: (async (targetBundleId, currentBundleId, artifactProtocolVersion) => {
110
110
  const { readStorageText } = storage;
111
- if (artifactProtocolVersion !== ARTIFACT_PROTOCOL_VERSION || readStorageText === void 0) return null;
111
+ if (artifactProtocolVersion !== ARTIFACT_PROTOCOL_VERSION && artifactProtocolVersion !== ARTIFACT_FILES_PROTOCOL_VERSION || readStorageText === void 0) return null;
112
112
  const withCurrent = currentBundleId !== NIL_UUID;
113
113
  const [[target, current], patch] = await Promise.all([db.findByKeys("bundles", withCurrent ? [{ id: targetBundleId }, { id: currentBundleId }] : [{ id: targetBundleId }]), withCurrent ? db.findOne("bundle_patches", {
114
114
  bundle_id: targetBundleId,
115
115
  base_bundle_id: currentBundleId
116
116
  }) : null]);
117
117
  if (target === null || target === void 0) return null;
118
- return resolveManifestArtifacts({
118
+ const input = {
119
119
  currentBundle: current ? rowToBundle(toBundleRow(current)) : null,
120
120
  readStorageText,
121
121
  resolveFileUrl: storage.resolveFileUrl,
122
122
  targetBundle: rowToBundle(toBundleRow(target), patch === null ? [] : [toPatchRow(patch)])
123
+ };
124
+ return artifactProtocolVersion === ARTIFACT_FILES_PROTOCOL_VERSION ? resolveManifestArtifactsV2(input) : resolveManifestArtifacts(input);
125
+ }),
126
+ /** A Bundle's file URLs: one point read of the bundle and its manifest. */
127
+ async getArtifactFiles(bundleId, paths) {
128
+ const { readStorageText } = storage;
129
+ if (readStorageText === void 0) return null;
130
+ const row = await db.findOne("bundles", { id: bundleId });
131
+ if (row === null) return null;
132
+ return resolveManifestArtifactFiles({
133
+ paths,
134
+ readStorageText,
135
+ resolveFileUrl: storage.resolveFileUrl,
136
+ targetBundle: rowToBundle(toBundleRow(row))
123
137
  });
124
138
  },
125
139
  /** A bundle, its patches, and how many patches start from it. */
@@ -114,6 +114,11 @@ async function resolveAssets(plan, resolveFileUrl) {
114
114
  const resolvedEntries = entries.filter((entry) => entry !== null);
115
115
  return { assets: Object.fromEntries(resolvedEntries.map((entry) => entry.asset)) };
116
116
  }
117
+ /**
118
+ * Legacy: artifact protocol v1. Signs a URL for every file of the target, which grows with the
119
+ * Bundle's file count; apps whose SDK predates protocol v2 still ask for it.
120
+ * Remove with the `/artifacts/v1` route once no supported app uses it.
121
+ */
117
122
  async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
118
123
  const manifestStorageUri = targetBundle ? (0, _hot_updater_protocol.getManifestStorageUri)(targetBundle) : null;
119
124
  const manifestFileHash = targetBundle ? (0, _hot_updater_protocol.getManifestFileHash)(targetBundle) : null;
@@ -147,5 +152,99 @@ async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readSto
147
152
  ...archiveUrl ? { archiveUrl } : {}
148
153
  };
149
154
  }
155
+ /** The target's manifest and its plan, or null for a Bundle without one. */
156
+ async function loadTargetPlan({ currentBundle, readStorageText, targetBundle }) {
157
+ const manifestStorageUri = (0, _hot_updater_protocol.getManifestStorageUri)(targetBundle);
158
+ const manifestFileHash = (0, _hot_updater_protocol.getManifestFileHash)(targetBundle);
159
+ const assetBaseStorageUri = (0, _hot_updater_protocol.getAssetBaseStorageUri)(targetBundle);
160
+ if (!manifestStorageUri || !manifestFileHash || !assetBaseStorageUri) return null;
161
+ const targetManifest = await fetchBundleManifest(manifestStorageUri, readStorageText);
162
+ if (!targetManifest) return null;
163
+ return {
164
+ manifestFileHash,
165
+ manifestStorageUri,
166
+ plan: createManifestArtifactPlan({
167
+ assetBaseStorageUri,
168
+ currentBundle,
169
+ targetBundle,
170
+ targetManifest
171
+ }),
172
+ targetManifest
173
+ };
174
+ }
175
+ /**
176
+ * Artifact protocol v2: the manifest, the archive, and the one Hermes patch,
177
+ * but no URL per file. A device asks {@link resolveManifestArtifactFiles}
178
+ * for the files it downloads one by one, so signing and the response stay
179
+ * the same size however many files the Bundle has.
180
+ */
181
+ async function resolveManifestArtifactsV2({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
182
+ const target = await loadTargetPlan({
183
+ currentBundle,
184
+ readStorageText,
185
+ targetBundle
186
+ });
187
+ if (!target) return null;
188
+ const manifestUrl = await resolveFileUrl(target.manifestStorageUri);
189
+ if (!manifestUrl) return null;
190
+ const patched = target.plan.assets.find((asset) => asset.patch !== null);
191
+ let patch;
192
+ if (patched?.patch && !(patched.file.byteSize !== null && patched.patch.byteSize !== null && patched.patch.byteSize >= patched.file.byteSize)) try {
193
+ const patchUrl = await resolveFileUrl(patched.patch.storageUri);
194
+ if (patchUrl) patch = {
195
+ algorithm: "bsdiff",
196
+ assetPath: patched.assetPath,
197
+ baseBundleId: patched.patch.baseBundleId,
198
+ baseFileHash: patched.patch.baseFileHash,
199
+ patchFileHash: patched.patch.patchFileHash,
200
+ patchUrl,
201
+ ...patched.patch.byteSize === null ? {} : { byteSize: patched.patch.byteSize }
202
+ };
203
+ } catch {}
204
+ let archiveUrl = null;
205
+ const archive = target.targetManifest.archive;
206
+ if (archive && (0, _hot_updater_plugin_core.isContentAddressedAssetFileHash)(archive.downloadFileHash) && (asByteSize(archive.downloadByteSize) ?? 0) > 0 && (asByteSize(archive.tarByteSize) ?? 0) > 0) try {
207
+ archiveUrl = await resolveFileUrl((0, _hot_updater_plugin_core.getBundleArchiveStorageUri)({
208
+ manifestStorageUri: target.manifestStorageUri,
209
+ bundleId: targetBundle.id
210
+ }));
211
+ } catch {}
212
+ return {
213
+ artifactProtocolVersion: _hot_updater_protocol.ARTIFACT_FILES_PROTOCOL_VERSION,
214
+ manifestFileHash: target.manifestFileHash,
215
+ manifestUrl,
216
+ ...archiveUrl ? { archiveUrl } : {},
217
+ ...patch ? { patch } : {}
218
+ };
219
+ }
220
+ /**
221
+ * Download URLs for the files a device downloads one by one. Only paths the
222
+ * target's manifest names are signed, at most
223
+ * {@link ARTIFACT_FILES_REQUEST_LIMIT}; null when any path is unknown or a
224
+ * URL cannot be resolved.
225
+ */
226
+ async function resolveManifestArtifactFiles({ paths, resolveFileUrl, readStorageText, targetBundle }) {
227
+ if (paths.length === 0 || paths.length > _hot_updater_protocol.ARTIFACT_FILES_REQUEST_LIMIT || new Set(paths).size !== paths.length) return null;
228
+ const target = await loadTargetPlan({
229
+ currentBundle: null,
230
+ readStorageText,
231
+ targetBundle
232
+ });
233
+ if (!target) return null;
234
+ const planned = new Map(target.plan.assets.map((asset) => [asset.assetPath, asset]));
235
+ const requested = paths.map((path) => planned.get(path));
236
+ if (requested.some((asset) => asset === void 0)) return null;
237
+ const files = await Promise.all(requested.map(async (asset) => {
238
+ const url = await resolveFileUrl(asset.file.storageUri);
239
+ if (!url) return null;
240
+ const file = { url };
241
+ if (asset.file.compression) file.compression = asset.file.compression;
242
+ return [asset.assetPath, file];
243
+ }));
244
+ if (files.some((file) => file === null)) return null;
245
+ return { files: Object.fromEntries(files.filter((file) => file !== null)) };
246
+ }
150
247
  //#endregion
248
+ exports.resolveManifestArtifactFiles = resolveManifestArtifactFiles;
151
249
  exports.resolveManifestArtifacts = resolveManifestArtifacts;
250
+ exports.resolveManifestArtifactsV2 = resolveManifestArtifactsV2;
@@ -1,5 +1,5 @@
1
1
  import { getBundleArchiveStorageUri, getManifestAssetDownloadPath, isContentAddressedAssetFileHash, resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
2
- import { ARTIFACT_PROTOCOL_VERSION, getAssetBaseStorageUri, getBundlePatch, getManifestFileHash, getManifestStorageUri } from "@hot-updater/protocol";
2
+ import { ARTIFACT_FILES_PROTOCOL_VERSION, ARTIFACT_FILES_REQUEST_LIMIT, ARTIFACT_PROTOCOL_VERSION, getAssetBaseStorageUri, getBundlePatch, getManifestFileHash, getManifestStorageUri } from "@hot-updater/protocol";
3
3
  //#region src/db/updateArtifacts.ts
4
4
  const HBC_ASSET_PATH_RE = /\.bundle$/;
5
5
  const asByteSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
@@ -114,6 +114,11 @@ async function resolveAssets(plan, resolveFileUrl) {
114
114
  const resolvedEntries = entries.filter((entry) => entry !== null);
115
115
  return { assets: Object.fromEntries(resolvedEntries.map((entry) => entry.asset)) };
116
116
  }
117
+ /**
118
+ * Legacy: artifact protocol v1. Signs a URL for every file of the target, which grows with the
119
+ * Bundle's file count; apps whose SDK predates protocol v2 still ask for it.
120
+ * Remove with the `/artifacts/v1` route once no supported app uses it.
121
+ */
117
122
  async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
118
123
  const manifestStorageUri = targetBundle ? getManifestStorageUri(targetBundle) : null;
119
124
  const manifestFileHash = targetBundle ? getManifestFileHash(targetBundle) : null;
@@ -147,5 +152,97 @@ async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readSto
147
152
  ...archiveUrl ? { archiveUrl } : {}
148
153
  };
149
154
  }
155
+ /** The target's manifest and its plan, or null for a Bundle without one. */
156
+ async function loadTargetPlan({ currentBundle, readStorageText, targetBundle }) {
157
+ const manifestStorageUri = getManifestStorageUri(targetBundle);
158
+ const manifestFileHash = getManifestFileHash(targetBundle);
159
+ const assetBaseStorageUri = getAssetBaseStorageUri(targetBundle);
160
+ if (!manifestStorageUri || !manifestFileHash || !assetBaseStorageUri) return null;
161
+ const targetManifest = await fetchBundleManifest(manifestStorageUri, readStorageText);
162
+ if (!targetManifest) return null;
163
+ return {
164
+ manifestFileHash,
165
+ manifestStorageUri,
166
+ plan: createManifestArtifactPlan({
167
+ assetBaseStorageUri,
168
+ currentBundle,
169
+ targetBundle,
170
+ targetManifest
171
+ }),
172
+ targetManifest
173
+ };
174
+ }
175
+ /**
176
+ * Artifact protocol v2: the manifest, the archive, and the one Hermes patch,
177
+ * but no URL per file. A device asks {@link resolveManifestArtifactFiles}
178
+ * for the files it downloads one by one, so signing and the response stay
179
+ * the same size however many files the Bundle has.
180
+ */
181
+ async function resolveManifestArtifactsV2({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
182
+ const target = await loadTargetPlan({
183
+ currentBundle,
184
+ readStorageText,
185
+ targetBundle
186
+ });
187
+ if (!target) return null;
188
+ const manifestUrl = await resolveFileUrl(target.manifestStorageUri);
189
+ if (!manifestUrl) return null;
190
+ const patched = target.plan.assets.find((asset) => asset.patch !== null);
191
+ let patch;
192
+ if (patched?.patch && !(patched.file.byteSize !== null && patched.patch.byteSize !== null && patched.patch.byteSize >= patched.file.byteSize)) try {
193
+ const patchUrl = await resolveFileUrl(patched.patch.storageUri);
194
+ if (patchUrl) patch = {
195
+ algorithm: "bsdiff",
196
+ assetPath: patched.assetPath,
197
+ baseBundleId: patched.patch.baseBundleId,
198
+ baseFileHash: patched.patch.baseFileHash,
199
+ patchFileHash: patched.patch.patchFileHash,
200
+ patchUrl,
201
+ ...patched.patch.byteSize === null ? {} : { byteSize: patched.patch.byteSize }
202
+ };
203
+ } catch {}
204
+ let archiveUrl = null;
205
+ const archive = target.targetManifest.archive;
206
+ if (archive && isContentAddressedAssetFileHash(archive.downloadFileHash) && (asByteSize(archive.downloadByteSize) ?? 0) > 0 && (asByteSize(archive.tarByteSize) ?? 0) > 0) try {
207
+ archiveUrl = await resolveFileUrl(getBundleArchiveStorageUri({
208
+ manifestStorageUri: target.manifestStorageUri,
209
+ bundleId: targetBundle.id
210
+ }));
211
+ } catch {}
212
+ return {
213
+ artifactProtocolVersion: ARTIFACT_FILES_PROTOCOL_VERSION,
214
+ manifestFileHash: target.manifestFileHash,
215
+ manifestUrl,
216
+ ...archiveUrl ? { archiveUrl } : {},
217
+ ...patch ? { patch } : {}
218
+ };
219
+ }
220
+ /**
221
+ * Download URLs for the files a device downloads one by one. Only paths the
222
+ * target's manifest names are signed, at most
223
+ * {@link ARTIFACT_FILES_REQUEST_LIMIT}; null when any path is unknown or a
224
+ * URL cannot be resolved.
225
+ */
226
+ async function resolveManifestArtifactFiles({ paths, resolveFileUrl, readStorageText, targetBundle }) {
227
+ if (paths.length === 0 || paths.length > ARTIFACT_FILES_REQUEST_LIMIT || new Set(paths).size !== paths.length) return null;
228
+ const target = await loadTargetPlan({
229
+ currentBundle: null,
230
+ readStorageText,
231
+ targetBundle
232
+ });
233
+ if (!target) return null;
234
+ const planned = new Map(target.plan.assets.map((asset) => [asset.assetPath, asset]));
235
+ const requested = paths.map((path) => planned.get(path));
236
+ if (requested.some((asset) => asset === void 0)) return null;
237
+ const files = await Promise.all(requested.map(async (asset) => {
238
+ const url = await resolveFileUrl(asset.file.storageUri);
239
+ if (!url) return null;
240
+ const file = { url };
241
+ if (asset.file.compression) file.compression = asset.file.compression;
242
+ return [asset.assetPath, file];
243
+ }));
244
+ if (files.some((file) => file === null)) return null;
245
+ return { files: Object.fromEntries(files.filter((file) => file !== null)) };
246
+ }
150
247
  //#endregion
151
- export { resolveManifestArtifacts };
248
+ export { resolveManifestArtifactFiles, resolveManifestArtifacts, resolveManifestArtifactsV2 };
package/dist/handler.cjs CHANGED
@@ -90,6 +90,8 @@ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins =
90
90
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
91
91
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
92
92
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
93
+ addClientRoute("GET", "/artifacts/v2/:targetBundleId/from/:currentBundleId", "artifactV2");
94
+ addClientRoute("POST", "/artifacts/v2/:targetBundleId/files", "artifactFilesV2");
93
95
  const adminRouter = require_internalRouter.createRouter();
94
96
  const addAdminRoute = mount(adminRouter, true);
95
97
  addAdminRoute("GET", "/version", "adminVersion");
package/dist/handler.mjs CHANGED
@@ -91,6 +91,8 @@ function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins =
91
91
  addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
92
92
  addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
93
93
  addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
94
+ addClientRoute("GET", "/artifacts/v2/:targetBundleId/from/:currentBundleId", "artifactV2");
95
+ addClientRoute("POST", "/artifacts/v2/:targetBundleId/files", "artifactFilesV2");
94
96
  const adminRouter = createRouter();
95
97
  const addAdminRoute = mount(adminRouter, true);
96
98
  addAdminRoute("GET", "/version", "adminVersion");
@@ -4,6 +4,7 @@ let _hot_updater_protocol = require("@hot-updater/protocol");
4
4
  //#region src/handlerReleaseCatalogRoutes.ts
5
5
  const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
6
6
  const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
7
+ const ARTIFACT_V2_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=2";
7
8
  const ORIGIN_CACHE_TTL_MS = 5e3;
8
9
  const ORIGIN_CACHE_MAX_ENTRIES = 128;
9
10
  /** Marks the 404 of a scope that has no catalog. */
@@ -26,6 +27,18 @@ const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
26
27
  * `Cache-Control`.
27
28
  */
28
29
  const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
30
+ /** The paths of an artifact files request, or null when malformed. */
31
+ const readArtifactFilePaths = async (request) => {
32
+ let body;
33
+ try {
34
+ body = await request.json();
35
+ } catch {
36
+ return null;
37
+ }
38
+ const paths = body?.paths;
39
+ if (!Array.isArray(paths) || paths.length === 0 || paths.length > _hot_updater_protocol.ARTIFACT_FILES_REQUEST_LIMIT || !paths.every((path) => typeof path === "string" && path.length > 0) || new Set(paths).size !== paths.length) return null;
40
+ return paths;
41
+ };
29
42
  const privateNotFound = () => Response.json({ error: "Not found" }, {
30
43
  status: 404,
31
44
  headers: { "cache-control": "private, no-store" }
@@ -146,6 +159,30 @@ const createReleaseCatalogRouteHandlers = () => {
146
159
  if (invalid) return invalid;
147
160
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
148
161
  },
162
+ artifactV2: async (params, _request, { core }) => {
163
+ const info = await core.getArtifactInfo(require_handlerParameters.requireRouteParam(params, "targetBundleId"), require_handlerParameters.requireRouteParam(params, "currentBundleId"), 2);
164
+ if (info === null) return privateNotFound();
165
+ return new Response(JSON.stringify(info), {
166
+ status: 200,
167
+ headers: {
168
+ "cache-control": "private, no-store",
169
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
170
+ }
171
+ });
172
+ },
173
+ artifactFilesV2: async (params, request, { core }) => {
174
+ const paths = await readArtifactFilePaths(request);
175
+ if (paths === null) return badRequest(`paths must be 1 to ${_hot_updater_protocol.ARTIFACT_FILES_REQUEST_LIMIT} unique strings`);
176
+ const files = await core.getArtifactFiles(require_handlerParameters.requireRouteParam(params, "targetBundleId"), paths);
177
+ if (files === null) return privateNotFound();
178
+ return new Response(JSON.stringify(files), {
179
+ status: 200,
180
+ headers: {
181
+ "cache-control": "private, no-store",
182
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
183
+ }
184
+ });
185
+ },
149
186
  artifactV1: async (params, _request, { core }) => {
150
187
  const info = await core.getArtifactInfo(require_handlerParameters.requireRouteParam(params, "targetBundleId"), require_handlerParameters.requireRouteParam(params, "currentBundleId"), 1);
151
188
  if (info === null) return privateNotFound();
@@ -1,9 +1,10 @@
1
1
  import { requirePlatformParam, requireRouteParam } from "./handlerParameters.mjs";
2
2
  import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
3
- import { createReleaseCatalogScopeKey } from "@hot-updater/protocol";
3
+ import { ARTIFACT_FILES_REQUEST_LIMIT, createReleaseCatalogScopeKey } from "@hot-updater/protocol";
4
4
  //#region src/handlerReleaseCatalogRoutes.ts
5
5
  const CATALOG_CONTENT_TYPE = "application/vnd.hot-updater.release-catalog+json; version=1";
6
6
  const ARTIFACT_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=1";
7
+ const ARTIFACT_V2_CONTENT_TYPE = "application/vnd.hot-updater.artifact+json; version=2";
7
8
  const ORIGIN_CACHE_TTL_MS = 5e3;
8
9
  const ORIGIN_CACHE_MAX_ENTRIES = 128;
9
10
  /** Marks the 404 of a scope that has no catalog. */
@@ -26,6 +27,18 @@ const CATALOG_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
26
27
  * `Cache-Control`.
27
28
  */
28
29
  const CATALOG_CDN_CACHE_CONTROL = "public, max-age=5, stale-while-revalidate=5, stale-if-error=0";
30
+ /** The paths of an artifact files request, or null when malformed. */
31
+ const readArtifactFilePaths = async (request) => {
32
+ let body;
33
+ try {
34
+ body = await request.json();
35
+ } catch {
36
+ return null;
37
+ }
38
+ const paths = body?.paths;
39
+ if (!Array.isArray(paths) || paths.length === 0 || paths.length > ARTIFACT_FILES_REQUEST_LIMIT || !paths.every((path) => typeof path === "string" && path.length > 0) || new Set(paths).size !== paths.length) return null;
40
+ return paths;
41
+ };
29
42
  const privateNotFound = () => Response.json({ error: "Not found" }, {
30
43
  status: 404,
31
44
  headers: { "cache-control": "private, no-store" }
@@ -146,6 +159,30 @@ const createReleaseCatalogRouteHandlers = () => {
146
159
  if (invalid) return invalid;
147
160
  return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
148
161
  },
162
+ artifactV2: async (params, _request, { core }) => {
163
+ const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 2);
164
+ if (info === null) return privateNotFound();
165
+ return new Response(JSON.stringify(info), {
166
+ status: 200,
167
+ headers: {
168
+ "cache-control": "private, no-store",
169
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
170
+ }
171
+ });
172
+ },
173
+ artifactFilesV2: async (params, request, { core }) => {
174
+ const paths = await readArtifactFilePaths(request);
175
+ if (paths === null) return badRequest(`paths must be 1 to ${ARTIFACT_FILES_REQUEST_LIMIT} unique strings`);
176
+ const files = await core.getArtifactFiles(requireRouteParam(params, "targetBundleId"), paths);
177
+ if (files === null) return privateNotFound();
178
+ return new Response(JSON.stringify(files), {
179
+ status: 200,
180
+ headers: {
181
+ "cache-control": "private, no-store",
182
+ "content-type": ARTIFACT_V2_CONTENT_TYPE
183
+ }
184
+ });
185
+ },
149
186
  artifactV1: async (params, _request, { core }) => {
150
187
  const info = await core.getArtifactInfo(requireRouteParam(params, "targetBundleId"), requireRouteParam(params, "currentBundleId"), 1);
151
188
  if (info === null) return privateNotFound();
package/dist/package.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "1.0.4";
2
+ var version = "1.0.6";
3
3
  //#endregion
4
4
  Object.defineProperty(exports, "version", {
5
5
  enumerable: true,
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.0.4";
2
+ var version = "1.0.6";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/server",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "sideEffects": false,
@@ -50,11 +50,11 @@
50
50
  "dependencies": {
51
51
  "rou3": "0.7.9",
52
52
  "verkit": "0.4.0",
53
- "@hot-updater/protocol": "1.0.4",
54
- "@hot-updater/plugin-api-keys": "1.0.4",
55
- "@hot-updater/plugin-insights": "1.0.4",
56
- "@hot-updater/plugin-remote-config": "1.0.4",
57
- "@hot-updater/plugin-core": "1.0.4"
53
+ "@hot-updater/plugin-core": "1.0.6",
54
+ "@hot-updater/protocol": "1.0.6",
55
+ "@hot-updater/plugin-api-keys": "1.0.6",
56
+ "@hot-updater/plugin-insights": "1.0.6",
57
+ "@hot-updater/plugin-remote-config": "1.0.6"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@prisma/client": "*",
@@ -95,8 +95,8 @@
95
95
  "prisma": "6.19.3",
96
96
  "uuidv7": "^1.0.2",
97
97
  "wrangler": "^4.69.0",
98
- "@hot-updater/standalone": "1.0.4",
99
- "@hot-updater/test-utils": "1.0.4"
98
+ "@hot-updater/standalone": "1.0.6",
99
+ "@hot-updater/test-utils": "1.0.6"
100
100
  },
101
101
  "nx": {
102
102
  "implicitDependencies": [