@hot-updater/server 1.1.4 → 1.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/api.cjs +2 -0
- package/dist/core/api.d.cts +13 -0
- package/dist/core/api.d.mts +13 -0
- package/dist/core/api.mjs +2 -0
- package/dist/core/nativeBuilds.cjs +232 -0
- package/dist/core/nativeBuilds.mjs +229 -0
- package/dist/core/operations.cjs +7 -2
- package/dist/core/operations.mjs +7 -2
- package/dist/core/reads.cjs +3 -1
- package/dist/core/reads.mjs +3 -2
- package/dist/core/writes.cjs +16 -3
- package/dist/core/writes.mjs +16 -3
- package/dist/handlerAdminRoutes.cjs +102 -0
- package/dist/handlerAdminRoutes.mjs +103 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +8 -8
package/dist/core/api.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_reads = require("./reads.cjs");
|
|
2
|
+
const require_nativeBuilds = require("./nativeBuilds.cjs");
|
|
2
3
|
const require_operations = require("./operations.cjs");
|
|
3
4
|
//#region src/core/api.ts
|
|
4
5
|
/**
|
|
@@ -41,6 +42,7 @@ const createCoreApi = (db, storage, options = {}) => {
|
|
|
41
42
|
return {
|
|
42
43
|
...reads,
|
|
43
44
|
...require_operations.createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
|
|
45
|
+
...require_nativeBuilds.createNativeBuildApi(db, options),
|
|
44
46
|
/** One read, which checks the schema settings first. */
|
|
45
47
|
ready: async () => {
|
|
46
48
|
await reads.listReleaseCatalogs({ limit: 1 });
|
package/dist/core/api.d.cts
CHANGED
|
@@ -9,6 +9,19 @@ interface CoreApiOptions {
|
|
|
9
9
|
declare const createCoreApi: (db: CoreDatabase, storage: CoreStorage, options?: CoreApiOptions) => {
|
|
10
10
|
/** One read, which checks the schema settings first. */
|
|
11
11
|
ready: () => Promise<void>;
|
|
12
|
+
registerNativeBuild: (registration: import("@hot-updater/plugin-core").NativeBuildRegistration) => Promise<import("@hot-updater/plugin-core").NativeBuildRow>;
|
|
13
|
+
getNativeBuild: (id: string) => Promise<import("@hot-updater/plugin-core").NativeBuildRow | null>;
|
|
14
|
+
findNativeBuild: (scope: import("@hot-updater/plugin-core").NativeBuildScope, options?: {
|
|
15
|
+
readonly buildKey?: string;
|
|
16
|
+
}) => Promise<import("@hot-updater/plugin-core").NativeBuildRow | null>;
|
|
17
|
+
listNativeBuilds: (input: import("@hot-updater/plugin-core").KeysetInput & {
|
|
18
|
+
readonly channelId?: string;
|
|
19
|
+
}) => Promise<import("@hot-updater/plugin-core").NativeBuildRow[]>;
|
|
20
|
+
deleteNativeBuild: (id: string) => Promise<import("@hot-updater/plugin-core").NativeBuildDeleteResult>;
|
|
21
|
+
recordNativeBuildSubmission: (id: string, submission: {
|
|
22
|
+
readonly destination: import("@hot-updater/plugin-core").NativeBuildSubmissionDestination;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
}) => Promise<import("@hot-updater/plugin-core").NativeBuildRow | null>;
|
|
12
25
|
deploy: (deployments: readonly import("@hot-updater/plugin-core").Deployment[]) => Promise<import("@hot-updater/plugin-core").ReleaseCatalogMutationResult[]>;
|
|
13
26
|
updateReleasePolicy: (input: import("@hot-updater/plugin-core").ReleaseTarget & {
|
|
14
27
|
readonly patch: import("@hot-updater/plugin-core").ReleasePolicyPatch;
|
package/dist/core/api.d.mts
CHANGED
|
@@ -9,6 +9,19 @@ interface CoreApiOptions {
|
|
|
9
9
|
declare const createCoreApi: (db: CoreDatabase, storage: CoreStorage, options?: CoreApiOptions) => {
|
|
10
10
|
/** One read, which checks the schema settings first. */
|
|
11
11
|
ready: () => Promise<void>;
|
|
12
|
+
registerNativeBuild: (registration: import("@hot-updater/plugin-core").NativeBuildRegistration) => Promise<import("@hot-updater/plugin-core").NativeBuildRow>;
|
|
13
|
+
getNativeBuild: (id: string) => Promise<import("@hot-updater/plugin-core").NativeBuildRow | null>;
|
|
14
|
+
findNativeBuild: (scope: import("@hot-updater/plugin-core").NativeBuildScope, options?: {
|
|
15
|
+
readonly buildKey?: string;
|
|
16
|
+
}) => Promise<import("@hot-updater/plugin-core").NativeBuildRow | null>;
|
|
17
|
+
listNativeBuilds: (input: import("@hot-updater/plugin-core").KeysetInput & {
|
|
18
|
+
readonly channelId?: string;
|
|
19
|
+
}) => Promise<import("@hot-updater/plugin-core").NativeBuildRow[]>;
|
|
20
|
+
deleteNativeBuild: (id: string) => Promise<import("@hot-updater/plugin-core").NativeBuildDeleteResult>;
|
|
21
|
+
recordNativeBuildSubmission: (id: string, submission: {
|
|
22
|
+
readonly destination: import("@hot-updater/plugin-core").NativeBuildSubmissionDestination;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
}) => Promise<import("@hot-updater/plugin-core").NativeBuildRow | null>;
|
|
12
25
|
deploy: (deployments: readonly import("@hot-updater/plugin-core").Deployment[]) => Promise<import("@hot-updater/plugin-core").ReleaseCatalogMutationResult[]>;
|
|
13
26
|
updateReleasePolicy: (input: import("@hot-updater/plugin-core").ReleaseTarget & {
|
|
14
27
|
readonly patch: import("@hot-updater/plugin-core").ReleasePolicyPatch;
|
package/dist/core/api.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCoreReads } from "./reads.mjs";
|
|
2
|
+
import { createNativeBuildApi } from "./nativeBuilds.mjs";
|
|
2
3
|
import { createCoreOperations } from "./operations.mjs";
|
|
3
4
|
//#region src/core/api.ts
|
|
4
5
|
/**
|
|
@@ -41,6 +42,7 @@ const createCoreApi = (db, storage, options = {}) => {
|
|
|
41
42
|
return {
|
|
42
43
|
...reads,
|
|
43
44
|
...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
|
|
45
|
+
...createNativeBuildApi(db, options),
|
|
44
46
|
/** One read, which checks the schema settings first. */
|
|
45
47
|
ready: async () => {
|
|
46
48
|
await reads.listReleaseCatalogs({ limit: 1 });
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const require_reads = require("./reads.cjs");
|
|
2
|
+
const require_writes = require("./writes.cjs");
|
|
3
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
4
|
+
let _hot_updater_protocol = require("@hot-updater/protocol");
|
|
5
|
+
//#region src/core/nativeBuilds.ts
|
|
6
|
+
/** The artifacts each platform's build produces. */
|
|
7
|
+
const ARTIFACT_KINDS = {
|
|
8
|
+
android: ["apk", "aab"],
|
|
9
|
+
ios: ["ipa", "app"]
|
|
10
|
+
};
|
|
11
|
+
/** `native_builds.scope_key`'s bound in core's schema. */
|
|
12
|
+
const SCOPE_KEY_MAX_LENGTH = 2048;
|
|
13
|
+
/** The `git_commit_hash` bound of `bundles` and `native_builds`. */
|
|
14
|
+
const GIT_COMMIT_HASH_MAX_LENGTH = 255;
|
|
15
|
+
/**
|
|
16
|
+
* A native build key: `nbk1:` and a SHA-256, as the CLI computes it. The
|
|
17
|
+
* format is the CLI's convention, implemented here so the registry stores
|
|
18
|
+
* and looks up nothing else.
|
|
19
|
+
*/
|
|
20
|
+
const BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
|
|
21
|
+
/** A native build or scope core cannot store or look up: the caller's input. */
|
|
22
|
+
var NativeBuildInputError = class extends TypeError {
|
|
23
|
+
name = "NativeBuildInputError";
|
|
24
|
+
};
|
|
25
|
+
const pick = (row) => ({
|
|
26
|
+
id: row.id,
|
|
27
|
+
scope_key: row.scope_key,
|
|
28
|
+
platform: row.platform,
|
|
29
|
+
channel_id: row.channel_id,
|
|
30
|
+
fingerprint_hash: row.fingerprint_hash ?? null,
|
|
31
|
+
app_version: row.app_version ?? null,
|
|
32
|
+
min_bundle_id: row.min_bundle_id,
|
|
33
|
+
artifact_kind: row.artifact_kind,
|
|
34
|
+
artifact_storage_uri: row.artifact_storage_uri,
|
|
35
|
+
byte_size: row.byte_size,
|
|
36
|
+
git_commit_hash: row.git_commit_hash ?? null,
|
|
37
|
+
created_at_ms: row.created_at_ms,
|
|
38
|
+
build_key: row.build_key ?? null,
|
|
39
|
+
build_number: row.build_number ?? null,
|
|
40
|
+
submitted_to: row.submitted_to ?? null,
|
|
41
|
+
submitted_at_ms: row.submitted_at_ms ?? null,
|
|
42
|
+
submitted_version: row.submitted_version ?? null
|
|
43
|
+
});
|
|
44
|
+
/** The store track each platform's builds are submitted to. */
|
|
45
|
+
const SUBMISSION_DESTINATIONS = {
|
|
46
|
+
android: "play-internal",
|
|
47
|
+
ios: "testflight"
|
|
48
|
+
};
|
|
49
|
+
/** `native_builds.build_number`'s bound in core's schema. */
|
|
50
|
+
const BUILD_NUMBER_MAX_LENGTH = 64;
|
|
51
|
+
/** `native_builds.submitted_version`'s bound in core's schema. */
|
|
52
|
+
const SUBMITTED_VERSION_MAX_LENGTH = 255;
|
|
53
|
+
/**
|
|
54
|
+
* A native build scope's key: the Release Catalog scope key of a
|
|
55
|
+
* fingerprint, or of the app-version strategy followed by the exact app
|
|
56
|
+
* version the build compiled in. One key serves both strategies, so one
|
|
57
|
+
* index finds the newest build of either.
|
|
58
|
+
*/
|
|
59
|
+
const nativeBuildScopeKey = (scope) => {
|
|
60
|
+
let scopeKey;
|
|
61
|
+
try {
|
|
62
|
+
const channelKey = (0, _hot_updater_protocol.encodeChannelKey)(scope.channel);
|
|
63
|
+
scopeKey = scope.fingerprintHash === null ? `${(0, _hot_updater_protocol.createReleaseCatalogScopeKey)({
|
|
64
|
+
channelKey,
|
|
65
|
+
platform: scope.platform,
|
|
66
|
+
strategy: "APP_VERSION"
|
|
67
|
+
})}:${encodeURIComponent(scope.appVersion)}` : (0, _hot_updater_protocol.createReleaseCatalogScopeKey)({
|
|
68
|
+
channelKey,
|
|
69
|
+
fingerprintHash: scope.fingerprintHash,
|
|
70
|
+
platform: scope.platform,
|
|
71
|
+
strategy: "FINGERPRINT"
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
|
|
75
|
+
}
|
|
76
|
+
if (scopeKey.length > SCOPE_KEY_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build scope: its key is longer than ${SCOPE_KEY_MAX_LENGTH} characters; shorten the channel or app version.`);
|
|
77
|
+
return scopeKey;
|
|
78
|
+
};
|
|
79
|
+
/** Whether `bundleId` is a native build's built-in bundle: one unique read. */
|
|
80
|
+
const isBuiltInBundle = async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null;
|
|
81
|
+
const problemOf = ({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
|
|
82
|
+
if (!(0, _hot_updater_plugin_core.isUUIDv7)(id)) return "id must be a UUIDv7";
|
|
83
|
+
if (!(0, _hot_updater_plugin_core.isUUIDv7)(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
|
|
84
|
+
if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
|
|
85
|
+
if (channel.trim().length === 0) return "channel is required";
|
|
86
|
+
if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
|
|
87
|
+
if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
|
|
88
|
+
if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
|
|
89
|
+
if (buildNumber !== void 0 && buildNumber !== null && (buildNumber.trim().length === 0 || buildNumber.length > BUILD_NUMBER_MAX_LENGTH)) return `buildNumber must be the artifact's build number, up to ${BUILD_NUMBER_MAX_LENGTH} characters, or null`;
|
|
90
|
+
const kinds = ARTIFACT_KINDS[platform];
|
|
91
|
+
if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
|
|
92
|
+
if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
/** The native build registry: core's `native_builds` and the built-in bundle rows. */
|
|
96
|
+
const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
|
|
97
|
+
async registerNativeBuild(registration) {
|
|
98
|
+
const problem = problemOf(registration);
|
|
99
|
+
if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
|
|
100
|
+
const scopeKey = nativeBuildScopeKey(registration);
|
|
101
|
+
const channelId = `channel:${(0, _hot_updater_protocol.encodeChannelKey)(registration.channel)}`;
|
|
102
|
+
const { embeddedBundle } = registration;
|
|
103
|
+
const row = {
|
|
104
|
+
id: registration.id,
|
|
105
|
+
scope_key: scopeKey,
|
|
106
|
+
platform: registration.platform,
|
|
107
|
+
channel_id: channelId,
|
|
108
|
+
fingerprint_hash: registration.fingerprintHash,
|
|
109
|
+
app_version: registration.appVersion,
|
|
110
|
+
min_bundle_id: registration.minBundleId,
|
|
111
|
+
artifact_kind: registration.artifact.kind,
|
|
112
|
+
artifact_storage_uri: registration.artifact.storageUri,
|
|
113
|
+
byte_size: registration.artifact.byteSize,
|
|
114
|
+
git_commit_hash: registration.gitCommitHash,
|
|
115
|
+
created_at_ms: now(),
|
|
116
|
+
build_key: registration.buildKey,
|
|
117
|
+
build_number: registration.buildNumber ?? null,
|
|
118
|
+
submitted_to: null,
|
|
119
|
+
submitted_at_ms: null,
|
|
120
|
+
submitted_version: null
|
|
121
|
+
};
|
|
122
|
+
for (let attempt = 1;; attempt += 1) {
|
|
123
|
+
await require_writes.insertChannel(db, {
|
|
124
|
+
id: channelId,
|
|
125
|
+
name: registration.channel
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
await db.transaction(async (tx) => {
|
|
129
|
+
tx.create("bundles", {
|
|
130
|
+
id: registration.minBundleId,
|
|
131
|
+
platform: registration.platform,
|
|
132
|
+
git_commit_hash: registration.gitCommitHash,
|
|
133
|
+
metadata: embeddedBundle.metadata ?? {},
|
|
134
|
+
manifest_storage_uri: embeddedBundle.manifestStorageUri,
|
|
135
|
+
manifest_file_hash: embeddedBundle.manifestFileHash,
|
|
136
|
+
asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
|
|
137
|
+
});
|
|
138
|
+
tx.create("native_builds", row);
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (!(error instanceof _hot_updater_plugin_core.DatabaseConstraintError)) throw error;
|
|
143
|
+
if (error.reason === "exists" || error.reason === "unique") throw new _hot_updater_plugin_core.DatabaseConstraintError(error.reason, "native_builds");
|
|
144
|
+
if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return row;
|
|
149
|
+
},
|
|
150
|
+
async getNativeBuild(id) {
|
|
151
|
+
const row = await db.findOne("native_builds", { id });
|
|
152
|
+
return row === null ? null : pick(row);
|
|
153
|
+
},
|
|
154
|
+
async findNativeBuild(scope, options) {
|
|
155
|
+
const scopeKey = nativeBuildScopeKey(scope);
|
|
156
|
+
const buildKey = options?.buildKey;
|
|
157
|
+
if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
|
|
158
|
+
const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
|
|
159
|
+
index: "byScope",
|
|
160
|
+
where: { scope_key: scopeKey },
|
|
161
|
+
order: "desc",
|
|
162
|
+
limit: 1
|
|
163
|
+
}) : await db.findMany("native_builds", {
|
|
164
|
+
index: "byBuildKey",
|
|
165
|
+
where: {
|
|
166
|
+
scope_key: scopeKey,
|
|
167
|
+
build_key: buildKey
|
|
168
|
+
},
|
|
169
|
+
order: "desc",
|
|
170
|
+
limit: 1
|
|
171
|
+
});
|
|
172
|
+
return rows[0] === void 0 ? null : pick(rows[0]);
|
|
173
|
+
},
|
|
174
|
+
async recordNativeBuildSubmission(id, { destination, version }) {
|
|
175
|
+
if (version.trim().length === 0 || version.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
|
|
176
|
+
return db.transaction(async (tx) => {
|
|
177
|
+
const row = await tx.findOne("native_builds", { id });
|
|
178
|
+
if (row === null) return null;
|
|
179
|
+
const build = pick(row);
|
|
180
|
+
if (SUBMISSION_DESTINATIONS[build.platform] !== destination) throw new NativeBuildInputError(`Invalid native build submission: an ${build.platform} build is submitted to ${SUBMISSION_DESTINATIONS[build.platform]}, not ${destination}.`);
|
|
181
|
+
const set = {
|
|
182
|
+
submitted_to: destination,
|
|
183
|
+
submitted_at_ms: now(),
|
|
184
|
+
submitted_version: version
|
|
185
|
+
};
|
|
186
|
+
tx.update("native_builds", row, set);
|
|
187
|
+
return {
|
|
188
|
+
...build,
|
|
189
|
+
...set
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async deleteNativeBuild(id) {
|
|
194
|
+
return db.transaction(async (tx) => {
|
|
195
|
+
const row = await tx.findOne("native_builds", { id });
|
|
196
|
+
if (row === null) return {
|
|
197
|
+
deleted: false,
|
|
198
|
+
reason: "not_found"
|
|
199
|
+
};
|
|
200
|
+
await tx.delete("native_builds", row);
|
|
201
|
+
const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
|
|
202
|
+
if (bundle !== null) await tx.delete("bundles", bundle);
|
|
203
|
+
return {
|
|
204
|
+
deleted: true,
|
|
205
|
+
nativeBuild: pick(row)
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
async listNativeBuilds(input) {
|
|
210
|
+
const page = {
|
|
211
|
+
order: input.order ?? "asc",
|
|
212
|
+
limit: input.limit
|
|
213
|
+
};
|
|
214
|
+
const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
|
|
215
|
+
index: "all",
|
|
216
|
+
where: {},
|
|
217
|
+
...page,
|
|
218
|
+
...require_reads.after(input)
|
|
219
|
+
}) : await db.findMany("native_builds", {
|
|
220
|
+
index: "byChannel",
|
|
221
|
+
where: { channel_id: input.channelId },
|
|
222
|
+
...page,
|
|
223
|
+
...require_reads.after(input)
|
|
224
|
+
});
|
|
225
|
+
return rows.map(pick);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
//#endregion
|
|
229
|
+
exports.NativeBuildInputError = NativeBuildInputError;
|
|
230
|
+
exports.createNativeBuildApi = createNativeBuildApi;
|
|
231
|
+
exports.isBuiltInBundle = isBuiltInBundle;
|
|
232
|
+
exports.nativeBuildScopeKey = nativeBuildScopeKey;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { after } from "./reads.mjs";
|
|
2
|
+
import { insertChannel } from "./writes.mjs";
|
|
3
|
+
import { DatabaseConstraintError, isUUIDv7 } from "@hot-updater/plugin-core";
|
|
4
|
+
import { createReleaseCatalogScopeKey, encodeChannelKey } from "@hot-updater/protocol";
|
|
5
|
+
//#region src/core/nativeBuilds.ts
|
|
6
|
+
/** The artifacts each platform's build produces. */
|
|
7
|
+
const ARTIFACT_KINDS = {
|
|
8
|
+
android: ["apk", "aab"],
|
|
9
|
+
ios: ["ipa", "app"]
|
|
10
|
+
};
|
|
11
|
+
/** `native_builds.scope_key`'s bound in core's schema. */
|
|
12
|
+
const SCOPE_KEY_MAX_LENGTH = 2048;
|
|
13
|
+
/** The `git_commit_hash` bound of `bundles` and `native_builds`. */
|
|
14
|
+
const GIT_COMMIT_HASH_MAX_LENGTH = 255;
|
|
15
|
+
/**
|
|
16
|
+
* A native build key: `nbk1:` and a SHA-256, as the CLI computes it. The
|
|
17
|
+
* format is the CLI's convention, implemented here so the registry stores
|
|
18
|
+
* and looks up nothing else.
|
|
19
|
+
*/
|
|
20
|
+
const BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
|
|
21
|
+
/** A native build or scope core cannot store or look up: the caller's input. */
|
|
22
|
+
var NativeBuildInputError = class extends TypeError {
|
|
23
|
+
name = "NativeBuildInputError";
|
|
24
|
+
};
|
|
25
|
+
const pick = (row) => ({
|
|
26
|
+
id: row.id,
|
|
27
|
+
scope_key: row.scope_key,
|
|
28
|
+
platform: row.platform,
|
|
29
|
+
channel_id: row.channel_id,
|
|
30
|
+
fingerprint_hash: row.fingerprint_hash ?? null,
|
|
31
|
+
app_version: row.app_version ?? null,
|
|
32
|
+
min_bundle_id: row.min_bundle_id,
|
|
33
|
+
artifact_kind: row.artifact_kind,
|
|
34
|
+
artifact_storage_uri: row.artifact_storage_uri,
|
|
35
|
+
byte_size: row.byte_size,
|
|
36
|
+
git_commit_hash: row.git_commit_hash ?? null,
|
|
37
|
+
created_at_ms: row.created_at_ms,
|
|
38
|
+
build_key: row.build_key ?? null,
|
|
39
|
+
build_number: row.build_number ?? null,
|
|
40
|
+
submitted_to: row.submitted_to ?? null,
|
|
41
|
+
submitted_at_ms: row.submitted_at_ms ?? null,
|
|
42
|
+
submitted_version: row.submitted_version ?? null
|
|
43
|
+
});
|
|
44
|
+
/** The store track each platform's builds are submitted to. */
|
|
45
|
+
const SUBMISSION_DESTINATIONS = {
|
|
46
|
+
android: "play-internal",
|
|
47
|
+
ios: "testflight"
|
|
48
|
+
};
|
|
49
|
+
/** `native_builds.build_number`'s bound in core's schema. */
|
|
50
|
+
const BUILD_NUMBER_MAX_LENGTH = 64;
|
|
51
|
+
/** `native_builds.submitted_version`'s bound in core's schema. */
|
|
52
|
+
const SUBMITTED_VERSION_MAX_LENGTH = 255;
|
|
53
|
+
/**
|
|
54
|
+
* A native build scope's key: the Release Catalog scope key of a
|
|
55
|
+
* fingerprint, or of the app-version strategy followed by the exact app
|
|
56
|
+
* version the build compiled in. One key serves both strategies, so one
|
|
57
|
+
* index finds the newest build of either.
|
|
58
|
+
*/
|
|
59
|
+
const nativeBuildScopeKey = (scope) => {
|
|
60
|
+
let scopeKey;
|
|
61
|
+
try {
|
|
62
|
+
const channelKey = encodeChannelKey(scope.channel);
|
|
63
|
+
scopeKey = scope.fingerprintHash === null ? `${createReleaseCatalogScopeKey({
|
|
64
|
+
channelKey,
|
|
65
|
+
platform: scope.platform,
|
|
66
|
+
strategy: "APP_VERSION"
|
|
67
|
+
})}:${encodeURIComponent(scope.appVersion)}` : createReleaseCatalogScopeKey({
|
|
68
|
+
channelKey,
|
|
69
|
+
fingerprintHash: scope.fingerprintHash,
|
|
70
|
+
platform: scope.platform,
|
|
71
|
+
strategy: "FINGERPRINT"
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
|
|
75
|
+
}
|
|
76
|
+
if (scopeKey.length > SCOPE_KEY_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build scope: its key is longer than ${SCOPE_KEY_MAX_LENGTH} characters; shorten the channel or app version.`);
|
|
77
|
+
return scopeKey;
|
|
78
|
+
};
|
|
79
|
+
/** Whether `bundleId` is a native build's built-in bundle: one unique read. */
|
|
80
|
+
const isBuiltInBundle = async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null;
|
|
81
|
+
const problemOf = ({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
|
|
82
|
+
if (!isUUIDv7(id)) return "id must be a UUIDv7";
|
|
83
|
+
if (!isUUIDv7(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
|
|
84
|
+
if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
|
|
85
|
+
if (channel.trim().length === 0) return "channel is required";
|
|
86
|
+
if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
|
|
87
|
+
if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
|
|
88
|
+
if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
|
|
89
|
+
if (buildNumber !== void 0 && buildNumber !== null && (buildNumber.trim().length === 0 || buildNumber.length > BUILD_NUMBER_MAX_LENGTH)) return `buildNumber must be the artifact's build number, up to ${BUILD_NUMBER_MAX_LENGTH} characters, or null`;
|
|
90
|
+
const kinds = ARTIFACT_KINDS[platform];
|
|
91
|
+
if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
|
|
92
|
+
if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
/** The native build registry: core's `native_builds` and the built-in bundle rows. */
|
|
96
|
+
const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
|
|
97
|
+
async registerNativeBuild(registration) {
|
|
98
|
+
const problem = problemOf(registration);
|
|
99
|
+
if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
|
|
100
|
+
const scopeKey = nativeBuildScopeKey(registration);
|
|
101
|
+
const channelId = `channel:${encodeChannelKey(registration.channel)}`;
|
|
102
|
+
const { embeddedBundle } = registration;
|
|
103
|
+
const row = {
|
|
104
|
+
id: registration.id,
|
|
105
|
+
scope_key: scopeKey,
|
|
106
|
+
platform: registration.platform,
|
|
107
|
+
channel_id: channelId,
|
|
108
|
+
fingerprint_hash: registration.fingerprintHash,
|
|
109
|
+
app_version: registration.appVersion,
|
|
110
|
+
min_bundle_id: registration.minBundleId,
|
|
111
|
+
artifact_kind: registration.artifact.kind,
|
|
112
|
+
artifact_storage_uri: registration.artifact.storageUri,
|
|
113
|
+
byte_size: registration.artifact.byteSize,
|
|
114
|
+
git_commit_hash: registration.gitCommitHash,
|
|
115
|
+
created_at_ms: now(),
|
|
116
|
+
build_key: registration.buildKey,
|
|
117
|
+
build_number: registration.buildNumber ?? null,
|
|
118
|
+
submitted_to: null,
|
|
119
|
+
submitted_at_ms: null,
|
|
120
|
+
submitted_version: null
|
|
121
|
+
};
|
|
122
|
+
for (let attempt = 1;; attempt += 1) {
|
|
123
|
+
await insertChannel(db, {
|
|
124
|
+
id: channelId,
|
|
125
|
+
name: registration.channel
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
await db.transaction(async (tx) => {
|
|
129
|
+
tx.create("bundles", {
|
|
130
|
+
id: registration.minBundleId,
|
|
131
|
+
platform: registration.platform,
|
|
132
|
+
git_commit_hash: registration.gitCommitHash,
|
|
133
|
+
metadata: embeddedBundle.metadata ?? {},
|
|
134
|
+
manifest_storage_uri: embeddedBundle.manifestStorageUri,
|
|
135
|
+
manifest_file_hash: embeddedBundle.manifestFileHash,
|
|
136
|
+
asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
|
|
137
|
+
});
|
|
138
|
+
tx.create("native_builds", row);
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (!(error instanceof DatabaseConstraintError)) throw error;
|
|
143
|
+
if (error.reason === "exists" || error.reason === "unique") throw new DatabaseConstraintError(error.reason, "native_builds");
|
|
144
|
+
if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return row;
|
|
149
|
+
},
|
|
150
|
+
async getNativeBuild(id) {
|
|
151
|
+
const row = await db.findOne("native_builds", { id });
|
|
152
|
+
return row === null ? null : pick(row);
|
|
153
|
+
},
|
|
154
|
+
async findNativeBuild(scope, options) {
|
|
155
|
+
const scopeKey = nativeBuildScopeKey(scope);
|
|
156
|
+
const buildKey = options?.buildKey;
|
|
157
|
+
if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
|
|
158
|
+
const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
|
|
159
|
+
index: "byScope",
|
|
160
|
+
where: { scope_key: scopeKey },
|
|
161
|
+
order: "desc",
|
|
162
|
+
limit: 1
|
|
163
|
+
}) : await db.findMany("native_builds", {
|
|
164
|
+
index: "byBuildKey",
|
|
165
|
+
where: {
|
|
166
|
+
scope_key: scopeKey,
|
|
167
|
+
build_key: buildKey
|
|
168
|
+
},
|
|
169
|
+
order: "desc",
|
|
170
|
+
limit: 1
|
|
171
|
+
});
|
|
172
|
+
return rows[0] === void 0 ? null : pick(rows[0]);
|
|
173
|
+
},
|
|
174
|
+
async recordNativeBuildSubmission(id, { destination, version }) {
|
|
175
|
+
if (version.trim().length === 0 || version.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
|
|
176
|
+
return db.transaction(async (tx) => {
|
|
177
|
+
const row = await tx.findOne("native_builds", { id });
|
|
178
|
+
if (row === null) return null;
|
|
179
|
+
const build = pick(row);
|
|
180
|
+
if (SUBMISSION_DESTINATIONS[build.platform] !== destination) throw new NativeBuildInputError(`Invalid native build submission: an ${build.platform} build is submitted to ${SUBMISSION_DESTINATIONS[build.platform]}, not ${destination}.`);
|
|
181
|
+
const set = {
|
|
182
|
+
submitted_to: destination,
|
|
183
|
+
submitted_at_ms: now(),
|
|
184
|
+
submitted_version: version
|
|
185
|
+
};
|
|
186
|
+
tx.update("native_builds", row, set);
|
|
187
|
+
return {
|
|
188
|
+
...build,
|
|
189
|
+
...set
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async deleteNativeBuild(id) {
|
|
194
|
+
return db.transaction(async (tx) => {
|
|
195
|
+
const row = await tx.findOne("native_builds", { id });
|
|
196
|
+
if (row === null) return {
|
|
197
|
+
deleted: false,
|
|
198
|
+
reason: "not_found"
|
|
199
|
+
};
|
|
200
|
+
await tx.delete("native_builds", row);
|
|
201
|
+
const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
|
|
202
|
+
if (bundle !== null) await tx.delete("bundles", bundle);
|
|
203
|
+
return {
|
|
204
|
+
deleted: true,
|
|
205
|
+
nativeBuild: pick(row)
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
async listNativeBuilds(input) {
|
|
210
|
+
const page = {
|
|
211
|
+
order: input.order ?? "asc",
|
|
212
|
+
limit: input.limit
|
|
213
|
+
};
|
|
214
|
+
const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
|
|
215
|
+
index: "all",
|
|
216
|
+
where: {},
|
|
217
|
+
...page,
|
|
218
|
+
...after(input)
|
|
219
|
+
}) : await db.findMany("native_builds", {
|
|
220
|
+
index: "byChannel",
|
|
221
|
+
where: { channel_id: input.channelId },
|
|
222
|
+
...page,
|
|
223
|
+
...after(input)
|
|
224
|
+
});
|
|
225
|
+
return rows.map(pick);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
//#endregion
|
|
229
|
+
export { NativeBuildInputError, createNativeBuildApi, isBuiltInBundle, nativeBuildScopeKey };
|
package/dist/core/operations.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_reads = require("./reads.cjs");
|
|
2
2
|
const require_writes = require("./writes.cjs");
|
|
3
|
+
const require_nativeBuilds = require("./nativeBuilds.cjs");
|
|
3
4
|
const require_releases = require("./releases.cjs");
|
|
4
5
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
5
6
|
let _hot_updater_protocol = require("@hot-updater/protocol");
|
|
@@ -131,6 +132,7 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
131
132
|
if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
|
|
132
133
|
const row = await db.findOne("bundles", { id: deployment.bundleId });
|
|
133
134
|
if (row === null) throw new _hot_updater_plugin_core.DatabaseBundleNotFoundError(deployment.bundleId);
|
|
135
|
+
if (await require_nativeBuilds.isBuiltInBundle(db, deployment.bundleId)) throw new _hot_updater_plugin_core.ReleaseManagementError("TARGET_RELEASE_INVALID", `Bundle "${deployment.bundleId}" is a native build's built-in bundle, which no release publishes.`);
|
|
134
136
|
platforms.set(deployment.bundleId, require_reads.toBundleRow(row).platform);
|
|
135
137
|
}
|
|
136
138
|
const updatedAtMs = now();
|
|
@@ -326,7 +328,8 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
326
328
|
id
|
|
327
329
|
};
|
|
328
330
|
const { id: _id, ...set } = (0, _hot_updater_plugin_core.bundleToRow)(next);
|
|
329
|
-
|
|
331
|
+
const counted = set.platform === current.platform || !await require_nativeBuilds.isBuiltInBundle(tx, id);
|
|
332
|
+
require_writes.updateBundle(tx, current, set, { counted });
|
|
330
333
|
if (patches !== void 0) await require_writes.replaceBundlePatches(tx, current, (0, _hot_updater_plugin_core.bundleToPatchRows)({
|
|
331
334
|
...next,
|
|
332
335
|
patches
|
|
@@ -337,7 +340,9 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
337
340
|
await db.transaction(async (tx) => {
|
|
338
341
|
for (const id of new Set(ids)) {
|
|
339
342
|
const current = await tx.findOne("bundles", { id });
|
|
340
|
-
if (current
|
|
343
|
+
if (current === null) continue;
|
|
344
|
+
if (await require_nativeBuilds.isBuiltInBundle(tx, id)) throw new _hot_updater_plugin_core.DatabaseRowReferencedError();
|
|
345
|
+
await require_writes.deleteBundle(tx, current);
|
|
341
346
|
}
|
|
342
347
|
});
|
|
343
348
|
} catch (error) {
|
package/dist/core/operations.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toBundleRow, toReleaseRow } from "./reads.mjs";
|
|
2
2
|
import { deleteBundle, deleteChannel, insertChannel, replaceBundlePatches, updateBundle } from "./writes.mjs";
|
|
3
|
+
import { isBuiltInBundle } from "./nativeBuilds.mjs";
|
|
3
4
|
import { changeRelease, changeReleases, readCatalog, rebuildScope } from "./releases.mjs";
|
|
4
5
|
import { DatabaseBundleNotFoundError, DatabaseConstraintError, DatabaseRowReferencedError, ReleaseCatalogMutationError, ReleaseManagementError, bundleToPatchRows, bundleToRow, createUUIDv7After, isUUIDv7, rowToBundle } from "@hot-updater/plugin-core";
|
|
5
6
|
import { createReleaseCatalogScopeKey, decodeChannelKey, encodeChannelKey } from "@hot-updater/protocol";
|
|
@@ -131,6 +132,7 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
131
132
|
if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
|
|
132
133
|
const row = await db.findOne("bundles", { id: deployment.bundleId });
|
|
133
134
|
if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
|
|
135
|
+
if (await isBuiltInBundle(db, deployment.bundleId)) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", `Bundle "${deployment.bundleId}" is a native build's built-in bundle, which no release publishes.`);
|
|
134
136
|
platforms.set(deployment.bundleId, toBundleRow(row).platform);
|
|
135
137
|
}
|
|
136
138
|
const updatedAtMs = now();
|
|
@@ -326,7 +328,8 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
326
328
|
id
|
|
327
329
|
};
|
|
328
330
|
const { id: _id, ...set } = bundleToRow(next);
|
|
329
|
-
|
|
331
|
+
const counted = set.platform === current.platform || !await isBuiltInBundle(tx, id);
|
|
332
|
+
updateBundle(tx, current, set, { counted });
|
|
330
333
|
if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
|
|
331
334
|
...next,
|
|
332
335
|
patches
|
|
@@ -337,7 +340,9 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
337
340
|
await db.transaction(async (tx) => {
|
|
338
341
|
for (const id of new Set(ids)) {
|
|
339
342
|
const current = await tx.findOne("bundles", { id });
|
|
340
|
-
if (current
|
|
343
|
+
if (current === null) continue;
|
|
344
|
+
if (await isBuiltInBundle(tx, id)) throw new DatabaseRowReferencedError();
|
|
345
|
+
await deleteBundle(tx, current);
|
|
341
346
|
}
|
|
342
347
|
});
|
|
343
348
|
} catch (error) {
|
package/dist/core/reads.cjs
CHANGED
|
@@ -73,6 +73,8 @@ const drain = async (read) => {
|
|
|
73
73
|
} while (cursor !== void 0);
|
|
74
74
|
return rows;
|
|
75
75
|
};
|
|
76
|
+
/** A keyset page's range: the keys strictly after `after`, in `order`. */
|
|
77
|
+
const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
|
|
76
78
|
/** Core's reads, each through one declared index or key. */
|
|
77
79
|
const createCoreReads = (db, storage) => {
|
|
78
80
|
/** A bundle row's patches, exactly as many as its reference counter holds. */
|
|
@@ -97,7 +99,6 @@ const createCoreReads = (db, storage) => {
|
|
|
97
99
|
childCount: row._refs_bundle_patches_base_bundle_id ?? 0
|
|
98
100
|
};
|
|
99
101
|
};
|
|
100
|
-
const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
|
|
101
102
|
return {
|
|
102
103
|
/** The update check: one point read of its scope's catalog. */
|
|
103
104
|
async getReleaseCatalog(input) {
|
|
@@ -278,6 +279,7 @@ const createCoreReads = (db, storage) => {
|
|
|
278
279
|
};
|
|
279
280
|
};
|
|
280
281
|
//#endregion
|
|
282
|
+
exports.after = after;
|
|
281
283
|
exports.createCoreReads = createCoreReads;
|
|
282
284
|
exports.drain = drain;
|
|
283
285
|
exports.toBundleRow = toBundleRow;
|
package/dist/core/reads.mjs
CHANGED
|
@@ -73,6 +73,8 @@ const drain = async (read) => {
|
|
|
73
73
|
} while (cursor !== void 0);
|
|
74
74
|
return rows;
|
|
75
75
|
};
|
|
76
|
+
/** A keyset page's range: the keys strictly after `after`, in `order`. */
|
|
77
|
+
const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
|
|
76
78
|
/** Core's reads, each through one declared index or key. */
|
|
77
79
|
const createCoreReads = (db, storage) => {
|
|
78
80
|
/** A bundle row's patches, exactly as many as its reference counter holds. */
|
|
@@ -97,7 +99,6 @@ const createCoreReads = (db, storage) => {
|
|
|
97
99
|
childCount: row._refs_bundle_patches_base_bundle_id ?? 0
|
|
98
100
|
};
|
|
99
101
|
};
|
|
100
|
-
const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
|
|
101
102
|
return {
|
|
102
103
|
/** The update check: one point read of its scope's catalog. */
|
|
103
104
|
async getReleaseCatalog(input) {
|
|
@@ -278,4 +279,4 @@ const createCoreReads = (db, storage) => {
|
|
|
278
279
|
};
|
|
279
280
|
};
|
|
280
281
|
//#endregion
|
|
281
|
-
export { createCoreReads, drain, toBundleRow, toCatalogRow, toChannelRow, toPatchRow, toReleaseRow };
|
|
282
|
+
export { after, createCoreReads, drain, toBundleRow, toCatalogRow, toChannelRow, toPatchRow, toReleaseRow };
|
package/dist/core/writes.cjs
CHANGED
|
@@ -10,9 +10,13 @@ const insertBundle = (tx, row) => {
|
|
|
10
10
|
tx.create("bundles", row);
|
|
11
11
|
countBundle(tx, row.platform, 1);
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Updates a bundle; a platform change moves it between platform totals,
|
|
15
|
+
* except for a native build's built-in bundle, which they never count.
|
|
16
|
+
*/
|
|
17
|
+
const updateBundle = (tx, current, set, { counted = true } = {}) => {
|
|
14
18
|
tx.update("bundles", current, set);
|
|
15
|
-
if (set.platform !== void 0 && set.platform !== current.platform) {
|
|
19
|
+
if (counted && set.platform !== void 0 && set.platform !== current.platform) {
|
|
16
20
|
countBundle(tx, current.platform, -1);
|
|
17
21
|
countBundle(tx, set.platform, 1);
|
|
18
22
|
}
|
|
@@ -73,7 +77,7 @@ const insertChannel = async (db, row) => {
|
|
|
73
77
|
throw error;
|
|
74
78
|
}
|
|
75
79
|
};
|
|
76
|
-
/** Deletes a channel no release uses: 1 read
|
|
80
|
+
/** Deletes a channel no release or native build uses: 1 read, 1 query, 1 write. */
|
|
77
81
|
const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
78
82
|
const row = await tx.findOne("channels", { id });
|
|
79
83
|
if (row === null) return {
|
|
@@ -84,6 +88,15 @@ const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
|
84
88
|
deleted: false,
|
|
85
89
|
reason: "not_empty"
|
|
86
90
|
};
|
|
91
|
+
const { rows: builds } = await tx.findMany("native_builds", {
|
|
92
|
+
index: "byChannel",
|
|
93
|
+
where: { channel_id: id },
|
|
94
|
+
limit: 1
|
|
95
|
+
});
|
|
96
|
+
if (builds.length > 0) return {
|
|
97
|
+
deleted: false,
|
|
98
|
+
reason: "not_empty"
|
|
99
|
+
};
|
|
87
100
|
await tx.delete("channels", row);
|
|
88
101
|
return { deleted: true };
|
|
89
102
|
});
|
package/dist/core/writes.mjs
CHANGED
|
@@ -10,9 +10,13 @@ const insertBundle = (tx, row) => {
|
|
|
10
10
|
tx.create("bundles", row);
|
|
11
11
|
countBundle(tx, row.platform, 1);
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Updates a bundle; a platform change moves it between platform totals,
|
|
15
|
+
* except for a native build's built-in bundle, which they never count.
|
|
16
|
+
*/
|
|
17
|
+
const updateBundle = (tx, current, set, { counted = true } = {}) => {
|
|
14
18
|
tx.update("bundles", current, set);
|
|
15
|
-
if (set.platform !== void 0 && set.platform !== current.platform) {
|
|
19
|
+
if (counted && set.platform !== void 0 && set.platform !== current.platform) {
|
|
16
20
|
countBundle(tx, current.platform, -1);
|
|
17
21
|
countBundle(tx, set.platform, 1);
|
|
18
22
|
}
|
|
@@ -73,7 +77,7 @@ const insertChannel = async (db, row) => {
|
|
|
73
77
|
throw error;
|
|
74
78
|
}
|
|
75
79
|
};
|
|
76
|
-
/** Deletes a channel no release uses: 1 read
|
|
80
|
+
/** Deletes a channel no release or native build uses: 1 read, 1 query, 1 write. */
|
|
77
81
|
const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
78
82
|
const row = await tx.findOne("channels", { id });
|
|
79
83
|
if (row === null) return {
|
|
@@ -84,6 +88,15 @@ const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
|
84
88
|
deleted: false,
|
|
85
89
|
reason: "not_empty"
|
|
86
90
|
};
|
|
91
|
+
const { rows: builds } = await tx.findMany("native_builds", {
|
|
92
|
+
index: "byChannel",
|
|
93
|
+
where: { channel_id: id },
|
|
94
|
+
limit: 1
|
|
95
|
+
});
|
|
96
|
+
if (builds.length > 0) return {
|
|
97
|
+
deleted: false,
|
|
98
|
+
reason: "not_empty"
|
|
99
|
+
};
|
|
87
100
|
await tx.delete("channels", row);
|
|
88
101
|
return { deleted: true };
|
|
89
102
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_nativeBuilds = require("./core/nativeBuilds.cjs");
|
|
1
2
|
const require_handlerErrors = require("./handlerErrors.cjs");
|
|
2
3
|
const require_handlerParameters = require("./handlerParameters.cjs");
|
|
3
4
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
@@ -27,6 +28,10 @@ const answer = async (operation) => {
|
|
|
27
28
|
code: "REFERENCED",
|
|
28
29
|
error: error.message
|
|
29
30
|
}, 409);
|
|
31
|
+
if (error instanceof _hot_updater_plugin_core.DatabaseConstraintError && error.model === "native_builds") return json({
|
|
32
|
+
code: "NATIVE_BUILD_EXISTS",
|
|
33
|
+
error: "A native build with this ID or built-in bundle ID is already registered."
|
|
34
|
+
}, 409);
|
|
30
35
|
throw error;
|
|
31
36
|
}
|
|
32
37
|
};
|
|
@@ -129,6 +134,42 @@ const bundleUpdateOf = (value, bundleId) => {
|
|
|
129
134
|
if (id !== void 0 && id !== bundleId) throw new require_handlerErrors.HandlerBadRequestError("Bundle id mismatch");
|
|
130
135
|
return update;
|
|
131
136
|
};
|
|
137
|
+
const NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
|
|
138
|
+
"apk",
|
|
139
|
+
"aab",
|
|
140
|
+
"ipa",
|
|
141
|
+
"app"
|
|
142
|
+
]);
|
|
143
|
+
/** A native build's scope: `fingerprintHash` or `appVersion`, never both. */
|
|
144
|
+
const nativeBuildScopeOf = (url) => {
|
|
145
|
+
const params = url.searchParams;
|
|
146
|
+
const platform = platformOf(url);
|
|
147
|
+
const channel = params.get("channel");
|
|
148
|
+
const fingerprintHash = params.get("fingerprintHash");
|
|
149
|
+
const appVersion = params.get("appVersion");
|
|
150
|
+
if (platform === void 0 || !isText(channel) || fingerprintHash === null === (appVersion === null) || fingerprintHash === "" || appVersion === "") throw new require_handlerErrors.HandlerBadRequestError("A native build lookup needs platform, channel, and fingerprintHash or appVersion.");
|
|
151
|
+
return fingerprintHash !== null ? {
|
|
152
|
+
platform,
|
|
153
|
+
channel,
|
|
154
|
+
fingerprintHash,
|
|
155
|
+
appVersion: null
|
|
156
|
+
} : {
|
|
157
|
+
platform,
|
|
158
|
+
channel,
|
|
159
|
+
fingerprintHash: null,
|
|
160
|
+
appVersion
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
/** A native build or scope core refuses: a bad request. */
|
|
164
|
+
const refusingInvalid = async (operation) => {
|
|
165
|
+
try {
|
|
166
|
+
return await operation();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof require_nativeBuilds.NativeBuildInputError) throw new require_handlerErrors.HandlerBadRequestError(error.message);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const isNativeBuildRegistration = (value) => isRecord(value) && isText(value.id) && isText(value.minBundleId) && typeof value.platform === "string" && require_handlerParameters.isPlatform(value.platform) && isText(value.channel) && isTextOrNull(value.fingerprintHash) && isTextOrNull(value.appVersion) && value.fingerprintHash === null !== (value.appVersion === null) && isTextOrNull(value.gitCommitHash) && isTextOrNull(value.buildKey) && (value.buildNumber === void 0 || isTextOrNull(value.buildNumber)) && isRecord(value.artifact) && typeof value.artifact.kind === "string" && NATIVE_BUILD_ARTIFACT_KINDS.has(value.artifact.kind) && isText(value.artifact.storageUri) && Number.isSafeInteger(value.artifact.byteSize) && value.artifact.byteSize >= 0 && isRecord(value.embeddedBundle) && isText(value.embeddedBundle.manifestStorageUri) && isText(value.embeddedBundle.manifestFileHash) && isText(value.embeddedBundle.assetBaseStorageUri) && (value.embeddedBundle.metadata === void 0 || isRecord(value.embeddedBundle.metadata));
|
|
132
173
|
const scopeKeyOf = (params) => require_handlerParameters.decodeMaybe(require_handlerParameters.requireRouteParam(params, "scopeKey")) ?? "";
|
|
133
174
|
/** Every admin route's handler, by the name `ADMIN_ROUTES` mounts it under. */
|
|
134
175
|
const createAdminRouteHandlers = () => ({
|
|
@@ -248,6 +289,37 @@ const createAdminRouteHandlers = () => ({
|
|
|
248
289
|
}) });
|
|
249
290
|
}),
|
|
250
291
|
preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
|
|
292
|
+
registerNativeBuild: onCore(async (core, request) => {
|
|
293
|
+
const input = await body(request);
|
|
294
|
+
if (!isNativeBuildRegistration(input)) throw new require_handlerErrors.HandlerBadRequestError("Invalid native build");
|
|
295
|
+
return json({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
|
|
296
|
+
}),
|
|
297
|
+
findNativeBuild: onCore(async (core, request) => {
|
|
298
|
+
const url = new URL(request.url);
|
|
299
|
+
const scope = nativeBuildScopeOf(url);
|
|
300
|
+
const buildKey = url.searchParams.get("buildKey");
|
|
301
|
+
return json({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
|
|
302
|
+
}),
|
|
303
|
+
getNativeBuild: onCore(async (core, _request, params) => json({ data: await core.getNativeBuild(require_handlerParameters.requireRouteParam(params, "id")) })),
|
|
304
|
+
listNativeBuilds: onCore(async (core, request) => {
|
|
305
|
+
const url = new URL(request.url);
|
|
306
|
+
const input = keyset(url);
|
|
307
|
+
const channelId = url.searchParams.get("channelId");
|
|
308
|
+
return page(await core.listNativeBuilds({
|
|
309
|
+
...input,
|
|
310
|
+
...channelId === null ? {} : { channelId }
|
|
311
|
+
}), input, ({ id }) => id);
|
|
312
|
+
}),
|
|
313
|
+
recordNativeBuildSubmission: onCore(async (core, request, params) => {
|
|
314
|
+
const input = await body(request);
|
|
315
|
+
if (!isRecord(input) || input.destination !== "testflight" && input.destination !== "play-internal" || !isText(input.version)) throw new require_handlerErrors.HandlerBadRequestError("A native build submission needs destination (testflight or play-internal) and version.");
|
|
316
|
+
const submission = {
|
|
317
|
+
destination: input.destination,
|
|
318
|
+
version: input.version
|
|
319
|
+
};
|
|
320
|
+
return json({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(require_handlerParameters.requireRouteParam(params, "id"), submission)) });
|
|
321
|
+
}),
|
|
322
|
+
deleteNativeBuild: onCore(async (core, _request, params) => json({ data: await core.deleteNativeBuild(require_handlerParameters.requireRouteParam(params, "id")) })),
|
|
251
323
|
deleteBundles: onCore(async (core, request) => {
|
|
252
324
|
const { ids } = await body(request);
|
|
253
325
|
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new require_handlerErrors.HandlerBadRequestError("Bundle deletion needs ids");
|
|
@@ -366,6 +438,36 @@ const ADMIN_ROUTES = [
|
|
|
366
438
|
method: "GET",
|
|
367
439
|
path: "/base-candidates/:candidateKey",
|
|
368
440
|
handler: "findBaseCandidates"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
method: "POST",
|
|
444
|
+
path: "/native-builds",
|
|
445
|
+
handler: "registerNativeBuild"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
method: "GET",
|
|
449
|
+
path: "/native-builds/latest",
|
|
450
|
+
handler: "findNativeBuild"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
method: "GET",
|
|
454
|
+
path: "/native-builds/:id",
|
|
455
|
+
handler: "getNativeBuild"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
method: "GET",
|
|
459
|
+
path: "/native-builds",
|
|
460
|
+
handler: "listNativeBuilds"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: "/native-builds/:id/submission",
|
|
465
|
+
handler: "recordNativeBuildSubmission"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
method: "DELETE",
|
|
469
|
+
path: "/native-builds/:id",
|
|
470
|
+
handler: "deleteNativeBuild"
|
|
369
471
|
}
|
|
370
472
|
];
|
|
371
473
|
//#endregion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { NativeBuildInputError } from "./core/nativeBuilds.mjs";
|
|
1
2
|
import { HandlerBadRequestError } from "./handlerErrors.mjs";
|
|
2
3
|
import { decodeMaybe, isPlatform, parseBooleanSearchParam, requireRouteParam } from "./handlerParameters.mjs";
|
|
3
|
-
import { DatabaseBundleNotFoundError, DatabaseRowReferencedError, ReleaseCatalogMutationError, ReleaseManagementError } from "@hot-updater/plugin-core";
|
|
4
|
+
import { DatabaseBundleNotFoundError, DatabaseConstraintError, DatabaseRowReferencedError, ReleaseCatalogMutationError, ReleaseManagementError } from "@hot-updater/plugin-core";
|
|
4
5
|
//#region src/handlerAdminRoutes.ts
|
|
5
6
|
const json = (body, status = 200) => Response.json(body, { status });
|
|
6
7
|
const notFound = () => json({ error: "Not found" }, 404);
|
|
@@ -27,6 +28,10 @@ const answer = async (operation) => {
|
|
|
27
28
|
code: "REFERENCED",
|
|
28
29
|
error: error.message
|
|
29
30
|
}, 409);
|
|
31
|
+
if (error instanceof DatabaseConstraintError && error.model === "native_builds") return json({
|
|
32
|
+
code: "NATIVE_BUILD_EXISTS",
|
|
33
|
+
error: "A native build with this ID or built-in bundle ID is already registered."
|
|
34
|
+
}, 409);
|
|
30
35
|
throw error;
|
|
31
36
|
}
|
|
32
37
|
};
|
|
@@ -129,6 +134,42 @@ const bundleUpdateOf = (value, bundleId) => {
|
|
|
129
134
|
if (id !== void 0 && id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
|
|
130
135
|
return update;
|
|
131
136
|
};
|
|
137
|
+
const NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
|
|
138
|
+
"apk",
|
|
139
|
+
"aab",
|
|
140
|
+
"ipa",
|
|
141
|
+
"app"
|
|
142
|
+
]);
|
|
143
|
+
/** A native build's scope: `fingerprintHash` or `appVersion`, never both. */
|
|
144
|
+
const nativeBuildScopeOf = (url) => {
|
|
145
|
+
const params = url.searchParams;
|
|
146
|
+
const platform = platformOf(url);
|
|
147
|
+
const channel = params.get("channel");
|
|
148
|
+
const fingerprintHash = params.get("fingerprintHash");
|
|
149
|
+
const appVersion = params.get("appVersion");
|
|
150
|
+
if (platform === void 0 || !isText(channel) || fingerprintHash === null === (appVersion === null) || fingerprintHash === "" || appVersion === "") throw new HandlerBadRequestError("A native build lookup needs platform, channel, and fingerprintHash or appVersion.");
|
|
151
|
+
return fingerprintHash !== null ? {
|
|
152
|
+
platform,
|
|
153
|
+
channel,
|
|
154
|
+
fingerprintHash,
|
|
155
|
+
appVersion: null
|
|
156
|
+
} : {
|
|
157
|
+
platform,
|
|
158
|
+
channel,
|
|
159
|
+
fingerprintHash: null,
|
|
160
|
+
appVersion
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
/** A native build or scope core refuses: a bad request. */
|
|
164
|
+
const refusingInvalid = async (operation) => {
|
|
165
|
+
try {
|
|
166
|
+
return await operation();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof NativeBuildInputError) throw new HandlerBadRequestError(error.message);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const isNativeBuildRegistration = (value) => isRecord(value) && isText(value.id) && isText(value.minBundleId) && typeof value.platform === "string" && isPlatform(value.platform) && isText(value.channel) && isTextOrNull(value.fingerprintHash) && isTextOrNull(value.appVersion) && value.fingerprintHash === null !== (value.appVersion === null) && isTextOrNull(value.gitCommitHash) && isTextOrNull(value.buildKey) && (value.buildNumber === void 0 || isTextOrNull(value.buildNumber)) && isRecord(value.artifact) && typeof value.artifact.kind === "string" && NATIVE_BUILD_ARTIFACT_KINDS.has(value.artifact.kind) && isText(value.artifact.storageUri) && Number.isSafeInteger(value.artifact.byteSize) && value.artifact.byteSize >= 0 && isRecord(value.embeddedBundle) && isText(value.embeddedBundle.manifestStorageUri) && isText(value.embeddedBundle.manifestFileHash) && isText(value.embeddedBundle.assetBaseStorageUri) && (value.embeddedBundle.metadata === void 0 || isRecord(value.embeddedBundle.metadata));
|
|
132
173
|
const scopeKeyOf = (params) => decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "";
|
|
133
174
|
/** Every admin route's handler, by the name `ADMIN_ROUTES` mounts it under. */
|
|
134
175
|
const createAdminRouteHandlers = () => ({
|
|
@@ -248,6 +289,37 @@ const createAdminRouteHandlers = () => ({
|
|
|
248
289
|
}) });
|
|
249
290
|
}),
|
|
250
291
|
preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
|
|
292
|
+
registerNativeBuild: onCore(async (core, request) => {
|
|
293
|
+
const input = await body(request);
|
|
294
|
+
if (!isNativeBuildRegistration(input)) throw new HandlerBadRequestError("Invalid native build");
|
|
295
|
+
return json({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
|
|
296
|
+
}),
|
|
297
|
+
findNativeBuild: onCore(async (core, request) => {
|
|
298
|
+
const url = new URL(request.url);
|
|
299
|
+
const scope = nativeBuildScopeOf(url);
|
|
300
|
+
const buildKey = url.searchParams.get("buildKey");
|
|
301
|
+
return json({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
|
|
302
|
+
}),
|
|
303
|
+
getNativeBuild: onCore(async (core, _request, params) => json({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
|
|
304
|
+
listNativeBuilds: onCore(async (core, request) => {
|
|
305
|
+
const url = new URL(request.url);
|
|
306
|
+
const input = keyset(url);
|
|
307
|
+
const channelId = url.searchParams.get("channelId");
|
|
308
|
+
return page(await core.listNativeBuilds({
|
|
309
|
+
...input,
|
|
310
|
+
...channelId === null ? {} : { channelId }
|
|
311
|
+
}), input, ({ id }) => id);
|
|
312
|
+
}),
|
|
313
|
+
recordNativeBuildSubmission: onCore(async (core, request, params) => {
|
|
314
|
+
const input = await body(request);
|
|
315
|
+
if (!isRecord(input) || input.destination !== "testflight" && input.destination !== "play-internal" || !isText(input.version)) throw new HandlerBadRequestError("A native build submission needs destination (testflight or play-internal) and version.");
|
|
316
|
+
const submission = {
|
|
317
|
+
destination: input.destination,
|
|
318
|
+
version: input.version
|
|
319
|
+
};
|
|
320
|
+
return json({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
|
|
321
|
+
}),
|
|
322
|
+
deleteNativeBuild: onCore(async (core, _request, params) => json({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
|
|
251
323
|
deleteBundles: onCore(async (core, request) => {
|
|
252
324
|
const { ids } = await body(request);
|
|
253
325
|
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new HandlerBadRequestError("Bundle deletion needs ids");
|
|
@@ -366,6 +438,36 @@ const ADMIN_ROUTES = [
|
|
|
366
438
|
method: "GET",
|
|
367
439
|
path: "/base-candidates/:candidateKey",
|
|
368
440
|
handler: "findBaseCandidates"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
method: "POST",
|
|
444
|
+
path: "/native-builds",
|
|
445
|
+
handler: "registerNativeBuild"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
method: "GET",
|
|
449
|
+
path: "/native-builds/latest",
|
|
450
|
+
handler: "findNativeBuild"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
method: "GET",
|
|
454
|
+
path: "/native-builds/:id",
|
|
455
|
+
handler: "getNativeBuild"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
method: "GET",
|
|
459
|
+
path: "/native-builds",
|
|
460
|
+
handler: "listNativeBuilds"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: "/native-builds/:id/submission",
|
|
465
|
+
handler: "recordNativeBuildSubmission"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
method: "DELETE",
|
|
469
|
+
path: "/native-builds/:id",
|
|
470
|
+
handler: "deleteNativeBuild"
|
|
369
471
|
}
|
|
370
472
|
];
|
|
371
473
|
//#endregion
|
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.1
|
|
3
|
+
"version": "1.2.0-rc.1",
|
|
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.1
|
|
54
|
-
"@hot-updater/plugin-
|
|
55
|
-
"@hot-updater/plugin-remote-config": "1.1
|
|
56
|
-
"@hot-updater/plugin-
|
|
57
|
-
"@hot-updater/plugin-
|
|
53
|
+
"@hot-updater/protocol": "1.2.0-rc.1",
|
|
54
|
+
"@hot-updater/plugin-insights": "1.2.0-rc.1",
|
|
55
|
+
"@hot-updater/plugin-remote-config": "1.2.0-rc.1",
|
|
56
|
+
"@hot-updater/plugin-api-keys": "1.2.0-rc.1",
|
|
57
|
+
"@hot-updater/plugin-core": "1.2.0-rc.1"
|
|
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.1
|
|
99
|
-
"@hot-updater/test-utils": "1.1
|
|
98
|
+
"@hot-updater/standalone": "1.2.0-rc.1",
|
|
99
|
+
"@hot-updater/test-utils": "1.2.0-rc.1"
|
|
100
100
|
},
|
|
101
101
|
"nx": {
|
|
102
102
|
"implicitDependencies": [
|