@hot-updater/server 0.36.6 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/databaseConstraintErrors.cjs +23 -0
- package/dist/adapters/databaseConstraintErrors.mjs +21 -0
- package/dist/adapters/databasePluginUtils.cjs +117 -0
- package/dist/adapters/databasePluginUtils.mjs +106 -0
- package/dist/adapters/drizzle.cjs +53 -176
- package/dist/adapters/drizzle.d.cts +6 -4
- package/dist/adapters/drizzle.d.mts +6 -4
- package/dist/adapters/drizzle.mjs +54 -177
- package/dist/adapters/drizzleCrud.cjs +250 -0
- package/dist/adapters/drizzleCrud.mjs +249 -0
- package/dist/adapters/drizzleLazyDB.cjs +84 -36
- package/dist/adapters/drizzleLazyDB.mjs +84 -36
- package/dist/adapters/drizzleQuery.cjs +68 -0
- package/dist/adapters/drizzleQuery.mjs +67 -0
- package/dist/adapters/kysely.cjs +18 -138
- package/dist/adapters/kysely.d.cts +3 -12
- package/dist/adapters/kysely.d.mts +3 -12
- package/dist/adapters/kysely.mjs +19 -139
- package/dist/adapters/kyselyCrud.cjs +254 -0
- package/dist/adapters/kyselyCrud.mjs +253 -0
- package/dist/adapters/kyselyQuery.cjs +56 -0
- package/dist/adapters/kyselyQuery.mjs +55 -0
- package/dist/adapters/mongodb.cjs +21 -174
- package/dist/adapters/mongodb.d.cts +3 -2
- package/dist/adapters/mongodb.d.mts +3 -2
- package/dist/adapters/mongodb.mjs +21 -174
- package/dist/adapters/mongodbCollections.cjs +36 -0
- package/dist/adapters/mongodbCollections.mjs +30 -0
- package/dist/adapters/mongodbQuery.cjs +77 -0
- package/dist/adapters/mongodbQuery.mjs +70 -0
- package/dist/adapters/mongodbReads.cjs +148 -0
- package/dist/adapters/mongodbReads.mjs +147 -0
- package/dist/adapters/mongodbWrites.cjs +134 -0
- package/dist/adapters/mongodbWrites.mjs +133 -0
- package/dist/adapters/prisma.cjs +205 -180
- package/dist/adapters/prisma.d.cts +3 -3
- package/dist/adapters/prisma.d.mts +3 -3
- package/dist/adapters/prisma.mjs +206 -181
- package/dist/adapters/prismaQuery.cjs +58 -0
- package/dist/adapters/prismaQuery.mjs +56 -0
- package/dist/adapters/prismaRows.cjs +198 -0
- package/dist/adapters/prismaRows.mjs +188 -0
- package/dist/analytics/bounded/activeOverview.cjs +87 -0
- package/dist/analytics/bounded/activeOverview.mjs +85 -0
- package/dist/analytics/bounded/installationSearch.cjs +42 -0
- package/dist/analytics/bounded/installationSearch.mjs +42 -0
- package/dist/analytics/bounded/persistence.cjs +39 -0
- package/dist/analytics/bounded/persistence.mjs +38 -0
- package/dist/analytics/bounded/provider.cjs +170 -0
- package/dist/analytics/bounded/provider.d.cts +7 -0
- package/dist/analytics/bounded/provider.d.mts +7 -0
- package/dist/analytics/bounded/provider.mjs +170 -0
- package/dist/analytics/bounded/scan.cjs +144 -0
- package/dist/analytics/bounded/scan.mjs +138 -0
- package/dist/analytics/domain.d.cts +121 -0
- package/dist/analytics/domain.d.mts +121 -0
- package/dist/analytics/errors.cjs +22 -0
- package/dist/analytics/errors.mjs +20 -0
- package/dist/analytics/eventInput.cjs +111 -0
- package/dist/analytics/eventInput.mjs +111 -0
- package/dist/analytics/persistence.d.cts +6 -0
- package/dist/analytics/persistence.d.mts +6 -0
- package/dist/analytics/queryInput.cjs +58 -0
- package/dist/analytics/queryInput.mjs +55 -0
- package/dist/analytics/routes.cjs +63 -0
- package/dist/analytics/routes.mjs +61 -0
- package/dist/analytics/types.d.cts +17 -0
- package/dist/analytics/types.d.mts +17 -0
- package/dist/apiKeys.cjs +139 -0
- package/dist/apiKeys.d.cts +50 -0
- package/dist/apiKeys.d.mts +50 -0
- package/dist/apiKeys.mjs +133 -0
- package/dist/createHotUpdaterCore.cjs +48 -26
- package/dist/createHotUpdaterCore.d.cts +49 -17
- package/dist/createHotUpdaterCore.d.mts +49 -17
- package/dist/createHotUpdaterCore.mjs +51 -29
- package/dist/db/createBundleDiff.cjs +45 -68
- package/dist/db/createBundleDiff.d.cts +3 -3
- package/dist/db/createBundleDiff.d.mts +3 -3
- package/dist/db/createBundleDiff.mjs +47 -68
- package/dist/db/databasePluginCore.cjs +138 -0
- package/dist/db/databasePluginCore.mjs +137 -0
- package/dist/db/fixedMigrator.cjs +36 -93
- package/dist/db/fixedMigrator.mjs +36 -93
- package/dist/db/fixedMigratorMongo.cjs +319 -0
- package/dist/db/fixedMigratorMongo.mjs +319 -0
- package/dist/db/fixedMigratorShared.cjs +23 -0
- package/dist/db/fixedMigratorShared.mjs +18 -0
- package/dist/db/mongoMigrationExecution.cjs +11 -0
- package/dist/db/mongoMigrationExecution.mjs +11 -0
- package/dist/db/releaseCatalog.cjs +66 -0
- package/dist/db/releaseCatalog.d.cts +17 -0
- package/dist/db/releaseCatalog.d.mts +17 -0
- package/dist/db/releaseCatalog.mjs +64 -0
- package/dist/db/schema/mongodb.cjs +14 -6
- package/dist/db/schema/mongodb.mjs +14 -6
- package/dist/db/schema/registry.cjs +0 -2
- package/dist/db/schema/registry.mjs +1 -2
- package/dist/db/schema/sql.cjs +31 -14
- package/dist/db/schema/sql.mjs +32 -13
- package/dist/db/schema/sqlOperations.cjs +4 -4
- package/dist/db/schema/sqlOperations.mjs +5 -5
- package/dist/db/schemaGenerators.cjs +52 -18
- package/dist/db/schemaGenerators.mjs +52 -18
- package/dist/db/schemaReadiness.cjs +4 -3
- package/dist/db/schemaReadiness.mjs +4 -3
- package/dist/db/sqlMigrationExecution.cjs +67 -0
- package/dist/db/sqlMigrationExecution.mjs +66 -0
- package/dist/db/types.cjs +2 -6
- package/dist/db/types.d.cts +60 -16
- package/dist/db/types.d.mts +60 -16
- package/dist/db/types.mjs +3 -6
- package/dist/db/updateArtifacts.cjs +114 -60
- package/dist/db/updateArtifacts.mjs +117 -62
- package/dist/handler.cjs +108 -282
- package/dist/handler.d.cts +3 -46
- package/dist/handler.d.mts +3 -46
- package/dist/handler.mjs +107 -281
- package/dist/handlerBundleRoutes.cjs +155 -0
- package/dist/handlerBundleRoutes.mjs +155 -0
- package/dist/handlerErrors.cjs +9 -0
- package/dist/handlerErrors.mjs +9 -0
- package/dist/handlerParameters.cjs +39 -0
- package/dist/handlerParameters.mjs +34 -0
- package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
- package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
- package/dist/handlerReleaseManagementRoutes.cjs +125 -0
- package/dist/handlerReleaseManagementRoutes.mjs +124 -0
- package/dist/handlerTypes.d.cts +63 -0
- package/dist/handlerTypes.d.mts +63 -0
- package/dist/handlerVersionRoutes.cjs +10 -0
- package/dist/handlerVersionRoutes.d.cts +4 -0
- package/dist/handlerVersionRoutes.d.mts +4 -0
- package/dist/handlerVersionRoutes.mjs +9 -0
- package/dist/index.cjs +10 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.mts +9 -3
- package/dist/index.mjs +5 -2
- package/dist/node.cjs +19 -12
- package/dist/node.d.cts +15 -10
- package/dist/node.d.mts +15 -10
- package/dist/node.mjs +19 -13
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/schema/dsl.cjs +19 -2
- package/dist/schema/dsl.mjs +17 -3
- package/dist/schema/index.cjs +1 -8
- package/dist/schema/index.mjs +2 -8
- package/dist/schema/types.cjs +3 -1
- package/dist/schema/types.mjs +3 -2
- package/dist/schema/v1_0_0.cjs +364 -0
- package/dist/schema/v1_0_0.mjs +357 -0
- package/dist/storageAccess.cjs +69 -25
- package/dist/storageAccess.mjs +68 -25
- package/dist/types/index.d.cts +4 -3
- package/dist/types/index.d.mts +5 -4
- package/package.json +8 -7
- package/src/adapters/databaseConstraintErrors.ts +25 -0
- package/src/adapters/databasePluginTestDatabase.ts +106 -0
- package/src/adapters/databasePluginUtils.spec.ts +29 -0
- package/src/adapters/databasePluginUtils.ts +214 -0
- package/src/adapters/drizzle.spec.ts +208 -47
- package/src/adapters/drizzle.ts +106 -293
- package/src/adapters/drizzleCrud.ts +419 -0
- package/src/adapters/drizzleLazyDB.ts +206 -100
- package/src/adapters/drizzleQuery.ts +164 -0
- package/src/adapters/kysely.spec.ts +126 -88
- package/src/adapters/kysely.ts +76 -334
- package/src/adapters/kyselyCrud.ts +588 -0
- package/src/adapters/kyselyQuery.ts +128 -0
- package/src/adapters/mongodb.spec.ts +141 -0
- package/src/adapters/mongodb.ts +60 -288
- package/src/adapters/mongodbCollections.ts +62 -0
- package/src/adapters/mongodbQuery.ts +226 -0
- package/src/adapters/mongodbReads.ts +311 -0
- package/src/adapters/mongodbRows.spec.ts +64 -0
- package/src/adapters/mongodbRows.ts +118 -0
- package/src/adapters/mongodbTestClient.ts +342 -0
- package/src/adapters/mongodbTestFilter.ts +166 -0
- package/src/adapters/mongodbWrites.ts +260 -0
- package/src/adapters/prisma.spec.ts +196 -0
- package/src/adapters/prisma.ts +417 -292
- package/src/adapters/prismaQuery.ts +129 -0
- package/src/adapters/prismaRows.spec.ts +98 -0
- package/src/adapters/prismaRows.ts +301 -0
- package/src/adapters/prismaTestClient.ts +396 -0
- package/src/analytics/analytics.integration.spec.ts +144 -0
- package/src/analytics/bounded/activeOverview.spec.ts +189 -0
- package/src/analytics/bounded/activeOverview.ts +148 -0
- package/src/analytics/bounded/installationSearch.spec.ts +130 -0
- package/src/analytics/bounded/installationSearch.ts +69 -0
- package/src/analytics/bounded/persistence.ts +43 -0
- package/src/analytics/bounded/provider.spec.ts +219 -0
- package/src/analytics/bounded/provider.ts +229 -0
- package/src/analytics/bounded/scan.ts +235 -0
- package/src/analytics/domain.ts +139 -0
- package/src/analytics/errors.ts +18 -0
- package/src/analytics/eventInput.ts +168 -0
- package/src/analytics/persistence.ts +10 -0
- package/src/analytics/queryInput.ts +102 -0
- package/src/analytics/routes.ts +123 -0
- package/src/analytics/types.ts +44 -0
- package/src/apiKeys.integration.spec.ts +192 -0
- package/src/apiKeys.spec.ts +162 -0
- package/src/apiKeys.ts +220 -0
- package/src/createHotUpdaterCore.ts +164 -100
- package/src/db/bundleRows.spec.ts +164 -0
- package/src/db/bundleRows.ts +10 -140
- package/src/db/createBundleDiff.spec.ts +172 -127
- package/src/db/createBundleDiff.ts +91 -110
- package/src/db/databasePluginCore.spec.ts +62 -0
- package/src/db/databasePluginCore.testFixtures.ts +76 -0
- package/src/db/databasePluginCore.ts +185 -0
- package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
- package/src/db/fixedMigrator.spec.ts +184 -42
- package/src/db/fixedMigrator.ts +64 -205
- package/src/db/fixedMigratorMongo.spec.ts +192 -0
- package/src/db/fixedMigratorMongo.ts +425 -0
- package/src/db/fixedMigratorShared.ts +36 -0
- package/src/db/hotUpdaterSchema.ts +0 -1
- package/src/db/index.spec.ts +333 -1025
- package/src/db/mongoMigrationExecution.ts +23 -0
- package/src/db/node-sqlite.d.ts +25 -0
- package/src/db/releaseCatalog.ts +148 -0
- package/src/db/schema/mongodb.ts +20 -6
- package/src/db/schema/sql.ts +52 -11
- package/src/db/schema/sqlOperations.ts +5 -4
- package/src/db/schemaGenerators.ts +107 -23
- package/src/db/schemaReadiness.ts +2 -1
- package/src/db/sqlMigrationExecution.spec.ts +65 -0
- package/src/db/sqlMigrationExecution.ts +108 -0
- package/src/db/types.spec.ts +55 -0
- package/src/db/types.ts +129 -65
- package/src/db/updateArtifacts.spec.ts +359 -0
- package/src/db/updateArtifacts.ts +290 -126
- package/src/handler-standalone.integration.spec.ts +401 -364
- package/src/handler.spec.ts +137 -582
- package/src/handler.testFixtures.ts +30 -0
- package/src/handler.ts +218 -616
- package/src/handlerAuth.integration.spec.ts +33 -0
- package/src/handlerBundleRoutes.ts +259 -0
- package/src/handlerErrors.ts +7 -0
- package/src/handlerManagement.spec.ts +262 -0
- package/src/handlerParameters.ts +105 -0
- package/src/handlerReleaseCatalogRoutes.ts +154 -0
- package/src/handlerReleaseManagementRoutes.ts +213 -0
- package/src/handlerTypes.ts +96 -0
- package/src/handlerVersionRoutes.ts +12 -0
- package/src/index.ts +22 -2
- package/src/node.spec.ts +79 -10
- package/src/node.ts +20 -17
- package/src/releaseCatalogRoutes.integration.spec.ts +238 -0
- package/src/runtime.spec.ts +126 -959
- package/src/runtime.testFixtures.ts +71 -0
- package/src/schema/dsl-all-versions.spec.ts +26 -17
- package/src/schema/dsl.ts +41 -2
- package/src/schema/index.ts +3 -9
- package/src/schema/types.ts +9 -3
- package/src/schema/v1_0_0.spec.ts +146 -0
- package/src/schema/v1_0_0.ts +465 -0
- package/src/storageAccess.spec.ts +127 -32
- package/src/storageAccess.ts +106 -55
- package/src/types/index.ts +2 -1
- package/dist/db/bundleRows.cjs +0 -82
- package/dist/db/bundleRows.d.cts +0 -33
- package/dist/db/bundleRows.d.mts +0 -33
- package/dist/db/bundleRows.mjs +0 -79
- package/dist/db/pluginCore.cjs +0 -242
- package/dist/db/pluginCore.mjs +0 -241
- package/dist/db/schema/sqlMigrations.cjs +0 -74
- package/dist/db/schema/sqlMigrations.mjs +0 -73
- package/dist/db/schemaEnhancements.cjs +0 -20
- package/dist/db/schemaEnhancements.mjs +0 -19
- package/dist/route.cjs +0 -7
- package/dist/route.mjs +0 -7
- package/dist/schema/v0_21_0.cjs +0 -38
- package/dist/schema/v0_21_0.mjs +0 -37
- package/dist/schema/v0_29_0.cjs +0 -45
- package/dist/schema/v0_29_0.mjs +0 -44
- package/dist/schema/v0_31_0.cjs +0 -82
- package/dist/schema/v0_31_0.mjs +0 -80
- package/src/db/pluginCore.spec.ts +0 -1095
- package/src/db/pluginCore.ts +0 -475
- package/src/db/pluginUpdateCheck.bench.ts +0 -254
- package/src/db/schema/sqlMigrations.ts +0 -219
- package/src/db/schemaEnhancements.ts +0 -55
- package/src/route.ts +0 -7
- package/src/schema/v0_21_0.ts +0 -52
- package/src/schema/v0_29_0.ts +0 -62
- package/src/schema/v0_31_0.spec.ts +0 -73
- package/src/schema/v0_31_0.ts +0 -120
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { ReleaseCatalog } from "@hot-updater/core";
|
|
2
|
+
import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
|
|
3
|
+
|
|
4
|
+
import { requirePlatformParam, requireRouteParam } from "./handlerParameters";
|
|
5
|
+
import type { RouteHandler } from "./handlerTypes";
|
|
6
|
+
|
|
7
|
+
const CATALOG_CONTENT_TYPE =
|
|
8
|
+
"application/vnd.hot-updater.release-catalog+json; version=1";
|
|
9
|
+
const ORIGIN_CACHE_TTL_MS = 5_000;
|
|
10
|
+
const ORIGIN_CACHE_MAX_ENTRIES = 128;
|
|
11
|
+
|
|
12
|
+
const privateNotFound = (): Response =>
|
|
13
|
+
Response.json(
|
|
14
|
+
{ error: "Not found" },
|
|
15
|
+
{
|
|
16
|
+
status: 404,
|
|
17
|
+
headers: { "cache-control": "private, no-store" },
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const responseHash = async (body: string): Promise<string> => {
|
|
22
|
+
const digest = await crypto.subtle.digest(
|
|
23
|
+
"SHA-256",
|
|
24
|
+
new TextEncoder().encode(body),
|
|
25
|
+
);
|
|
26
|
+
return [...new Uint8Array(digest)]
|
|
27
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
28
|
+
.join("");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const catalogResponse = async (
|
|
32
|
+
catalog: ReleaseCatalog | null,
|
|
33
|
+
request: Request,
|
|
34
|
+
clientAccessHeaderName: string,
|
|
35
|
+
): Promise<Response> => {
|
|
36
|
+
if (catalog === null) return privateNotFound();
|
|
37
|
+
const body = JSON.stringify(catalog);
|
|
38
|
+
const etag = `"sha256:${await responseHash(body)}"`;
|
|
39
|
+
const headers = {
|
|
40
|
+
"cache-control": "public, max-age=0, s-maxage=5",
|
|
41
|
+
"content-type": CATALOG_CONTENT_TYPE,
|
|
42
|
+
etag,
|
|
43
|
+
vary: `Accept-Encoding, ${clientAccessHeaderName}`,
|
|
44
|
+
};
|
|
45
|
+
if (request.headers.get("if-none-match") === etag) {
|
|
46
|
+
return new Response(null, { headers, status: 304 });
|
|
47
|
+
}
|
|
48
|
+
return new Response(body, { headers, status: 200 });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createReleaseCatalogRouteHandlers = (
|
|
52
|
+
clientAccessHeaderName = "x-api-key",
|
|
53
|
+
): Record<string, RouteHandler> => {
|
|
54
|
+
const cache = new Map<
|
|
55
|
+
string,
|
|
56
|
+
{ readonly catalog: ReleaseCatalog; readonly expiresAt: number }
|
|
57
|
+
>();
|
|
58
|
+
const inFlight = new Map<string, Promise<ReleaseCatalog | null>>();
|
|
59
|
+
const loadCatalog = async (
|
|
60
|
+
key: string,
|
|
61
|
+
load: () => Promise<ReleaseCatalog | null>,
|
|
62
|
+
): Promise<ReleaseCatalog | null> => {
|
|
63
|
+
const cached = cache.get(key);
|
|
64
|
+
if (cached !== undefined && cached.expiresAt > Date.now()) {
|
|
65
|
+
cache.delete(key);
|
|
66
|
+
cache.set(key, cached);
|
|
67
|
+
return cached.catalog;
|
|
68
|
+
}
|
|
69
|
+
if (cached !== undefined) cache.delete(key);
|
|
70
|
+
const pending = inFlight.get(key);
|
|
71
|
+
if (pending !== undefined) return pending;
|
|
72
|
+
|
|
73
|
+
const next = load().then((catalog) => {
|
|
74
|
+
if (catalog !== null) {
|
|
75
|
+
cache.set(key, {
|
|
76
|
+
catalog,
|
|
77
|
+
expiresAt: Date.now() + ORIGIN_CACHE_TTL_MS,
|
|
78
|
+
});
|
|
79
|
+
while (cache.size > ORIGIN_CACHE_MAX_ENTRIES) {
|
|
80
|
+
const oldestKey = cache.keys().next().value as string | undefined;
|
|
81
|
+
if (oldestKey === undefined) break;
|
|
82
|
+
cache.delete(oldestKey);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return catalog;
|
|
86
|
+
});
|
|
87
|
+
inFlight.set(key, next);
|
|
88
|
+
try {
|
|
89
|
+
return await next;
|
|
90
|
+
} finally {
|
|
91
|
+
inFlight.delete(key);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
appVersionReleaseCatalog: async (params, request, api) => {
|
|
97
|
+
if (api.getReleaseCatalog === undefined) return privateNotFound();
|
|
98
|
+
const rawAppVersion = requireRouteParam(params, "appVersion");
|
|
99
|
+
const appVersion = canonicalizeAppVersion(rawAppVersion);
|
|
100
|
+
if (appVersion === null || appVersion !== rawAppVersion) {
|
|
101
|
+
return Response.json(
|
|
102
|
+
{ error: "Invalid app version" },
|
|
103
|
+
{ status: 400, headers: { "cache-control": "private, no-store" } },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const input = {
|
|
107
|
+
appVersion,
|
|
108
|
+
channelKey: requireRouteParam(params, "channelKey"),
|
|
109
|
+
platform: requirePlatformParam(params),
|
|
110
|
+
strategy: "APP_VERSION",
|
|
111
|
+
} as const;
|
|
112
|
+
return catalogResponse(
|
|
113
|
+
await loadCatalog(`app-version:${JSON.stringify(input)}`, () =>
|
|
114
|
+
api.getReleaseCatalog!(input),
|
|
115
|
+
),
|
|
116
|
+
request,
|
|
117
|
+
clientAccessHeaderName,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
fingerprintReleaseCatalog: async (params, request, api) => {
|
|
122
|
+
if (api.getReleaseCatalog === undefined) return privateNotFound();
|
|
123
|
+
const input = {
|
|
124
|
+
channelKey: requireRouteParam(params, "channelKey"),
|
|
125
|
+
fingerprintHash: requireRouteParam(params, "fingerprintHash"),
|
|
126
|
+
platform: requirePlatformParam(params),
|
|
127
|
+
strategy: "FINGERPRINT",
|
|
128
|
+
} as const;
|
|
129
|
+
return catalogResponse(
|
|
130
|
+
await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () =>
|
|
131
|
+
api.getReleaseCatalog!(input),
|
|
132
|
+
),
|
|
133
|
+
request,
|
|
134
|
+
clientAccessHeaderName,
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
artifact: async (params, _request, api) => {
|
|
139
|
+
if (api.getArtifactInfo === undefined) return privateNotFound();
|
|
140
|
+
const info = await api.getArtifactInfo(
|
|
141
|
+
requireRouteParam(params, "targetBundleId"),
|
|
142
|
+
requireRouteParam(params, "currentBundleId"),
|
|
143
|
+
);
|
|
144
|
+
if (info === null) return privateNotFound();
|
|
145
|
+
return new Response(JSON.stringify(info), {
|
|
146
|
+
status: 200,
|
|
147
|
+
headers: {
|
|
148
|
+
"cache-control": "private, no-store",
|
|
149
|
+
"content-type": "application/json",
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReleaseCatalogMutationError,
|
|
3
|
+
ReleaseManagementError,
|
|
4
|
+
type DatabaseCommit,
|
|
5
|
+
type ReleasePolicyPatch,
|
|
6
|
+
} from "@hot-updater/plugin-core";
|
|
7
|
+
|
|
8
|
+
import { HandlerBadRequestError } from "./handlerErrors";
|
|
9
|
+
import { decodeMaybe, requireRouteParam } from "./handlerParameters";
|
|
10
|
+
import type { RouteHandler } from "./handlerTypes";
|
|
11
|
+
|
|
12
|
+
const unavailable = (): Response =>
|
|
13
|
+
Response.json({ error: "Not found" }, { status: 404 });
|
|
14
|
+
|
|
15
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
|
|
18
|
+
const parseRevision = (value: unknown): number | undefined => {
|
|
19
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
20
|
+
const revision = typeof value === "number" ? value : Number(value);
|
|
21
|
+
if (!Number.isSafeInteger(revision) || revision < 1) {
|
|
22
|
+
throw new HandlerBadRequestError("Invalid expected Release revision");
|
|
23
|
+
}
|
|
24
|
+
return revision;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const parsePolicyInput = async (
|
|
28
|
+
request: Request,
|
|
29
|
+
): Promise<{
|
|
30
|
+
readonly expectedRevision?: number;
|
|
31
|
+
readonly patch: ReleasePolicyPatch;
|
|
32
|
+
}> => {
|
|
33
|
+
const body: unknown = await request.json();
|
|
34
|
+
if (!isRecord(body) || !isRecord(body.patch)) {
|
|
35
|
+
throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
expectedRevision: parseRevision(body.expectedRevision),
|
|
39
|
+
patch: body.patch as ReleasePolicyPatch,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mutationResponse = async <T>(operation: () => Promise<T>) => {
|
|
44
|
+
try {
|
|
45
|
+
return Response.json({ data: await operation() });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (
|
|
48
|
+
error instanceof ReleaseManagementError ||
|
|
49
|
+
error instanceof ReleaseCatalogMutationError
|
|
50
|
+
) {
|
|
51
|
+
const status =
|
|
52
|
+
error.code === "RELEASE_NOT_FOUND"
|
|
53
|
+
? 404
|
|
54
|
+
: error.code === "VERSION_CONFLICT"
|
|
55
|
+
? 409
|
|
56
|
+
: 400;
|
|
57
|
+
return Response.json(
|
|
58
|
+
{ code: error.code, error: error.message },
|
|
59
|
+
{ status },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const createReleaseManagementRouteHandlers = (): Record<
|
|
67
|
+
string,
|
|
68
|
+
RouteHandler
|
|
69
|
+
> => ({
|
|
70
|
+
getRelease: async (params, _request, api) => {
|
|
71
|
+
if (api.getReleaseById === undefined) return unavailable();
|
|
72
|
+
const row = await api.getReleaseById(requireRouteParam(params, "id"));
|
|
73
|
+
return row === null
|
|
74
|
+
? unavailable()
|
|
75
|
+
: Response.json({ data: row }, { status: 200 });
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
getReleases: async (_params, request, api) => {
|
|
79
|
+
const url = new URL(request.url);
|
|
80
|
+
const scopeKey = url.searchParams.get("scopeKey");
|
|
81
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
82
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 1000) {
|
|
83
|
+
throw new HandlerBadRequestError("Invalid Release scope query");
|
|
84
|
+
}
|
|
85
|
+
if (scopeKey !== null) {
|
|
86
|
+
if (scopeKey.length === 0 || api.getReleasesByScope === undefined) {
|
|
87
|
+
throw new HandlerBadRequestError("Invalid Release scope query");
|
|
88
|
+
}
|
|
89
|
+
const afterReleaseId =
|
|
90
|
+
url.searchParams.get("afterReleaseId") ?? undefined;
|
|
91
|
+
return Response.json({
|
|
92
|
+
data: await api.getReleasesByScope({
|
|
93
|
+
...(afterReleaseId === undefined ? {} : { afterReleaseId }),
|
|
94
|
+
limit,
|
|
95
|
+
scopeKey,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (api.getReleases === undefined) return unavailable();
|
|
100
|
+
const enabledValue = url.searchParams.get("enabled");
|
|
101
|
+
const enabled =
|
|
102
|
+
enabledValue === null
|
|
103
|
+
? undefined
|
|
104
|
+
: enabledValue === "true"
|
|
105
|
+
? true
|
|
106
|
+
: enabledValue === "false"
|
|
107
|
+
? false
|
|
108
|
+
: null;
|
|
109
|
+
const platformValue = url.searchParams.get("platform");
|
|
110
|
+
const platform =
|
|
111
|
+
platformValue === null
|
|
112
|
+
? undefined
|
|
113
|
+
: platformValue === "ios" || platformValue === "android"
|
|
114
|
+
? platformValue
|
|
115
|
+
: null;
|
|
116
|
+
if (enabled === null || platform === null) {
|
|
117
|
+
throw new HandlerBadRequestError("Invalid Release list filter");
|
|
118
|
+
}
|
|
119
|
+
return Response.json({
|
|
120
|
+
data: await api.getReleases({
|
|
121
|
+
afterReleaseId: url.searchParams.get("afterReleaseId") ?? undefined,
|
|
122
|
+
beforeReleaseId: url.searchParams.get("beforeReleaseId") ?? undefined,
|
|
123
|
+
bundleId: url.searchParams.get("bundleId") ?? undefined,
|
|
124
|
+
channelId: url.searchParams.get("channelId") ?? undefined,
|
|
125
|
+
enabled,
|
|
126
|
+
limit,
|
|
127
|
+
platform,
|
|
128
|
+
targetAppVersion: url.searchParams.get("targetAppVersion") ?? undefined,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
getReleaseCatalogRow: async (params, _request, api) => {
|
|
134
|
+
if (api.getReleaseCatalogByScopeKey === undefined) return unavailable();
|
|
135
|
+
const row = await api.getReleaseCatalogByScopeKey(
|
|
136
|
+
decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "",
|
|
137
|
+
);
|
|
138
|
+
return row === null
|
|
139
|
+
? unavailable()
|
|
140
|
+
: Response.json({ data: row }, { status: 200 });
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
getReleaseCatalogs: async (_params, request, api) => {
|
|
144
|
+
if (api.getReleaseCatalogs === undefined) return unavailable();
|
|
145
|
+
const url = new URL(request.url);
|
|
146
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
147
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 1000) {
|
|
148
|
+
throw new HandlerBadRequestError("Invalid Release catalog list query");
|
|
149
|
+
}
|
|
150
|
+
return Response.json({
|
|
151
|
+
data: await api.getReleaseCatalogs({
|
|
152
|
+
afterScopeKey: url.searchParams.get("afterScopeKey") ?? undefined,
|
|
153
|
+
limit,
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
updateRelease: async (params, request, api) => {
|
|
159
|
+
if (api.updateReleasePolicy === undefined) return unavailable();
|
|
160
|
+
const input = await parsePolicyInput(request);
|
|
161
|
+
return mutationResponse(() =>
|
|
162
|
+
api.updateReleasePolicy!({
|
|
163
|
+
...input,
|
|
164
|
+
releaseId: requireRouteParam(params, "id"),
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
preflightRelease: async (params, request, api) => {
|
|
170
|
+
if (api.preflightReleasePolicy === undefined) return unavailable();
|
|
171
|
+
const input = await parsePolicyInput(request);
|
|
172
|
+
return mutationResponse(() =>
|
|
173
|
+
api.preflightReleasePolicy!({
|
|
174
|
+
...input,
|
|
175
|
+
releaseId: requireRouteParam(params, "id"),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
deleteRelease: async (params, request, api) => {
|
|
181
|
+
if (api.deleteRelease === undefined) return unavailable();
|
|
182
|
+
const releaseId = requireRouteParam(params, "id");
|
|
183
|
+
const url = new URL(request.url);
|
|
184
|
+
if (url.searchParams.get("confirm") !== releaseId) {
|
|
185
|
+
throw new HandlerBadRequestError(
|
|
186
|
+
"Release hard deletion requires confirm=<release-id>",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return mutationResponse(() =>
|
|
190
|
+
api.deleteRelease!({
|
|
191
|
+
expectedRevision: parseRevision(
|
|
192
|
+
url.searchParams.get("expectedRevision"),
|
|
193
|
+
),
|
|
194
|
+
releaseId,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
rebuildReleaseCatalog: async (params, _request, api) => {
|
|
200
|
+
if (api.rebuildReleaseCatalog === undefined) return unavailable();
|
|
201
|
+
return mutationResponse(() =>
|
|
202
|
+
api.rebuildReleaseCatalog!(
|
|
203
|
+
decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "",
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
commitDatabase: async (_params, request, api) => {
|
|
209
|
+
if (api.commitDatabase === undefined) return unavailable();
|
|
210
|
+
const input = (await request.json()) as DatabaseCommit;
|
|
211
|
+
return Response.json({ data: await api.commitDatabase(input) });
|
|
212
|
+
},
|
|
213
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ArtifactInfo, Bundle, ReleaseCatalog } from "@hot-updater/core";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelDeleteInput,
|
|
4
|
+
ChannelDeleteResult,
|
|
5
|
+
ChannelInsertInput,
|
|
6
|
+
ChannelInsertResult,
|
|
7
|
+
ChannelRow,
|
|
8
|
+
DatabaseBundleQueryOptions,
|
|
9
|
+
DatabaseCommit,
|
|
10
|
+
DatabaseCommitResult,
|
|
11
|
+
ReleaseCatalogRow,
|
|
12
|
+
ReleaseCatalogMutationPreflight,
|
|
13
|
+
ReleaseCatalogMutationResult,
|
|
14
|
+
ReleaseCatalogRebuildResult,
|
|
15
|
+
ReleasePolicyPatch,
|
|
16
|
+
ReleaseRow,
|
|
17
|
+
} from "@hot-updater/plugin-core";
|
|
18
|
+
|
|
19
|
+
import type { ReleaseCatalogRequest } from "./db/releaseCatalog";
|
|
20
|
+
import type { PaginatedResult } from "./types";
|
|
21
|
+
|
|
22
|
+
export interface HandlerAPI {
|
|
23
|
+
getReleaseCatalog?: (
|
|
24
|
+
input: ReleaseCatalogRequest,
|
|
25
|
+
) => Promise<ReleaseCatalog | null>;
|
|
26
|
+
getArtifactInfo?: (
|
|
27
|
+
targetBundleId: string,
|
|
28
|
+
currentBundleId: string,
|
|
29
|
+
) => Promise<ArtifactInfo | null>;
|
|
30
|
+
getReleaseById?: (id: string) => Promise<ReleaseRow | null>;
|
|
31
|
+
getReleasesByScope?: (input: {
|
|
32
|
+
readonly scopeKey: string;
|
|
33
|
+
readonly afterReleaseId?: string;
|
|
34
|
+
readonly limit: number;
|
|
35
|
+
}) => Promise<readonly ReleaseRow[]>;
|
|
36
|
+
getReleases?: (input: {
|
|
37
|
+
readonly afterReleaseId?: string;
|
|
38
|
+
readonly beforeReleaseId?: string;
|
|
39
|
+
readonly bundleId?: string;
|
|
40
|
+
readonly channelId?: string;
|
|
41
|
+
readonly enabled?: boolean;
|
|
42
|
+
readonly platform?: "ios" | "android";
|
|
43
|
+
readonly targetAppVersion?: string;
|
|
44
|
+
readonly limit: number;
|
|
45
|
+
}) => Promise<readonly ReleaseRow[]>;
|
|
46
|
+
getReleaseCatalogByScopeKey?: (
|
|
47
|
+
scopeKey: string,
|
|
48
|
+
) => Promise<ReleaseCatalogRow | null>;
|
|
49
|
+
getReleaseCatalogs?: (input: {
|
|
50
|
+
readonly afterScopeKey?: string;
|
|
51
|
+
readonly limit: number;
|
|
52
|
+
}) => Promise<readonly ReleaseCatalogRow[]>;
|
|
53
|
+
updateReleasePolicy?: (input: {
|
|
54
|
+
readonly expectedRevision?: number;
|
|
55
|
+
readonly patch: ReleasePolicyPatch;
|
|
56
|
+
readonly releaseId: string;
|
|
57
|
+
}) => Promise<ReleaseCatalogMutationResult>;
|
|
58
|
+
preflightReleasePolicy?: (input: {
|
|
59
|
+
readonly expectedRevision?: number;
|
|
60
|
+
readonly patch: ReleasePolicyPatch;
|
|
61
|
+
readonly releaseId: string;
|
|
62
|
+
}) => Promise<ReleaseCatalogMutationPreflight>;
|
|
63
|
+
deleteRelease?: (input: {
|
|
64
|
+
readonly expectedRevision?: number;
|
|
65
|
+
readonly releaseId: string;
|
|
66
|
+
}) => Promise<ReleaseCatalogMutationResult>;
|
|
67
|
+
rebuildReleaseCatalog?: (
|
|
68
|
+
scopeKey: string,
|
|
69
|
+
) => Promise<ReleaseCatalogRebuildResult>;
|
|
70
|
+
commitDatabase?: (input: DatabaseCommit) => Promise<DatabaseCommitResult>;
|
|
71
|
+
getBundleById: (id: string) => Promise<Bundle | null>;
|
|
72
|
+
getBundles: (options: DatabaseBundleQueryOptions) => Promise<PaginatedResult>;
|
|
73
|
+
insertBundle: (bundle: Bundle) => Promise<void>;
|
|
74
|
+
insertBundles?: (bundles: readonly Bundle[]) => Promise<void>;
|
|
75
|
+
updateBundleById: (
|
|
76
|
+
bundleId: string,
|
|
77
|
+
bundle: Partial<Bundle>,
|
|
78
|
+
) => Promise<void>;
|
|
79
|
+
deleteBundleById: (bundleId: string) => Promise<void>;
|
|
80
|
+
getChannels: () => Promise<readonly ChannelRow[]>;
|
|
81
|
+
insertChannel: (input: ChannelInsertInput) => Promise<ChannelInsertResult>;
|
|
82
|
+
deleteChannel: (input: ChannelDeleteInput) => Promise<ChannelDeleteResult>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type HotUpdaterHandler = (request: Request) => Promise<Response>;
|
|
86
|
+
|
|
87
|
+
export interface HotUpdaterHandlers {
|
|
88
|
+
readonly client: HotUpdaterHandler;
|
|
89
|
+
readonly admin: HotUpdaterHandler;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type RouteHandler = (
|
|
93
|
+
params: Record<string, string>,
|
|
94
|
+
request: Request,
|
|
95
|
+
api: HandlerAPI,
|
|
96
|
+
) => Promise<Response>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RouteHandler } from "./handlerTypes";
|
|
2
|
+
import { HOT_UPDATER_SERVER_VERSION } from "./version";
|
|
3
|
+
|
|
4
|
+
export const HOT_UPDATER_INFRASTRUCTURE_GENERATION = 1;
|
|
5
|
+
|
|
6
|
+
export const createVersionRouteHandlers = (): Record<string, RouteHandler> => ({
|
|
7
|
+
version: async () =>
|
|
8
|
+
Response.json({
|
|
9
|
+
infrastructureGeneration: HOT_UPDATER_INFRASTRUCTURE_GENERATION,
|
|
10
|
+
version: HOT_UPDATER_SERVER_VERSION,
|
|
11
|
+
}),
|
|
12
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { createHandlers } from "./handler";
|
|
2
|
+
export type {
|
|
3
|
+
HandlerAPI,
|
|
4
|
+
HotUpdaterHandler,
|
|
5
|
+
HotUpdaterHandlers,
|
|
6
|
+
} from "./handler";
|
|
7
|
+
export { createAnalyticsProvider } from "./analytics/bounded/provider";
|
|
8
|
+
export type * from "./analytics/domain";
|
|
9
|
+
export type { AnalyticsProvider } from "./analytics/types";
|
|
10
|
+
export {
|
|
11
|
+
API_KEY_HEADER_NAME,
|
|
12
|
+
createApiKey,
|
|
13
|
+
provisionApiKey,
|
|
14
|
+
registerApiKey,
|
|
15
|
+
} from "./apiKeys";
|
|
16
|
+
export type {
|
|
17
|
+
ApiKeyManagementAPI,
|
|
18
|
+
ApiKeyMetadata,
|
|
19
|
+
CreatedApiKey,
|
|
20
|
+
} from "./apiKeys";
|
|
3
21
|
export { createHotUpdater } from "./createHotUpdaterCore";
|
|
4
22
|
export type {
|
|
23
|
+
ClientAccessPolicy,
|
|
5
24
|
CreateHotUpdaterOptions,
|
|
6
25
|
HotUpdaterAPI,
|
|
7
26
|
RuntimeHotUpdaterAPI,
|
|
8
27
|
} from "./createHotUpdaterCore";
|
|
9
28
|
export * from "./types";
|
|
10
29
|
export { HOT_UPDATER_SERVER_VERSION } from "./version";
|
|
30
|
+
export { HOT_UPDATER_INFRASTRUCTURE_GENERATION } from "./handlerVersionRoutes";
|
package/src/node.spec.ts
CHANGED
|
@@ -4,14 +4,12 @@ import { toNodeHandler } from "./node";
|
|
|
4
4
|
|
|
5
5
|
describe("server node entry", () => {
|
|
6
6
|
it("converts a Web Request handler to Node middleware", async () => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
const middleware = toNodeHandler(hotUpdater);
|
|
7
|
+
const middleware = toNodeHandler(async (request: Request) =>
|
|
8
|
+
Response.json({
|
|
9
|
+
method: request.method,
|
|
10
|
+
pathname: new URL(request.url).pathname,
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
15
13
|
const headers = new Map<string, string | string[]>();
|
|
16
14
|
const response = {
|
|
17
15
|
body: "",
|
|
@@ -23,8 +21,8 @@ describe("server node entry", () => {
|
|
|
23
21
|
setHeader(name: string, value: string | string[]) {
|
|
24
22
|
headers.set(name, value);
|
|
25
23
|
},
|
|
26
|
-
send(body:
|
|
27
|
-
this.body = body;
|
|
24
|
+
send(body: Uint8Array) {
|
|
25
|
+
this.body = new TextDecoder().decode(body);
|
|
28
26
|
},
|
|
29
27
|
end() {},
|
|
30
28
|
};
|
|
@@ -47,4 +45,75 @@ describe("server node entry", () => {
|
|
|
47
45
|
pathname: "/api/check",
|
|
48
46
|
});
|
|
49
47
|
});
|
|
48
|
+
|
|
49
|
+
it("uses the mount-relative URL supplied by Express", async () => {
|
|
50
|
+
const middleware = toNodeHandler(async (request: Request) =>
|
|
51
|
+
Response.json({ pathname: new URL(request.url).pathname }),
|
|
52
|
+
);
|
|
53
|
+
const response = {
|
|
54
|
+
body: "",
|
|
55
|
+
statusCode: 0,
|
|
56
|
+
status(code: number) {
|
|
57
|
+
this.statusCode = code;
|
|
58
|
+
return this;
|
|
59
|
+
},
|
|
60
|
+
setHeader() {},
|
|
61
|
+
send(body: Uint8Array) {
|
|
62
|
+
this.body = new TextDecoder().decode(body);
|
|
63
|
+
},
|
|
64
|
+
end() {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await middleware(
|
|
68
|
+
{
|
|
69
|
+
method: "GET",
|
|
70
|
+
url: "/channels",
|
|
71
|
+
headers: { host: "example.com" },
|
|
72
|
+
protocol: "https",
|
|
73
|
+
get: (name: string) => (name === "host" ? "example.com" : undefined),
|
|
74
|
+
},
|
|
75
|
+
response,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(JSON.parse(response.body)).toEqual({
|
|
79
|
+
pathname: "/channels",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("preserves binary response bytes", async () => {
|
|
84
|
+
const bytes = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0xff, 0xfe, 0x00]);
|
|
85
|
+
const middleware = toNodeHandler(
|
|
86
|
+
async () =>
|
|
87
|
+
new Response(bytes, {
|
|
88
|
+
headers: { "content-type": "application/zip" },
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
const response = {
|
|
92
|
+
body: new Uint8Array(),
|
|
93
|
+
statusCode: 0,
|
|
94
|
+
status(code: number) {
|
|
95
|
+
this.statusCode = code;
|
|
96
|
+
return this;
|
|
97
|
+
},
|
|
98
|
+
setHeader() {},
|
|
99
|
+
send(body: Uint8Array) {
|
|
100
|
+
this.body = new Uint8Array(body);
|
|
101
|
+
},
|
|
102
|
+
end() {},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await middleware(
|
|
106
|
+
{
|
|
107
|
+
method: "GET",
|
|
108
|
+
url: "/hot-updater/storage/token/signature",
|
|
109
|
+
headers: { host: "example.com" },
|
|
110
|
+
protocol: "https",
|
|
111
|
+
get: (name: string) => (name === "host" ? "example.com" : undefined),
|
|
112
|
+
},
|
|
113
|
+
response,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(response.statusCode).toBe(200);
|
|
117
|
+
expect([...response.body]).toEqual([...bytes]);
|
|
118
|
+
});
|
|
50
119
|
});
|
package/src/node.ts
CHANGED
|
@@ -14,16 +14,13 @@ interface NodeRequest {
|
|
|
14
14
|
interface NodeResponse {
|
|
15
15
|
status(code: number): NodeResponse;
|
|
16
16
|
setHeader(name: string, value: string | string[]): void;
|
|
17
|
-
send(body: string): void;
|
|
17
|
+
send(body: string | Uint8Array): void;
|
|
18
18
|
end(): void;
|
|
19
19
|
[key: string]: unknown;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
type HandlerHotUpdaterAPI = {
|
|
23
|
-
readonly handler: (request: Request) => Promise<Response>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
22
|
export { HOT_UPDATER_SERVER_VERSION } from "./version";
|
|
23
|
+
export { HOT_UPDATER_INFRASTRUCTURE_GENERATION } from "./handlerVersionRoutes";
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
26
|
* Converts a Hot Updater handler to a Node.js-compatible middleware
|
|
@@ -36,22 +33,28 @@ export { HOT_UPDATER_SERVER_VERSION } from "./version";
|
|
|
36
33
|
*
|
|
37
34
|
* const app = express();
|
|
38
35
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
36
|
+
* app.use(
|
|
37
|
+
* "/hot-updater/admin",
|
|
38
|
+
* bearerMiddleware,
|
|
39
|
+
* express.json({ limit: "1mb" }),
|
|
40
|
+
* toNodeHandler(hotUpdater.handlers.admin),
|
|
41
|
+
* );
|
|
42
|
+
* app.use(
|
|
43
|
+
* "/hot-updater",
|
|
44
|
+
* express.json({ limit: "1mb" }),
|
|
45
|
+
* toNodeHandler(hotUpdater.handlers.client),
|
|
46
|
+
* );
|
|
44
47
|
* ```
|
|
45
48
|
*/
|
|
46
49
|
export function toNodeHandler(
|
|
47
|
-
|
|
50
|
+
handler: HotUpdaterHandler,
|
|
48
51
|
): (req: any, res: any, next?: any) => Promise<void> {
|
|
49
52
|
return async (req: NodeRequest, res: NodeResponse) => {
|
|
50
53
|
try {
|
|
51
54
|
// Build full URL
|
|
52
55
|
const protocol = req.protocol || "http";
|
|
53
56
|
const host = req.get?.("host") || "localhost";
|
|
54
|
-
const url = `${protocol}://${host}${req.url
|
|
57
|
+
const url = `${protocol}://${host}${req.url ?? "/"}`;
|
|
55
58
|
|
|
56
59
|
// Convert headers to Web Headers
|
|
57
60
|
const headers = new Headers();
|
|
@@ -80,8 +83,7 @@ export function toNodeHandler(
|
|
|
80
83
|
body,
|
|
81
84
|
});
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
const response = await hotUpdater.handler(webRequest);
|
|
86
|
+
const response = await handler(webRequest);
|
|
85
87
|
|
|
86
88
|
// Set status code
|
|
87
89
|
res.status(response.status);
|
|
@@ -92,9 +94,9 @@ export function toNodeHandler(
|
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
// Send response body
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
res.send(
|
|
97
|
+
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
98
|
+
if (responseBody.length > 0) {
|
|
99
|
+
res.send(responseBody);
|
|
98
100
|
} else {
|
|
99
101
|
res.end();
|
|
100
102
|
}
|
|
@@ -106,3 +108,4 @@ export function toNodeHandler(
|
|
|
106
108
|
}
|
|
107
109
|
};
|
|
108
110
|
}
|
|
111
|
+
import type { HotUpdaterHandler } from "./handlerTypes";
|