@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.
- package/dist/core/api.d.cts +2 -0
- package/dist/core/api.d.mts +2 -0
- package/dist/core/reads.cjs +17 -3
- package/dist/core/reads.mjs +19 -5
- package/dist/db/updateArtifacts.cjs +99 -0
- package/dist/db/updateArtifacts.mjs +99 -2
- package/dist/handler.cjs +2 -0
- package/dist/handler.mjs +2 -0
- package/dist/handlerReleaseCatalogRoutes.cjs +37 -0
- package/dist/handlerReleaseCatalogRoutes.mjs +38 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +8 -8
package/dist/core/api.d.cts
CHANGED
|
@@ -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[]>;
|
package/dist/core/api.d.mts
CHANGED
|
@@ -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[]>;
|
package/dist/core/reads.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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. */
|
package/dist/core/reads.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
package/dist/package.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "1.0.
|
|
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/
|
|
54
|
-
"@hot-updater/
|
|
55
|
-
"@hot-updater/plugin-
|
|
56
|
-
"@hot-updater/plugin-
|
|
57
|
-
"@hot-updater/plugin-
|
|
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.
|
|
99
|
-
"@hot-updater/test-utils": "1.0.
|
|
98
|
+
"@hot-updater/standalone": "1.0.6",
|
|
99
|
+
"@hot-updater/test-utils": "1.0.6"
|
|
100
100
|
},
|
|
101
101
|
"nx": {
|
|
102
102
|
"implicitDependencies": [
|