@hot-updater/cloudflare 0.27.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/iac/index.cjs +719 -905
- package/dist/iac/{index.js → index.mjs} +677 -851
- package/dist/index.cjs +558 -674
- package/dist/index.d.cts +3 -3
- package/dist/{index.d.ts → index.d.mts} +3 -3
- package/dist/{index.js → index.mjs} +550 -654
- package/dist/worker/index.cjs +225 -0
- package/dist/worker/index.d.cts +39 -0
- package/dist/worker/index.d.mts +39 -0
- package/dist/worker/index.mjs +217 -0
- package/package.json +25 -16
- package/sql/bundles.sql +4 -1
- package/src/cloudflareWorkerDatabase.ts +360 -0
- package/src/d1Database.spec.ts +173 -0
- package/src/d1Database.ts +366 -0
- package/src/index.ts +2 -0
- package/src/r2Storage.ts +129 -0
- package/src/r2WorkerStorage.ts +94 -0
- package/src/utils/createWrangler.ts +27 -0
- package/src/worker/index.ts +33 -0
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +8753 -5053
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/worker/src/getUpdateInfo.ts +200 -0
- package/worker/src/index.integration.spec.ts +171 -0
- package/worker/src/index.ts +87 -0
- package/worker/src/testWorker.ts +5 -0
- /package/dist/iac/{index.d.ts → index.d.mts} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _hot_updater_js = require("@hot-updater/js");
|
|
3
|
+
let _hot_updater_core = require("@hot-updater/core");
|
|
4
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
5
|
+
//#region src/cloudflareWorkerDatabase.ts
|
|
6
|
+
function buildWhereClause(conditions) {
|
|
7
|
+
if (!conditions) return {
|
|
8
|
+
sql: "",
|
|
9
|
+
params: []
|
|
10
|
+
};
|
|
11
|
+
const clauses = [];
|
|
12
|
+
const params = [];
|
|
13
|
+
if (conditions.channel) {
|
|
14
|
+
clauses.push("channel = ?");
|
|
15
|
+
params.push(conditions.channel);
|
|
16
|
+
}
|
|
17
|
+
if (conditions.platform) {
|
|
18
|
+
clauses.push("platform = ?");
|
|
19
|
+
params.push(conditions.platform);
|
|
20
|
+
}
|
|
21
|
+
if (conditions.enabled !== void 0) {
|
|
22
|
+
clauses.push("enabled = ?");
|
|
23
|
+
params.push(conditions.enabled ? 1 : 0);
|
|
24
|
+
}
|
|
25
|
+
if (conditions.id?.in) if (conditions.id.in.length === 0) clauses.push("1 = 0");
|
|
26
|
+
else {
|
|
27
|
+
clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
|
|
28
|
+
params.push(...conditions.id.in);
|
|
29
|
+
}
|
|
30
|
+
if (conditions.id?.eq) {
|
|
31
|
+
clauses.push("id = ?");
|
|
32
|
+
params.push(conditions.id.eq);
|
|
33
|
+
}
|
|
34
|
+
if (conditions.id?.gt) {
|
|
35
|
+
clauses.push("id > ?");
|
|
36
|
+
params.push(conditions.id.gt);
|
|
37
|
+
}
|
|
38
|
+
if (conditions.id?.gte) {
|
|
39
|
+
clauses.push("id >= ?");
|
|
40
|
+
params.push(conditions.id.gte);
|
|
41
|
+
}
|
|
42
|
+
if (conditions.id?.lt) {
|
|
43
|
+
clauses.push("id < ?");
|
|
44
|
+
params.push(conditions.id.lt);
|
|
45
|
+
}
|
|
46
|
+
if (conditions.id?.lte) {
|
|
47
|
+
clauses.push("id <= ?");
|
|
48
|
+
params.push(conditions.id.lte);
|
|
49
|
+
}
|
|
50
|
+
if (conditions.targetAppVersionNotNull) clauses.push("target_app_version IS NOT NULL");
|
|
51
|
+
if (conditions.targetAppVersion !== void 0) if (conditions.targetAppVersion === null) clauses.push("target_app_version IS NULL");
|
|
52
|
+
else {
|
|
53
|
+
clauses.push("target_app_version = ?");
|
|
54
|
+
params.push(conditions.targetAppVersion);
|
|
55
|
+
}
|
|
56
|
+
if (conditions.targetAppVersionIn) if (conditions.targetAppVersionIn.length === 0) clauses.push("1 = 0");
|
|
57
|
+
else {
|
|
58
|
+
clauses.push(`target_app_version IN (${conditions.targetAppVersionIn.map(() => "?").join(", ")})`);
|
|
59
|
+
params.push(...conditions.targetAppVersionIn);
|
|
60
|
+
}
|
|
61
|
+
if (conditions.fingerprintHash !== void 0) if (conditions.fingerprintHash === null) clauses.push("fingerprint_hash IS NULL");
|
|
62
|
+
else {
|
|
63
|
+
clauses.push("fingerprint_hash = ?");
|
|
64
|
+
params.push(conditions.fingerprintHash);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
sql: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
|
|
68
|
+
params
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseTargetCohorts(value) {
|
|
72
|
+
if (!value) return null;
|
|
73
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
|
|
74
|
+
if (typeof value === "string") try {
|
|
75
|
+
const parsed = JSON.parse(value);
|
|
76
|
+
if (Array.isArray(parsed)) return parsed.filter((item) => typeof item === "string");
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function transformRowToBundle(row) {
|
|
83
|
+
return {
|
|
84
|
+
id: row.id,
|
|
85
|
+
channel: row.channel,
|
|
86
|
+
enabled: Boolean(row.enabled),
|
|
87
|
+
shouldForceUpdate: Boolean(row.should_force_update),
|
|
88
|
+
fileHash: row.file_hash,
|
|
89
|
+
gitCommitHash: row.git_commit_hash,
|
|
90
|
+
message: row.message,
|
|
91
|
+
platform: row.platform,
|
|
92
|
+
targetAppVersion: row.target_app_version,
|
|
93
|
+
storageUri: row.storage_uri,
|
|
94
|
+
fingerprintHash: row.fingerprint_hash,
|
|
95
|
+
metadata: row?.metadata ? JSON.parse(row.metadata) : {},
|
|
96
|
+
rolloutCohortCount: row.rollout_cohort_count ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
97
|
+
targetCohorts: parseTargetCohorts(row.target_cohorts)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const resolveDbFromContext = (context) => {
|
|
101
|
+
const db = context?.env?.DB;
|
|
102
|
+
if (!db) throw new Error("d1WorkerDatabase requires env.DB in the hot updater context.");
|
|
103
|
+
return db;
|
|
104
|
+
};
|
|
105
|
+
const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin)({
|
|
106
|
+
name: "d1WorkerDatabase",
|
|
107
|
+
factory: (config) => {
|
|
108
|
+
let bundles = [];
|
|
109
|
+
const queryAll = async (sql, params = [], context) => {
|
|
110
|
+
return (await config.getDb(context).prepare(sql).bind(...params).all()).results ?? [];
|
|
111
|
+
};
|
|
112
|
+
const queryFirst = async (sql, params = [], context) => {
|
|
113
|
+
return await config.getDb(context).prepare(sql).bind(...params).first() ?? null;
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
async getBundleById(bundleId, context) {
|
|
117
|
+
const found = bundles.find((bundle) => bundle.id === bundleId);
|
|
118
|
+
if (found) return found;
|
|
119
|
+
const row = await queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context);
|
|
120
|
+
return row ? transformRowToBundle(row) : null;
|
|
121
|
+
},
|
|
122
|
+
async getBundles(options, context) {
|
|
123
|
+
const { where, limit, offset, orderBy } = options;
|
|
124
|
+
const { sql: whereClause, params } = buildWhereClause(where);
|
|
125
|
+
const orderSql = orderBy?.direction === "asc" ? "ORDER BY id ASC" : "ORDER BY id DESC";
|
|
126
|
+
const total = (await queryAll(`SELECT COUNT(*) as total FROM bundles${whereClause}`, params, context))[0]?.total ?? 0;
|
|
127
|
+
bundles = (await queryAll(`SELECT * FROM bundles${whereClause} ${orderSql} LIMIT ? OFFSET ?`, [
|
|
128
|
+
...params,
|
|
129
|
+
limit,
|
|
130
|
+
offset
|
|
131
|
+
], context)).map(transformRowToBundle);
|
|
132
|
+
return {
|
|
133
|
+
data: bundles,
|
|
134
|
+
pagination: (0, _hot_updater_plugin_core.calculatePagination)(total, {
|
|
135
|
+
limit,
|
|
136
|
+
offset
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
async getChannels(context) {
|
|
141
|
+
return (await queryAll("SELECT channel FROM bundles GROUP BY channel", [], context)).map((row) => row.channel);
|
|
142
|
+
},
|
|
143
|
+
async commitBundle({ changedSets }, context) {
|
|
144
|
+
if (changedSets.length === 0) return;
|
|
145
|
+
const db = config.getDb(context);
|
|
146
|
+
for (const operation of changedSets) {
|
|
147
|
+
if (operation.operation === "delete") {
|
|
148
|
+
await db.prepare("DELETE FROM bundles WHERE id = ?").bind(operation.data.id).run();
|
|
149
|
+
bundles = bundles.filter((bundle) => bundle.id !== operation.data.id);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const bundle = operation.data;
|
|
153
|
+
await db.prepare(`
|
|
154
|
+
INSERT OR REPLACE INTO bundles (
|
|
155
|
+
id,
|
|
156
|
+
channel,
|
|
157
|
+
enabled,
|
|
158
|
+
should_force_update,
|
|
159
|
+
file_hash,
|
|
160
|
+
git_commit_hash,
|
|
161
|
+
message,
|
|
162
|
+
platform,
|
|
163
|
+
target_app_version,
|
|
164
|
+
storage_uri,
|
|
165
|
+
fingerprint_hash,
|
|
166
|
+
metadata,
|
|
167
|
+
rollout_cohort_count,
|
|
168
|
+
target_cohorts
|
|
169
|
+
)
|
|
170
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
171
|
+
`).bind(bundle.id, bundle.channel, bundle.enabled ? 1 : 0, bundle.shouldForceUpdate ? 1 : 0, bundle.fileHash, bundle.gitCommitHash || null, bundle.message || null, bundle.platform, bundle.targetAppVersion, bundle.storageUri, bundle.fingerprintHash, JSON.stringify(bundle.metadata ?? {}), bundle.rolloutCohortCount ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT, bundle.targetCohorts ? JSON.stringify(bundle.targetCohorts) : null).run();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
})({ getDb: resolveDbFromContext });
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/r2WorkerStorage.ts
|
|
179
|
+
const resolveContextValue = async (value, context) => {
|
|
180
|
+
return typeof value === "function" ? await value(context) : value;
|
|
181
|
+
};
|
|
182
|
+
const resolveJwtSecretFromContext = (context) => {
|
|
183
|
+
const jwtSecret = context?.env?.JWT_SECRET;
|
|
184
|
+
if (!jwtSecret) throw new Error("r2WorkerStorage requires env.JWT_SECRET in the hot updater context.");
|
|
185
|
+
return jwtSecret;
|
|
186
|
+
};
|
|
187
|
+
const r2WorkerStorage = (config) => {
|
|
188
|
+
return () => {
|
|
189
|
+
return {
|
|
190
|
+
name: "r2WorkerStorage",
|
|
191
|
+
supportedProtocol: "r2",
|
|
192
|
+
async upload() {
|
|
193
|
+
throw new Error("r2WorkerStorage does not support upload() in the worker runtime.");
|
|
194
|
+
},
|
|
195
|
+
async delete() {
|
|
196
|
+
throw new Error("r2WorkerStorage does not support delete() in the worker runtime.");
|
|
197
|
+
},
|
|
198
|
+
async getDownloadUrl(storageUri, context) {
|
|
199
|
+
const storageUrl = new URL(storageUri);
|
|
200
|
+
if (storageUrl.protocol !== "r2:") throw new Error("Invalid R2 storage URI protocol");
|
|
201
|
+
const key = `${storageUrl.host}${storageUrl.pathname}`;
|
|
202
|
+
const [jwtSecret, publicBaseUrl] = await Promise.all([resolveContextValue(config.jwtSecret ?? resolveJwtSecretFromContext, context), resolveContextValue(config.publicBaseUrl, context)]);
|
|
203
|
+
const token = await (0, _hot_updater_js.signToken)(key, jwtSecret);
|
|
204
|
+
const url = new URL(publicBaseUrl);
|
|
205
|
+
url.pathname = key;
|
|
206
|
+
url.search = "";
|
|
207
|
+
url.searchParams.set("token", token);
|
|
208
|
+
return { fileUrl: url.toString() };
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/worker/index.ts
|
|
215
|
+
const d1Database = () => d1WorkerDatabase();
|
|
216
|
+
const r2Storage = (config) => r2WorkerStorage(config);
|
|
217
|
+
//#endregion
|
|
218
|
+
exports.d1Database = d1Database;
|
|
219
|
+
exports.r2Storage = r2Storage;
|
|
220
|
+
Object.defineProperty(exports, "verifyJwtSignedUrl", {
|
|
221
|
+
enumerable: true,
|
|
222
|
+
get: function() {
|
|
223
|
+
return _hot_updater_js.verifyJwtSignedUrl;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
|
|
2
|
+
import { HotUpdaterContext, RequestEnvContext as RequestEnvContext$1 } from "@hot-updater/plugin-core";
|
|
3
|
+
import { verifyJwtSignedUrl } from "@hot-updater/js";
|
|
4
|
+
|
|
5
|
+
//#region src/cloudflareWorkerDatabase.d.ts
|
|
6
|
+
type D1Result<T> = {
|
|
7
|
+
results?: T[];
|
|
8
|
+
};
|
|
9
|
+
type D1PreparedStatement = {
|
|
10
|
+
bind: (...values: unknown[]) => {
|
|
11
|
+
all: <T>() => Promise<D1Result<T>>;
|
|
12
|
+
first: <T>() => Promise<T | null>;
|
|
13
|
+
run: () => Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type D1Like = {
|
|
17
|
+
prepare: (sql: string) => D1PreparedStatement;
|
|
18
|
+
};
|
|
19
|
+
interface CloudflareWorkerDatabaseEnv {
|
|
20
|
+
DB: D1Like;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/r2WorkerStorage.d.ts
|
|
24
|
+
interface CloudflareWorkerStorageEnv {
|
|
25
|
+
JWT_SECRET: string;
|
|
26
|
+
}
|
|
27
|
+
type ContextResolver<TContext, TValue> = (context?: HotUpdaterContext<TContext>) => TValue | Promise<TValue>;
|
|
28
|
+
interface CloudflareWorkerStorageConfig<TContext extends RequestEnvContext$1<CloudflareWorkerStorageEnv>> {
|
|
29
|
+
jwtSecret?: string | ContextResolver<TContext, string>;
|
|
30
|
+
publicBaseUrl: string | ContextResolver<TContext, string>;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/worker/index.d.ts
|
|
34
|
+
interface CloudflareWorkerRuntimeEnv extends CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv {}
|
|
35
|
+
type RequestEnvContext<TEnv = CloudflareWorkerRuntimeEnv> = RequestEnvContext$1<TEnv>;
|
|
36
|
+
declare const d1Database: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>() => () => _$_hot_updater_plugin_core0.DatabasePlugin<TContext>;
|
|
37
|
+
declare const r2Storage: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>(config: CloudflareWorkerStorageConfig<TContext>) => () => _$_hot_updater_plugin_core0.StoragePlugin<TContext>;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { type CloudflareWorkerDatabaseEnv, CloudflareWorkerRuntimeEnv, type CloudflareWorkerStorageEnv, RequestEnvContext, d1Database, r2Storage, verifyJwtSignedUrl };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { verifyJwtSignedUrl } from "@hot-updater/js";
|
|
2
|
+
import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
|
|
3
|
+
import { HotUpdaterContext, RequestEnvContext as RequestEnvContext$1 } from "@hot-updater/plugin-core";
|
|
4
|
+
|
|
5
|
+
//#region src/cloudflareWorkerDatabase.d.ts
|
|
6
|
+
type D1Result<T> = {
|
|
7
|
+
results?: T[];
|
|
8
|
+
};
|
|
9
|
+
type D1PreparedStatement = {
|
|
10
|
+
bind: (...values: unknown[]) => {
|
|
11
|
+
all: <T>() => Promise<D1Result<T>>;
|
|
12
|
+
first: <T>() => Promise<T | null>;
|
|
13
|
+
run: () => Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type D1Like = {
|
|
17
|
+
prepare: (sql: string) => D1PreparedStatement;
|
|
18
|
+
};
|
|
19
|
+
interface CloudflareWorkerDatabaseEnv {
|
|
20
|
+
DB: D1Like;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/r2WorkerStorage.d.ts
|
|
24
|
+
interface CloudflareWorkerStorageEnv {
|
|
25
|
+
JWT_SECRET: string;
|
|
26
|
+
}
|
|
27
|
+
type ContextResolver<TContext, TValue> = (context?: HotUpdaterContext<TContext>) => TValue | Promise<TValue>;
|
|
28
|
+
interface CloudflareWorkerStorageConfig<TContext extends RequestEnvContext$1<CloudflareWorkerStorageEnv>> {
|
|
29
|
+
jwtSecret?: string | ContextResolver<TContext, string>;
|
|
30
|
+
publicBaseUrl: string | ContextResolver<TContext, string>;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/worker/index.d.ts
|
|
34
|
+
interface CloudflareWorkerRuntimeEnv extends CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv {}
|
|
35
|
+
type RequestEnvContext<TEnv = CloudflareWorkerRuntimeEnv> = RequestEnvContext$1<TEnv>;
|
|
36
|
+
declare const d1Database: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>() => () => _$_hot_updater_plugin_core0.DatabasePlugin<TContext>;
|
|
37
|
+
declare const r2Storage: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>(config: CloudflareWorkerStorageConfig<TContext>) => () => _$_hot_updater_plugin_core0.StoragePlugin<TContext>;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { type CloudflareWorkerDatabaseEnv, CloudflareWorkerRuntimeEnv, type CloudflareWorkerStorageEnv, RequestEnvContext, d1Database, r2Storage, verifyJwtSignedUrl };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { signToken, verifyJwtSignedUrl } from "@hot-updater/js";
|
|
2
|
+
import { DEFAULT_ROLLOUT_COHORT_COUNT } from "@hot-updater/core";
|
|
3
|
+
import { calculatePagination, createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
4
|
+
//#region src/cloudflareWorkerDatabase.ts
|
|
5
|
+
function buildWhereClause(conditions) {
|
|
6
|
+
if (!conditions) return {
|
|
7
|
+
sql: "",
|
|
8
|
+
params: []
|
|
9
|
+
};
|
|
10
|
+
const clauses = [];
|
|
11
|
+
const params = [];
|
|
12
|
+
if (conditions.channel) {
|
|
13
|
+
clauses.push("channel = ?");
|
|
14
|
+
params.push(conditions.channel);
|
|
15
|
+
}
|
|
16
|
+
if (conditions.platform) {
|
|
17
|
+
clauses.push("platform = ?");
|
|
18
|
+
params.push(conditions.platform);
|
|
19
|
+
}
|
|
20
|
+
if (conditions.enabled !== void 0) {
|
|
21
|
+
clauses.push("enabled = ?");
|
|
22
|
+
params.push(conditions.enabled ? 1 : 0);
|
|
23
|
+
}
|
|
24
|
+
if (conditions.id?.in) if (conditions.id.in.length === 0) clauses.push("1 = 0");
|
|
25
|
+
else {
|
|
26
|
+
clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
|
|
27
|
+
params.push(...conditions.id.in);
|
|
28
|
+
}
|
|
29
|
+
if (conditions.id?.eq) {
|
|
30
|
+
clauses.push("id = ?");
|
|
31
|
+
params.push(conditions.id.eq);
|
|
32
|
+
}
|
|
33
|
+
if (conditions.id?.gt) {
|
|
34
|
+
clauses.push("id > ?");
|
|
35
|
+
params.push(conditions.id.gt);
|
|
36
|
+
}
|
|
37
|
+
if (conditions.id?.gte) {
|
|
38
|
+
clauses.push("id >= ?");
|
|
39
|
+
params.push(conditions.id.gte);
|
|
40
|
+
}
|
|
41
|
+
if (conditions.id?.lt) {
|
|
42
|
+
clauses.push("id < ?");
|
|
43
|
+
params.push(conditions.id.lt);
|
|
44
|
+
}
|
|
45
|
+
if (conditions.id?.lte) {
|
|
46
|
+
clauses.push("id <= ?");
|
|
47
|
+
params.push(conditions.id.lte);
|
|
48
|
+
}
|
|
49
|
+
if (conditions.targetAppVersionNotNull) clauses.push("target_app_version IS NOT NULL");
|
|
50
|
+
if (conditions.targetAppVersion !== void 0) if (conditions.targetAppVersion === null) clauses.push("target_app_version IS NULL");
|
|
51
|
+
else {
|
|
52
|
+
clauses.push("target_app_version = ?");
|
|
53
|
+
params.push(conditions.targetAppVersion);
|
|
54
|
+
}
|
|
55
|
+
if (conditions.targetAppVersionIn) if (conditions.targetAppVersionIn.length === 0) clauses.push("1 = 0");
|
|
56
|
+
else {
|
|
57
|
+
clauses.push(`target_app_version IN (${conditions.targetAppVersionIn.map(() => "?").join(", ")})`);
|
|
58
|
+
params.push(...conditions.targetAppVersionIn);
|
|
59
|
+
}
|
|
60
|
+
if (conditions.fingerprintHash !== void 0) if (conditions.fingerprintHash === null) clauses.push("fingerprint_hash IS NULL");
|
|
61
|
+
else {
|
|
62
|
+
clauses.push("fingerprint_hash = ?");
|
|
63
|
+
params.push(conditions.fingerprintHash);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
sql: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
|
|
67
|
+
params
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseTargetCohorts(value) {
|
|
71
|
+
if (!value) return null;
|
|
72
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
|
|
73
|
+
if (typeof value === "string") try {
|
|
74
|
+
const parsed = JSON.parse(value);
|
|
75
|
+
if (Array.isArray(parsed)) return parsed.filter((item) => typeof item === "string");
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function transformRowToBundle(row) {
|
|
82
|
+
return {
|
|
83
|
+
id: row.id,
|
|
84
|
+
channel: row.channel,
|
|
85
|
+
enabled: Boolean(row.enabled),
|
|
86
|
+
shouldForceUpdate: Boolean(row.should_force_update),
|
|
87
|
+
fileHash: row.file_hash,
|
|
88
|
+
gitCommitHash: row.git_commit_hash,
|
|
89
|
+
message: row.message,
|
|
90
|
+
platform: row.platform,
|
|
91
|
+
targetAppVersion: row.target_app_version,
|
|
92
|
+
storageUri: row.storage_uri,
|
|
93
|
+
fingerprintHash: row.fingerprint_hash,
|
|
94
|
+
metadata: row?.metadata ? JSON.parse(row.metadata) : {},
|
|
95
|
+
rolloutCohortCount: row.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
96
|
+
targetCohorts: parseTargetCohorts(row.target_cohorts)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const resolveDbFromContext = (context) => {
|
|
100
|
+
const db = context?.env?.DB;
|
|
101
|
+
if (!db) throw new Error("d1WorkerDatabase requires env.DB in the hot updater context.");
|
|
102
|
+
return db;
|
|
103
|
+
};
|
|
104
|
+
const d1WorkerDatabase = () => createDatabasePlugin({
|
|
105
|
+
name: "d1WorkerDatabase",
|
|
106
|
+
factory: (config) => {
|
|
107
|
+
let bundles = [];
|
|
108
|
+
const queryAll = async (sql, params = [], context) => {
|
|
109
|
+
return (await config.getDb(context).prepare(sql).bind(...params).all()).results ?? [];
|
|
110
|
+
};
|
|
111
|
+
const queryFirst = async (sql, params = [], context) => {
|
|
112
|
+
return await config.getDb(context).prepare(sql).bind(...params).first() ?? null;
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
async getBundleById(bundleId, context) {
|
|
116
|
+
const found = bundles.find((bundle) => bundle.id === bundleId);
|
|
117
|
+
if (found) return found;
|
|
118
|
+
const row = await queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context);
|
|
119
|
+
return row ? transformRowToBundle(row) : null;
|
|
120
|
+
},
|
|
121
|
+
async getBundles(options, context) {
|
|
122
|
+
const { where, limit, offset, orderBy } = options;
|
|
123
|
+
const { sql: whereClause, params } = buildWhereClause(where);
|
|
124
|
+
const orderSql = orderBy?.direction === "asc" ? "ORDER BY id ASC" : "ORDER BY id DESC";
|
|
125
|
+
const total = (await queryAll(`SELECT COUNT(*) as total FROM bundles${whereClause}`, params, context))[0]?.total ?? 0;
|
|
126
|
+
bundles = (await queryAll(`SELECT * FROM bundles${whereClause} ${orderSql} LIMIT ? OFFSET ?`, [
|
|
127
|
+
...params,
|
|
128
|
+
limit,
|
|
129
|
+
offset
|
|
130
|
+
], context)).map(transformRowToBundle);
|
|
131
|
+
return {
|
|
132
|
+
data: bundles,
|
|
133
|
+
pagination: calculatePagination(total, {
|
|
134
|
+
limit,
|
|
135
|
+
offset
|
|
136
|
+
})
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async getChannels(context) {
|
|
140
|
+
return (await queryAll("SELECT channel FROM bundles GROUP BY channel", [], context)).map((row) => row.channel);
|
|
141
|
+
},
|
|
142
|
+
async commitBundle({ changedSets }, context) {
|
|
143
|
+
if (changedSets.length === 0) return;
|
|
144
|
+
const db = config.getDb(context);
|
|
145
|
+
for (const operation of changedSets) {
|
|
146
|
+
if (operation.operation === "delete") {
|
|
147
|
+
await db.prepare("DELETE FROM bundles WHERE id = ?").bind(operation.data.id).run();
|
|
148
|
+
bundles = bundles.filter((bundle) => bundle.id !== operation.data.id);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const bundle = operation.data;
|
|
152
|
+
await db.prepare(`
|
|
153
|
+
INSERT OR REPLACE INTO bundles (
|
|
154
|
+
id,
|
|
155
|
+
channel,
|
|
156
|
+
enabled,
|
|
157
|
+
should_force_update,
|
|
158
|
+
file_hash,
|
|
159
|
+
git_commit_hash,
|
|
160
|
+
message,
|
|
161
|
+
platform,
|
|
162
|
+
target_app_version,
|
|
163
|
+
storage_uri,
|
|
164
|
+
fingerprint_hash,
|
|
165
|
+
metadata,
|
|
166
|
+
rollout_cohort_count,
|
|
167
|
+
target_cohorts
|
|
168
|
+
)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
`).bind(bundle.id, bundle.channel, bundle.enabled ? 1 : 0, bundle.shouldForceUpdate ? 1 : 0, bundle.fileHash, bundle.gitCommitHash || null, bundle.message || null, bundle.platform, bundle.targetAppVersion, bundle.storageUri, bundle.fingerprintHash, JSON.stringify(bundle.metadata ?? {}), bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT, bundle.targetCohorts ? JSON.stringify(bundle.targetCohorts) : null).run();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
})({ getDb: resolveDbFromContext });
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/r2WorkerStorage.ts
|
|
178
|
+
const resolveContextValue = async (value, context) => {
|
|
179
|
+
return typeof value === "function" ? await value(context) : value;
|
|
180
|
+
};
|
|
181
|
+
const resolveJwtSecretFromContext = (context) => {
|
|
182
|
+
const jwtSecret = context?.env?.JWT_SECRET;
|
|
183
|
+
if (!jwtSecret) throw new Error("r2WorkerStorage requires env.JWT_SECRET in the hot updater context.");
|
|
184
|
+
return jwtSecret;
|
|
185
|
+
};
|
|
186
|
+
const r2WorkerStorage = (config) => {
|
|
187
|
+
return () => {
|
|
188
|
+
return {
|
|
189
|
+
name: "r2WorkerStorage",
|
|
190
|
+
supportedProtocol: "r2",
|
|
191
|
+
async upload() {
|
|
192
|
+
throw new Error("r2WorkerStorage does not support upload() in the worker runtime.");
|
|
193
|
+
},
|
|
194
|
+
async delete() {
|
|
195
|
+
throw new Error("r2WorkerStorage does not support delete() in the worker runtime.");
|
|
196
|
+
},
|
|
197
|
+
async getDownloadUrl(storageUri, context) {
|
|
198
|
+
const storageUrl = new URL(storageUri);
|
|
199
|
+
if (storageUrl.protocol !== "r2:") throw new Error("Invalid R2 storage URI protocol");
|
|
200
|
+
const key = `${storageUrl.host}${storageUrl.pathname}`;
|
|
201
|
+
const [jwtSecret, publicBaseUrl] = await Promise.all([resolveContextValue(config.jwtSecret ?? resolveJwtSecretFromContext, context), resolveContextValue(config.publicBaseUrl, context)]);
|
|
202
|
+
const token = await signToken(key, jwtSecret);
|
|
203
|
+
const url = new URL(publicBaseUrl);
|
|
204
|
+
url.pathname = key;
|
|
205
|
+
url.search = "";
|
|
206
|
+
url.searchParams.set("token", token);
|
|
207
|
+
return { fileUrl: url.toString() };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/worker/index.ts
|
|
214
|
+
const d1Database = () => d1WorkerDatabase();
|
|
215
|
+
const r2Storage = (config) => r2WorkerStorage(config);
|
|
216
|
+
//#endregion
|
|
217
|
+
export { d1Database, r2Storage, verifyJwtSignedUrl };
|
package/package.json
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/cloudflare",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.0",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
|
-
"module": "dist/index.
|
|
8
|
-
"types": "dist/index.d.
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.cts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"import": "./dist/index.
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
12
|
"require": "./dist/index.cjs"
|
|
13
13
|
},
|
|
14
14
|
"./worker": {
|
|
15
|
-
"
|
|
15
|
+
"types": "./dist/worker/index.d.cts",
|
|
16
|
+
"import": "./dist/worker/index.mjs",
|
|
17
|
+
"require": "./dist/worker/index.cjs",
|
|
18
|
+
"default": "./dist/worker/index.mjs"
|
|
16
19
|
},
|
|
20
|
+
"./worker/config": "./worker/wrangler.json",
|
|
21
|
+
"./worker/wrangler.json": "./worker/wrangler.json",
|
|
17
22
|
"./iac": {
|
|
18
|
-
"types": "./dist/iac/index.d.
|
|
19
|
-
"import": "./dist/iac/index.
|
|
23
|
+
"types": "./dist/iac/index.d.cts",
|
|
24
|
+
"import": "./dist/iac/index.mjs",
|
|
20
25
|
"require": "./dist/iac/index.cjs"
|
|
21
26
|
},
|
|
22
27
|
"./package.json": "./package.json"
|
|
@@ -34,35 +39,39 @@
|
|
|
34
39
|
"files": [
|
|
35
40
|
"dist",
|
|
36
41
|
"sql",
|
|
42
|
+
"src",
|
|
37
43
|
"worker/dist",
|
|
38
44
|
"worker/migrations",
|
|
45
|
+
"worker/src",
|
|
39
46
|
"worker/wrangler.json",
|
|
40
47
|
"package.json"
|
|
41
48
|
],
|
|
42
49
|
"dependencies": {
|
|
43
50
|
"cloudflare": "4.2.0",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"@hot-updater/
|
|
47
|
-
"@hot-updater/
|
|
51
|
+
"hono": "4.12.9",
|
|
52
|
+
"uuidv7": "^1.0.2",
|
|
53
|
+
"@hot-updater/cli-tools": "0.29.0",
|
|
54
|
+
"@hot-updater/js": "0.29.0",
|
|
55
|
+
"@hot-updater/core": "0.29.0",
|
|
56
|
+
"@hot-updater/plugin-core": "0.29.0",
|
|
57
|
+
"@hot-updater/server": "0.29.0"
|
|
48
58
|
},
|
|
49
59
|
"devDependencies": {
|
|
50
|
-
"@cloudflare/vitest-pool-workers": "
|
|
51
|
-
"@cloudflare/workers-types": "^4.
|
|
60
|
+
"@cloudflare/vitest-pool-workers": "0.13.0",
|
|
61
|
+
"@cloudflare/workers-types": "^4.20260312.1",
|
|
52
62
|
"@types/node": "^20",
|
|
53
63
|
"@types/semver": "^7.5.8",
|
|
54
64
|
"dayjs": "^1.11.13",
|
|
55
65
|
"execa": "9.5.2",
|
|
56
|
-
"hono": "^4.6.3",
|
|
57
66
|
"mime": "^4.0.4",
|
|
58
67
|
"pg-minify": "^1.6.5",
|
|
59
68
|
"semver": "^7.6.3",
|
|
60
69
|
"toml": "^3.0.0",
|
|
61
70
|
"typescript": "^5.5.2",
|
|
62
|
-
"vitest": "
|
|
71
|
+
"vitest": "4.1.0",
|
|
63
72
|
"wrangler": "^4.5.0",
|
|
64
73
|
"xdg-app-paths": "^8.3.0",
|
|
65
|
-
"@hot-updater/test-utils": "0.
|
|
74
|
+
"@hot-updater/test-utils": "0.29.0"
|
|
66
75
|
},
|
|
67
76
|
"scripts": {
|
|
68
77
|
"build": "tsdown && pnpm build:worker",
|
package/sql/bundles.sql
CHANGED
|
@@ -12,7 +12,10 @@ CREATE TABLE bundles (
|
|
|
12
12
|
channel TEXT NOT NULL,
|
|
13
13
|
storage_uri TEXT,
|
|
14
14
|
fingerprint_hash TEXT,
|
|
15
|
-
metadata JSONB DEFAULT '{}'
|
|
15
|
+
metadata JSONB DEFAULT '{}',
|
|
16
|
+
rollout_cohort_count INTEGER DEFAULT 1000
|
|
17
|
+
CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000),
|
|
18
|
+
target_cohorts TEXT
|
|
16
19
|
);
|
|
17
20
|
|
|
18
21
|
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|