@hot-updater/firebase 1.0.0-rc.43 → 1.0.0-rc.45
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/firebase/functions/index.cjs +343 -407
- package/dist/index.cjs +0 -15
- package/dist/index.d.cts +2 -1016
- package/dist/index.d.mts +2 -1016
- package/dist/index.mjs +1 -15
- package/dist/init/index.cjs +18 -28
- package/dist/init/index.mjs +18 -28
- package/package.json +7 -7
|
@@ -1426,31 +1426,6 @@ var ReleaseCatalogMutationError = class extends Error {
|
|
|
1426
1426
|
}
|
|
1427
1427
|
};
|
|
1428
1428
|
//#endregion
|
|
1429
|
-
//#region ../plugin-core/dist/storageDownloadPath.mjs
|
|
1430
|
-
const decodeBase64Url$1 = (value) => {
|
|
1431
|
-
try {
|
|
1432
|
-
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
1433
|
-
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
|
|
1434
|
-
return new TextDecoder(void 0, { fatal: true }).decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)));
|
|
1435
|
-
} catch {
|
|
1436
|
-
return null;
|
|
1437
|
-
}
|
|
1438
|
-
};
|
|
1439
|
-
const parseStorageDownloadPath = (path) => {
|
|
1440
|
-
const match = /^\/storage\/([^/]+)\/([^/]+)$/.exec(path);
|
|
1441
|
-
if (!match) return null;
|
|
1442
|
-
const storageUri = decodeBase64Url$1(match[1]);
|
|
1443
|
-
if (storageUri === null) return null;
|
|
1444
|
-
try {
|
|
1445
|
-
return {
|
|
1446
|
-
signature: decodeURIComponent(match[2]),
|
|
1447
|
-
storageUri
|
|
1448
|
-
};
|
|
1449
|
-
} catch {
|
|
1450
|
-
return null;
|
|
1451
|
-
}
|
|
1452
|
-
};
|
|
1453
|
-
//#endregion
|
|
1454
1429
|
//#region ../plugin-core/dist/serverPlugin/definePlugin.mjs
|
|
1455
1430
|
/** Declares a plugin; Hot Updater's own plugins and third-party ones use the same contract. */
|
|
1456
1431
|
const definePlugin = (plugin) => plugin;
|
|
@@ -4591,7 +4566,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
4591
4566
|
};
|
|
4592
4567
|
//#endregion
|
|
4593
4568
|
//#region ../../packages/server/dist/version.mjs
|
|
4594
|
-
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.
|
|
4569
|
+
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.45";
|
|
4595
4570
|
//#endregion
|
|
4596
4571
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
4597
4572
|
/**
|
|
@@ -6777,6 +6752,22 @@ const privateNotFound = () => Response.json({ error: "Not found" }, {
|
|
|
6777
6752
|
status: 404,
|
|
6778
6753
|
headers: { "cache-control": "private, no-store" }
|
|
6779
6754
|
});
|
|
6755
|
+
const badRequest = (error) => Response.json({ error }, {
|
|
6756
|
+
status: 400,
|
|
6757
|
+
headers: { "cache-control": "private, no-store" }
|
|
6758
|
+
});
|
|
6759
|
+
/**
|
|
6760
|
+
* A 400 for a catalog path whose scope is malformed, such as a channel name
|
|
6761
|
+
* where its base64url key belongs; null for a well-formed scope.
|
|
6762
|
+
*/
|
|
6763
|
+
const invalidScope = (input) => {
|
|
6764
|
+
try {
|
|
6765
|
+
createReleaseCatalogScopeKey(input);
|
|
6766
|
+
return null;
|
|
6767
|
+
} catch (error) {
|
|
6768
|
+
return badRequest(error instanceof Error ? error.message : "Invalid Release Catalog scope");
|
|
6769
|
+
}
|
|
6770
|
+
};
|
|
6780
6771
|
/**
|
|
6781
6772
|
* A scope with no catalog yet, such as a store version before its first
|
|
6782
6773
|
* OTA release: cached like a catalog, so its update checks don't all reach
|
|
@@ -6855,16 +6846,15 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
6855
6846
|
appVersionReleaseCatalog: async (params, request, { core }) => {
|
|
6856
6847
|
const rawAppVersion = requireRouteParam(params, "appVersion");
|
|
6857
6848
|
const appVersion = canonicalizeAppVersion(rawAppVersion);
|
|
6858
|
-
if (appVersion === null || appVersion !== rawAppVersion) return
|
|
6859
|
-
status: 400,
|
|
6860
|
-
headers: { "cache-control": "private, no-store" }
|
|
6861
|
-
});
|
|
6849
|
+
if (appVersion === null || appVersion !== rawAppVersion) return badRequest("Invalid app version");
|
|
6862
6850
|
const input = {
|
|
6863
6851
|
appVersion,
|
|
6864
6852
|
channelKey: requireRouteParam(params, "channelKey"),
|
|
6865
6853
|
platform: requirePlatformParam(params),
|
|
6866
6854
|
strategy: "APP_VERSION"
|
|
6867
6855
|
};
|
|
6856
|
+
const invalid = invalidScope(input);
|
|
6857
|
+
if (invalid) return invalid;
|
|
6868
6858
|
return catalogResponse(await loadCatalog(`app-version:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
6869
6859
|
},
|
|
6870
6860
|
fingerprintReleaseCatalog: async (params, request, { core }) => {
|
|
@@ -6874,6 +6864,8 @@ const createReleaseCatalogRouteHandlers = () => {
|
|
|
6874
6864
|
platform: requirePlatformParam(params),
|
|
6875
6865
|
strategy: "FINGERPRINT"
|
|
6876
6866
|
};
|
|
6867
|
+
const invalid = invalidScope(input);
|
|
6868
|
+
if (invalid) return invalid;
|
|
6877
6869
|
return catalogResponse(await loadCatalog(`fingerprint:${JSON.stringify(input)}`, () => core.getReleaseCatalog(input)), request);
|
|
6878
6870
|
},
|
|
6879
6871
|
artifactV1: async (params, _request, { core }) => {
|
|
@@ -6955,8 +6947,8 @@ const errorResponse = (error, status) => Response.json({ error }, {
|
|
|
6955
6947
|
headers: { "cache-control": "private, no-store" },
|
|
6956
6948
|
status
|
|
6957
6949
|
});
|
|
6958
|
-
/** `/version`
|
|
6959
|
-
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"
|
|
6950
|
+
/** `/version` never passes through the client policy. */
|
|
6951
|
+
const PUBLIC_CLIENT_ROUTES = /* @__PURE__ */ new Set(["version"]);
|
|
6960
6952
|
const routesOf = Symbol.for("@hot-updater/server/routes");
|
|
6961
6953
|
const withVary = (response, varyHeaders) => {
|
|
6962
6954
|
const cacheControl = response.headers.get("cache-control") ?? "";
|
|
@@ -7001,26 +6993,11 @@ const createRequestHandler = ({ api, clientPolicy, privateResponses = false, rou
|
|
|
7001
6993
|
});
|
|
7002
6994
|
}
|
|
7003
6995
|
};
|
|
7004
|
-
|
|
7005
|
-
const token = params.token;
|
|
7006
|
-
const signature = params.signature;
|
|
7007
|
-
if (!token || !signature) return errorResponse("Not found", 404);
|
|
7008
|
-
const response = await downloadStorageObject(token, signature);
|
|
7009
|
-
if (!response) return errorResponse("Not found", 404);
|
|
7010
|
-
const headers = new Headers(response.headers);
|
|
7011
|
-
if (!headers.has("cache-control")) headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
7012
|
-
return new Response(response.body, {
|
|
7013
|
-
headers,
|
|
7014
|
-
status: response.status,
|
|
7015
|
-
statusText: response.statusText
|
|
7016
|
-
});
|
|
7017
|
-
};
|
|
7018
|
-
function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, endpoints = [], plugins = [] }) {
|
|
6996
|
+
function createHotUpdaterHandlers({ api, clientPolicy, endpoints = [], plugins = [] }) {
|
|
7019
6997
|
const routeHandlers = {
|
|
7020
6998
|
...createVersionRouteHandlers(plugins),
|
|
7021
6999
|
...createReleaseCatalogRouteHandlers(),
|
|
7022
|
-
...createAdminRouteHandlers()
|
|
7023
|
-
...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
|
|
7000
|
+
...createAdminRouteHandlers()
|
|
7024
7001
|
};
|
|
7025
7002
|
const routes = [];
|
|
7026
7003
|
const shapes = /* @__PURE__ */ new Map();
|
|
@@ -7039,7 +7016,6 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
|
|
|
7039
7016
|
const clientRouter = createRouter();
|
|
7040
7017
|
const addClientRoute = mount(clientRouter, false);
|
|
7041
7018
|
addClientRoute("GET", "/version", "version");
|
|
7042
|
-
if (downloadStorageObject !== void 0) addClientRoute("GET", "/storage/:token/:signature", "downloadStorageObject");
|
|
7043
7019
|
addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
|
|
7044
7020
|
addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
|
|
7045
7021
|
addClientRoute("GET", "/artifacts/v1/:targetBundleId/from/:currentBundleId", "artifactV1");
|
|
@@ -7071,33 +7047,23 @@ function createHotUpdaterHandlers({ api, clientPolicy, downloadStorageObject, en
|
|
|
7071
7047
|
}
|
|
7072
7048
|
//#endregion
|
|
7073
7049
|
//#region ../../packages/server/dist/storageAccess.mjs
|
|
7050
|
+
/** A URL devices download from, which only an absolute HTTP(S) URL can be. */
|
|
7074
7051
|
const assertRemoteUrl = (value) => {
|
|
7075
|
-
|
|
7076
|
-
|
|
7052
|
+
let protocol;
|
|
7053
|
+
try {
|
|
7054
|
+
protocol = new URL(value).protocol;
|
|
7055
|
+
} catch {}
|
|
7056
|
+
if (protocol !== "http:" && protocol !== "https:") throw new Error("Storage getDownloadUrl must resolve to an absolute HTTP(S) URL.");
|
|
7077
7057
|
return value;
|
|
7078
7058
|
};
|
|
7079
7059
|
const getStorageProtocol = (storageUri) => new URL(storageUri).protocol.replace(":", "");
|
|
7080
7060
|
const isRemoteUrlProtocol = (protocol) => protocol === "http" || protocol === "https";
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
const
|
|
7087
|
-
const leftBytes = new TextEncoder().encode(left);
|
|
7088
|
-
const rightBytes = new TextEncoder().encode(right);
|
|
7089
|
-
let difference = leftBytes.length ^ rightBytes.length;
|
|
7090
|
-
const length = Math.max(leftBytes.length, rightBytes.length);
|
|
7091
|
-
for (let index = 0; index < length; index += 1) difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
|
|
7092
|
-
return difference === 0;
|
|
7093
|
-
};
|
|
7094
|
-
const createStorageAccess = (storageAdapters) => {
|
|
7095
|
-
const protocols = /* @__PURE__ */ new Set();
|
|
7096
|
-
for (const storage of storageAdapters) {
|
|
7097
|
-
if (protocols.has(storage.protocol)) throw new Error(`Multiple storage adapters handle protocol: ${storage.protocol}`);
|
|
7098
|
-
protocols.add(storage.protocol);
|
|
7099
|
-
}
|
|
7100
|
-
const findStorage = (protocol) => storageAdapters.find((item) => item.protocol === protocol);
|
|
7061
|
+
/**
|
|
7062
|
+
* Reads and download URLs over the server's storage, for the URIs of its
|
|
7063
|
+
* protocol; an HTTP(S) URI is used as it is.
|
|
7064
|
+
*/
|
|
7065
|
+
const createStorageAccess = (storageAdapter) => {
|
|
7066
|
+
const findStorage = (protocol) => storageAdapter.protocol === protocol ? storageAdapter : void 0;
|
|
7101
7067
|
const readStorageResponse = async (storageUri) => {
|
|
7102
7068
|
const protocol = getStorageProtocol(storageUri);
|
|
7103
7069
|
const storage = findStorage(protocol);
|
|
@@ -7121,38 +7087,12 @@ const createStorageAccess = (storageAdapters) => {
|
|
|
7121
7087
|
}
|
|
7122
7088
|
if (!storage.getDownloadUrl) return null;
|
|
7123
7089
|
const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri });
|
|
7124
|
-
|
|
7125
|
-
return assertRemoteUrl(downloadUrl);
|
|
7126
|
-
} catch (error) {
|
|
7127
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) throw error;
|
|
7128
|
-
}
|
|
7129
|
-
return resolveDownloadPath(downloadUrl, storageUri);
|
|
7090
|
+
return assertRemoteUrl(downloadUrl);
|
|
7130
7091
|
};
|
|
7131
7092
|
const readStorageText = async (storageUri) => {
|
|
7132
7093
|
return (await readStorageResponse(storageUri))?.text() ?? null;
|
|
7133
7094
|
};
|
|
7134
7095
|
return {
|
|
7135
|
-
downloadStorageObject: storageAdapters.some((storage) => storage.getDownloadUrl !== void 0) ? async (storageUriToken, encodedSignature) => {
|
|
7136
|
-
const requestedPath = `/storage/${storageUriToken}/${encodedSignature}`;
|
|
7137
|
-
const requested = parseStorageDownloadPath(requestedPath);
|
|
7138
|
-
if (!requested) return null;
|
|
7139
|
-
let storage;
|
|
7140
|
-
try {
|
|
7141
|
-
storage = findStorage(getStorageProtocol(requested.storageUri));
|
|
7142
|
-
} catch {
|
|
7143
|
-
return null;
|
|
7144
|
-
}
|
|
7145
|
-
if (!storage?.getDownloadUrl || !storage.get) return null;
|
|
7146
|
-
const { url: downloadUrl } = await storage.getDownloadUrl({ storageUri: requested.storageUri });
|
|
7147
|
-
try {
|
|
7148
|
-
new URL(downloadUrl);
|
|
7149
|
-
return null;
|
|
7150
|
-
} catch {
|
|
7151
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(downloadUrl)) return null;
|
|
7152
|
-
}
|
|
7153
|
-
if (!tokensEqual(downloadUrl, requestedPath)) return null;
|
|
7154
|
-
return (await storage.get({ storageUri: requested.storageUri })).response;
|
|
7155
|
-
} : void 0,
|
|
7156
7096
|
readStorageText,
|
|
7157
7097
|
resolveFileUrl
|
|
7158
7098
|
};
|
|
@@ -7174,17 +7114,21 @@ const databaseOf = (value) => {
|
|
|
7174
7114
|
}
|
|
7175
7115
|
throw new HotUpdaterConfigError("database must be a Hot Updater 1.0 database, such as kyselyAdapter(...) or postgres(...). Upgrade the provider package that created it.");
|
|
7176
7116
|
};
|
|
7177
|
-
/**
|
|
7117
|
+
/** The configured storage, when it is a storage adapter. */
|
|
7118
|
+
const storageOf = (value) => {
|
|
7119
|
+
if (isRecord$4(value) && typeof value.name === "string" && typeof value.protocol === "string") return value;
|
|
7120
|
+
throw new HotUpdaterConfigError("storage must be a storage adapter, such as s3Storage(...): the one hot-updater.config.ts uploads with.");
|
|
7121
|
+
};
|
|
7122
|
+
/** `"public"`, or nothing. */
|
|
7178
7123
|
const isPublic = (value) => {
|
|
7179
7124
|
if (value === void 0) return false;
|
|
7180
7125
|
if (value === "public") return true;
|
|
7181
|
-
throw new HotUpdaterConfigError(
|
|
7126
|
+
throw new HotUpdaterConfigError("clientAccess must be \"public\"; to protect client routes, add a plugin that provides clientAuth to plugins.");
|
|
7182
7127
|
};
|
|
7183
7128
|
function createHotUpdater(options) {
|
|
7184
|
-
for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
|
|
7185
7129
|
const database = databaseOf(options.database);
|
|
7186
|
-
const storage =
|
|
7187
|
-
const {
|
|
7130
|
+
const storage = storageOf(options.storage);
|
|
7131
|
+
const { readStorageText, resolveFileUrl } = createStorageAccess(storage);
|
|
7188
7132
|
const publicClients = isPublic(options.clientAccess);
|
|
7189
7133
|
const configured = Object.freeze([...options.plugins ?? []]);
|
|
7190
7134
|
const plugins = assemblePlugins(configured, database, { storage: {
|
|
@@ -7203,7 +7147,6 @@ function createHotUpdater(options) {
|
|
|
7203
7147
|
const handlers = createHotUpdaterHandlers({
|
|
7204
7148
|
api: { core: plugins.core },
|
|
7205
7149
|
...clientPolicy === void 0 ? {} : { clientPolicy },
|
|
7206
|
-
downloadStorageObject,
|
|
7207
7150
|
endpoints: plugins.endpoints,
|
|
7208
7151
|
plugins: Object.keys(plugins.api)
|
|
7209
7152
|
});
|
|
@@ -7212,7 +7155,7 @@ function createHotUpdater(options) {
|
|
|
7212
7155
|
adapterName: database.name,
|
|
7213
7156
|
get handlers() {
|
|
7214
7157
|
if (!serving) {
|
|
7215
|
-
|
|
7158
|
+
assertStorageOperations(storage, ["get", "getDownloadUrl"]);
|
|
7216
7159
|
serving = true;
|
|
7217
7160
|
}
|
|
7218
7161
|
return handlers;
|
|
@@ -7236,297 +7179,49 @@ function createHotUpdater(options) {
|
|
|
7236
7179
|
};
|
|
7237
7180
|
}
|
|
7238
7181
|
//#endregion
|
|
7239
|
-
//#region
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
const
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
const
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
keyBytes: {
|
|
7257
|
-
pk: 1500,
|
|
7258
|
-
sk: 1500
|
|
7182
|
+
//#region ../api-keys/dist/server/apiKeys.mjs
|
|
7183
|
+
const API_KEY_HEADER_NAME = "x-api-key";
|
|
7184
|
+
const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
7185
|
+
const bytesToBase64Url = (bytes) => {
|
|
7186
|
+
let binary = "";
|
|
7187
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
7188
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
7189
|
+
};
|
|
7190
|
+
const isApiKey = (value) => {
|
|
7191
|
+
if (!BASE64URL_32_PATTERN.test(value)) return false;
|
|
7192
|
+
try {
|
|
7193
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
7194
|
+
const binary = atob(`${base64}=`);
|
|
7195
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
7196
|
+
return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
|
|
7197
|
+
} catch {
|
|
7198
|
+
return false;
|
|
7259
7199
|
}
|
|
7260
7200
|
};
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
const
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
7271
|
-
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
7272
|
-
/**
|
|
7273
|
-
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
7274
|
-
* of a row that expires, which Firestore's TTL policy deletes.
|
|
7275
|
-
*/
|
|
7276
|
-
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
7277
|
-
const toDocument = (key, row, expiresAt) => ({
|
|
7278
|
-
pk: key.pk,
|
|
7279
|
-
sk: key.sk,
|
|
7280
|
-
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
7281
|
-
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
7282
|
-
});
|
|
7283
|
-
const toRow$1 = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
7284
|
-
const holds = (snapshot, condition) => {
|
|
7285
|
-
if (condition === void 0) return true;
|
|
7286
|
-
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
7287
|
-
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
7201
|
+
const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
|
|
7202
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7203
|
+
try {
|
|
7204
|
+
const headers = new Headers();
|
|
7205
|
+
headers.set(value, "");
|
|
7206
|
+
return headers.keys().next().value;
|
|
7207
|
+
} catch {
|
|
7208
|
+
throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7209
|
+
}
|
|
7288
7210
|
};
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
10,
|
|
7294
|
-
13,
|
|
7295
|
-
14
|
|
7296
|
-
]);
|
|
7297
|
-
const codeOf = (error) => error.code;
|
|
7298
|
-
/** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
|
|
7299
|
-
const missingDatabase = (error) => {
|
|
7300
|
-
throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
7211
|
+
const hashApiKey = async (apiKey) => {
|
|
7212
|
+
if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
|
|
7213
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
|
|
7214
|
+
return bytesToBase64Url(new Uint8Array(digest));
|
|
7301
7215
|
};
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
* ops guard.
|
|
7307
|
-
*/
|
|
7308
|
-
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
7309
|
-
const items = firestore.collection(collection);
|
|
7310
|
-
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
7311
|
-
return {
|
|
7312
|
-
id: "firestore",
|
|
7313
|
-
limits: FIRESTORE_LIMITS,
|
|
7314
|
-
async get(keys) {
|
|
7315
|
-
const found = /* @__PURE__ */ new Map();
|
|
7316
|
-
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
7317
|
-
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
7318
|
-
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
7319
|
-
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow$1(snapshot.data()));
|
|
7320
|
-
}
|
|
7321
|
-
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
7322
|
-
},
|
|
7323
|
-
async query({ pk, gte, lt, order, limit, after }) {
|
|
7324
|
-
let query = items.where("pk", "==", pk);
|
|
7325
|
-
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
7326
|
-
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
7327
|
-
query = query.orderBy("sk", order);
|
|
7328
|
-
if (after !== void 0) query = query.startAfter(after);
|
|
7329
|
-
const size = Math.min(limit, nativePageSize ?? limit);
|
|
7330
|
-
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
7331
|
-
return {
|
|
7332
|
-
items: docs.map((document) => ({
|
|
7333
|
-
sk: document.get("sk"),
|
|
7334
|
-
value: toRow$1(document.data())
|
|
7335
|
-
})),
|
|
7336
|
-
more: docs.length === size
|
|
7337
|
-
};
|
|
7338
|
-
},
|
|
7339
|
-
async write(ops) {
|
|
7340
|
-
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
7341
|
-
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
7342
|
-
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
7343
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
7344
|
-
if (read.length > 0) {
|
|
7345
|
-
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
7346
|
-
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
7347
|
-
}
|
|
7348
|
-
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
7349
|
-
if (failed !== void 0) {
|
|
7350
|
-
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
7351
|
-
return {
|
|
7352
|
-
ok: false,
|
|
7353
|
-
failedOp: failed
|
|
7354
|
-
};
|
|
7355
|
-
}
|
|
7356
|
-
for (const [position, op] of ops.entries()) {
|
|
7357
|
-
const document = reference(op.key);
|
|
7358
|
-
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
7359
|
-
else if (op.type === "delete") transaction.delete(document);
|
|
7360
|
-
else if (op.type === "add") {
|
|
7361
|
-
if (snapshots.get(position)?.exists === false) {
|
|
7362
|
-
const created = { ...op.init };
|
|
7363
|
-
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
7364
|
-
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
7365
|
-
} else {
|
|
7366
|
-
const [[name, delta], ...more] = Object.entries(op.by);
|
|
7367
|
-
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
7368
|
-
transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
|
|
7369
|
-
}
|
|
7370
|
-
}
|
|
7371
|
-
}
|
|
7372
|
-
return { ok: true };
|
|
7373
|
-
}, { maxAttempts: 5 });
|
|
7374
|
-
try {
|
|
7375
|
-
try {
|
|
7376
|
-
const result = await attempt(false);
|
|
7377
|
-
if (result !== null) return result;
|
|
7378
|
-
} catch (error) {
|
|
7379
|
-
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
7380
|
-
}
|
|
7381
|
-
return await attempt(true);
|
|
7382
|
-
} catch (error) {
|
|
7383
|
-
if (TRANSIENT.has(codeOf(error))) return {
|
|
7384
|
-
ok: false,
|
|
7385
|
-
retry: true
|
|
7386
|
-
};
|
|
7387
|
-
return missingDatabase(error);
|
|
7388
|
-
}
|
|
7389
|
-
},
|
|
7390
|
-
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
7391
|
-
async deleteConsumed(keys) {
|
|
7392
|
-
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
7393
|
-
const batch = firestore.batch();
|
|
7394
|
-
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
7395
|
-
await batch.commit().catch(missingDatabase);
|
|
7396
|
-
}
|
|
7397
|
-
},
|
|
7398
|
-
async dispose() {}
|
|
7399
|
-
};
|
|
7216
|
+
const apiKeyId = () => {
|
|
7217
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
7218
|
+
crypto.getRandomValues(bytes);
|
|
7219
|
+
return `api-${bytesToBase64Url(bytes)}`;
|
|
7400
7220
|
};
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
return createKvAdapter({ store: createFirestoreStore({
|
|
7406
|
-
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
7407
|
-
collection
|
|
7408
|
-
}) });
|
|
7409
|
-
};
|
|
7410
|
-
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
7411
|
-
const firebaseDatabase = (config) => createEngineDatabase({
|
|
7412
|
-
name: "firebaseDatabase",
|
|
7413
|
-
adapter: adapterOf(config),
|
|
7414
|
-
aggregateBatching: config.aggregateBatching ?? {}
|
|
7415
|
-
});
|
|
7416
|
-
//#endregion
|
|
7417
|
-
//#region src/firebaseStorage.ts
|
|
7418
|
-
const firebaseStorage = (config) => {
|
|
7419
|
-
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
7420
|
-
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
7421
|
-
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
7422
|
-
const parseAndValidate = (storageUri) => {
|
|
7423
|
-
const parsed = parseStorageUri(storageUri, "gs");
|
|
7424
|
-
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
7425
|
-
return parsed;
|
|
7426
|
-
};
|
|
7427
|
-
return createStorageAdapter({
|
|
7428
|
-
name: "firebaseStorage",
|
|
7429
|
-
protocol: "gs",
|
|
7430
|
-
async put({ key, body, contentType }) {
|
|
7431
|
-
const storageKey = getStorageKey(key);
|
|
7432
|
-
const bytes = new Uint8Array(await new Response(body).arrayBuffer());
|
|
7433
|
-
await bucket.file(storageKey).save(bytes, { metadata: {
|
|
7434
|
-
contentType,
|
|
7435
|
-
cacheControl: "public, max-age=31536000, immutable"
|
|
7436
|
-
} });
|
|
7437
|
-
return { storageUri: createStorageUri({
|
|
7438
|
-
bucket: config.storageBucket,
|
|
7439
|
-
key: storageKey,
|
|
7440
|
-
protocol: "gs"
|
|
7441
|
-
}) };
|
|
7442
|
-
},
|
|
7443
|
-
async get({ storageUri }) {
|
|
7444
|
-
const { key } = parseAndValidate(storageUri);
|
|
7445
|
-
try {
|
|
7446
|
-
const file = bucket.file(key);
|
|
7447
|
-
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
7448
|
-
const headers = new Headers();
|
|
7449
|
-
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
7450
|
-
headers.set("content-length", String(metadata.size ?? body.byteLength));
|
|
7451
|
-
return { response: new Response(body, { headers }) };
|
|
7452
|
-
} catch (error) {
|
|
7453
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
|
|
7454
|
-
throw error;
|
|
7455
|
-
}
|
|
7456
|
-
},
|
|
7457
|
-
async getDownloadUrl({ storageUri }) {
|
|
7458
|
-
const { key } = parseAndValidate(storageUri);
|
|
7459
|
-
if (config.cdnUrl) {
|
|
7460
|
-
const storageUrl = new URL(storageUri);
|
|
7461
|
-
const downloadUrl = new URL(config.cdnUrl);
|
|
7462
|
-
downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
|
|
7463
|
-
downloadUrl.search = "";
|
|
7464
|
-
downloadUrl.hash = "";
|
|
7465
|
-
return { url: downloadUrl.toString() };
|
|
7466
|
-
}
|
|
7467
|
-
const [url] = await bucket.file(key).getSignedUrl({
|
|
7468
|
-
action: "read",
|
|
7469
|
-
expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
|
|
7470
|
-
});
|
|
7471
|
-
if (!url) throw new Error("Failed to generate Firebase download URL");
|
|
7472
|
-
return { url };
|
|
7473
|
-
},
|
|
7474
|
-
async exists({ storageUri }) {
|
|
7475
|
-
const { key } = parseAndValidate(storageUri);
|
|
7476
|
-
const [exists] = await bucket.file(key).exists();
|
|
7477
|
-
return { exists };
|
|
7478
|
-
},
|
|
7479
|
-
async delete({ storageUri }) {
|
|
7480
|
-
const { key } = parseAndValidate(storageUri);
|
|
7481
|
-
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
7482
|
-
return { deleted: true };
|
|
7483
|
-
}
|
|
7484
|
-
});
|
|
7485
|
-
};
|
|
7486
|
-
//#endregion
|
|
7487
|
-
//#region ../api-keys/dist/server/apiKeys.mjs
|
|
7488
|
-
const API_KEY_HEADER_NAME = "x-api-key";
|
|
7489
|
-
const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
7490
|
-
const bytesToBase64Url = (bytes) => {
|
|
7491
|
-
let binary = "";
|
|
7492
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
7493
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
7494
|
-
};
|
|
7495
|
-
const isApiKey = (value) => {
|
|
7496
|
-
if (!BASE64URL_32_PATTERN.test(value)) return false;
|
|
7497
|
-
try {
|
|
7498
|
-
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
7499
|
-
const binary = atob(`${base64}=`);
|
|
7500
|
-
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
7501
|
-
return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
|
|
7502
|
-
} catch {
|
|
7503
|
-
return false;
|
|
7504
|
-
}
|
|
7505
|
-
};
|
|
7506
|
-
const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
|
|
7507
|
-
if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7508
|
-
try {
|
|
7509
|
-
const headers = new Headers();
|
|
7510
|
-
headers.set(value, "");
|
|
7511
|
-
return headers.keys().next().value;
|
|
7512
|
-
} catch {
|
|
7513
|
-
throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7514
|
-
}
|
|
7515
|
-
};
|
|
7516
|
-
const hashApiKey = async (apiKey) => {
|
|
7517
|
-
if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
|
|
7518
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
|
|
7519
|
-
return bytesToBase64Url(new Uint8Array(digest));
|
|
7520
|
-
};
|
|
7521
|
-
const apiKeyId = () => {
|
|
7522
|
-
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
7523
|
-
crypto.getRandomValues(bytes);
|
|
7524
|
-
return `api-${bytesToBase64Url(bytes)}`;
|
|
7525
|
-
};
|
|
7526
|
-
const normalizeApiKeyName = (name) => {
|
|
7527
|
-
const normalized = name.trim();
|
|
7528
|
-
if (normalized.length === 0 || normalized.length > 64 || /[\p{Cc}\p{Cf}]/u.test(normalized)) throw new TypeError("API key names must contain 1-64 visible characters.");
|
|
7529
|
-
return normalized;
|
|
7221
|
+
const normalizeApiKeyName = (name) => {
|
|
7222
|
+
const normalized = name.trim();
|
|
7223
|
+
if (normalized.length === 0 || normalized.length > 64 || /[\p{Cc}\p{Cf}]/u.test(normalized)) throw new TypeError("API key names must contain 1-64 visible characters.");
|
|
7224
|
+
return normalized;
|
|
7530
7225
|
};
|
|
7531
7226
|
const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
|
|
7532
7227
|
const registerApiKey = async (input) => {
|
|
@@ -7633,7 +7328,7 @@ const apiKeysCli = (headerName) => ({ clientCredential: {
|
|
|
7633
7328
|
} });
|
|
7634
7329
|
//#endregion
|
|
7635
7330
|
//#region ../api-keys/dist/server/model.mjs
|
|
7636
|
-
const toRow = (row) => ({
|
|
7331
|
+
const toRow$1 = (row) => ({
|
|
7637
7332
|
id: row.id,
|
|
7638
7333
|
hash: row.hash,
|
|
7639
7334
|
name: row.name,
|
|
@@ -7658,7 +7353,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7658
7353
|
},
|
|
7659
7354
|
async findByHash(hash) {
|
|
7660
7355
|
const row = await db.findOne("api_keys", { hash });
|
|
7661
|
-
return row === null ? null : toRow(row);
|
|
7356
|
+
return row === null ? null : toRow$1(row);
|
|
7662
7357
|
},
|
|
7663
7358
|
/** Every key, newest first, then by id; the whole list is the result. */
|
|
7664
7359
|
async list() {
|
|
@@ -7672,7 +7367,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7672
7367
|
limit: 500,
|
|
7673
7368
|
...cursor === void 0 ? {} : { cursor }
|
|
7674
7369
|
});
|
|
7675
|
-
rows.push(...page.rows.map(toRow));
|
|
7370
|
+
rows.push(...page.rows.map(toRow$1));
|
|
7676
7371
|
cursor = page.next;
|
|
7677
7372
|
} while (cursor !== void 0);
|
|
7678
7373
|
return rows.sort((left, right) => right.created_at_ms - left.created_at_ms || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
@@ -7681,7 +7376,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7681
7376
|
const row = await tx.findOne("api_keys", { id });
|
|
7682
7377
|
if (row === null) return null;
|
|
7683
7378
|
tx.update("api_keys", row, { revoked_at_ms: revokedAtMs });
|
|
7684
|
-
return toRow({
|
|
7379
|
+
return toRow$1({
|
|
7685
7380
|
...row,
|
|
7686
7381
|
revoked_at_ms: revokedAtMs
|
|
7687
7382
|
});
|
|
@@ -11643,16 +11338,253 @@ const remoteConfig = () => {
|
|
|
11643
11338
|
return markOfficial(plugin);
|
|
11644
11339
|
};
|
|
11645
11340
|
//#endregion
|
|
11646
|
-
//#region src/
|
|
11341
|
+
//#region src/firebaseInfrastructureNames.ts
|
|
11647
11342
|
/**
|
|
11648
|
-
* The
|
|
11649
|
-
*
|
|
11343
|
+
* The one collection the storage engine keeps every item in. It namespaces
|
|
11344
|
+
* the data, so item partitions are table names with no prefix.
|
|
11650
11345
|
*/
|
|
11651
|
-
const
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11346
|
+
const FIREBASE_V1_COLLECTION = "hot_updater_v1";
|
|
11347
|
+
//#endregion
|
|
11348
|
+
//#region src/firestoreStore.ts
|
|
11349
|
+
/**
|
|
11350
|
+
* Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
|
|
11351
|
+
* transaction stays a conservative bound. Indexed values over 1,500 bytes are
|
|
11352
|
+
* truncated, so longer keys are refused rather than read inconsistently.
|
|
11353
|
+
*/
|
|
11354
|
+
const FIRESTORE_LIMITS = {
|
|
11355
|
+
items: 500,
|
|
11356
|
+
bytes: 1e7,
|
|
11357
|
+
itemBytes: 1e6,
|
|
11358
|
+
keyBytes: {
|
|
11359
|
+
pk: 1500,
|
|
11360
|
+
sk: 1500
|
|
11361
|
+
}
|
|
11362
|
+
};
|
|
11363
|
+
/** `getAll` reads at most this many documents a call. */
|
|
11364
|
+
const GET_BATCH = 300;
|
|
11365
|
+
/** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
|
|
11366
|
+
const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
|
|
11367
|
+
/**
|
|
11368
|
+
* A document is `{ pk, sk, row }`. Row values that are maps or arrays become
|
|
11369
|
+
* JSON text inside a map, since Firestore has no nested arrays and does not
|
|
11370
|
+
* keep map key order; `row` is exempt from single-field indexing.
|
|
11371
|
+
*/
|
|
11372
|
+
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
11373
|
+
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
11374
|
+
/**
|
|
11375
|
+
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
11376
|
+
* of a row that expires, which Firestore's TTL policy deletes.
|
|
11377
|
+
*/
|
|
11378
|
+
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
11379
|
+
const toDocument = (key, row, expiresAt) => ({
|
|
11380
|
+
pk: key.pk,
|
|
11381
|
+
sk: key.sk,
|
|
11382
|
+
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
11383
|
+
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
11384
|
+
});
|
|
11385
|
+
const toRow = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
11386
|
+
const holds = (snapshot, condition) => {
|
|
11387
|
+
if (condition === void 0) return true;
|
|
11388
|
+
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
11389
|
+
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
11390
|
+
};
|
|
11391
|
+
/** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
|
|
11392
|
+
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
11393
|
+
4,
|
|
11394
|
+
8,
|
|
11395
|
+
10,
|
|
11396
|
+
13,
|
|
11397
|
+
14
|
|
11398
|
+
]);
|
|
11399
|
+
const codeOf = (error) => error.code;
|
|
11400
|
+
/** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
|
|
11401
|
+
const missingDatabase = (error) => {
|
|
11402
|
+
throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
11403
|
+
};
|
|
11404
|
+
/**
|
|
11405
|
+
* The key-value store over one Firestore collection of `{ pk, sk, row }`
|
|
11406
|
+
* documents, queried through the composite indexes on (pk, sk) in both
|
|
11407
|
+
* directions. A write is one transaction that reads only the documents its
|
|
11408
|
+
* ops guard.
|
|
11409
|
+
*/
|
|
11410
|
+
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
11411
|
+
const items = firestore.collection(collection);
|
|
11412
|
+
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
11413
|
+
return {
|
|
11414
|
+
id: "firestore",
|
|
11415
|
+
limits: FIRESTORE_LIMITS,
|
|
11416
|
+
async get(keys) {
|
|
11417
|
+
const found = /* @__PURE__ */ new Map();
|
|
11418
|
+
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
11419
|
+
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
11420
|
+
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
11421
|
+
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow(snapshot.data()));
|
|
11422
|
+
}
|
|
11423
|
+
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
11424
|
+
},
|
|
11425
|
+
async query({ pk, gte, lt, order, limit, after }) {
|
|
11426
|
+
let query = items.where("pk", "==", pk);
|
|
11427
|
+
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
11428
|
+
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
11429
|
+
query = query.orderBy("sk", order);
|
|
11430
|
+
if (after !== void 0) query = query.startAfter(after);
|
|
11431
|
+
const size = Math.min(limit, nativePageSize ?? limit);
|
|
11432
|
+
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
11433
|
+
return {
|
|
11434
|
+
items: docs.map((document) => ({
|
|
11435
|
+
sk: document.get("sk"),
|
|
11436
|
+
value: toRow(document.data())
|
|
11437
|
+
})),
|
|
11438
|
+
more: docs.length === size
|
|
11439
|
+
};
|
|
11440
|
+
},
|
|
11441
|
+
async write(ops) {
|
|
11442
|
+
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
11443
|
+
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
11444
|
+
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
11445
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
11446
|
+
if (read.length > 0) {
|
|
11447
|
+
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
11448
|
+
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
11449
|
+
}
|
|
11450
|
+
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
11451
|
+
if (failed !== void 0) {
|
|
11452
|
+
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
11453
|
+
return {
|
|
11454
|
+
ok: false,
|
|
11455
|
+
failedOp: failed
|
|
11456
|
+
};
|
|
11457
|
+
}
|
|
11458
|
+
for (const [position, op] of ops.entries()) {
|
|
11459
|
+
const document = reference(op.key);
|
|
11460
|
+
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
11461
|
+
else if (op.type === "delete") transaction.delete(document);
|
|
11462
|
+
else if (op.type === "add") {
|
|
11463
|
+
if (snapshots.get(position)?.exists === false) {
|
|
11464
|
+
const created = { ...op.init };
|
|
11465
|
+
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
11466
|
+
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
11467
|
+
} else {
|
|
11468
|
+
const [[name, delta], ...more] = Object.entries(op.by);
|
|
11469
|
+
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
11470
|
+
transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
|
|
11471
|
+
}
|
|
11472
|
+
}
|
|
11473
|
+
}
|
|
11474
|
+
return { ok: true };
|
|
11475
|
+
}, { maxAttempts: 5 });
|
|
11476
|
+
try {
|
|
11477
|
+
try {
|
|
11478
|
+
const result = await attempt(false);
|
|
11479
|
+
if (result !== null) return result;
|
|
11480
|
+
} catch (error) {
|
|
11481
|
+
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
11482
|
+
}
|
|
11483
|
+
return await attempt(true);
|
|
11484
|
+
} catch (error) {
|
|
11485
|
+
if (TRANSIENT.has(codeOf(error))) return {
|
|
11486
|
+
ok: false,
|
|
11487
|
+
retry: true
|
|
11488
|
+
};
|
|
11489
|
+
return missingDatabase(error);
|
|
11490
|
+
}
|
|
11491
|
+
},
|
|
11492
|
+
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
11493
|
+
async deleteConsumed(keys) {
|
|
11494
|
+
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
11495
|
+
const batch = firestore.batch();
|
|
11496
|
+
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
11497
|
+
await batch.commit().catch(missingDatabase);
|
|
11498
|
+
}
|
|
11499
|
+
},
|
|
11500
|
+
async dispose() {}
|
|
11501
|
+
};
|
|
11502
|
+
};
|
|
11503
|
+
//#endregion
|
|
11504
|
+
//#region src/firebaseDatabase.ts
|
|
11505
|
+
const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
|
|
11506
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
|
|
11507
|
+
return createKvAdapter({ store: createFirestoreStore({
|
|
11508
|
+
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
11509
|
+
collection
|
|
11510
|
+
}) });
|
|
11511
|
+
};
|
|
11512
|
+
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
11513
|
+
const firebaseDatabase = (config) => createEngineDatabase({
|
|
11514
|
+
name: "firebaseDatabase",
|
|
11515
|
+
adapter: adapterOf(config),
|
|
11516
|
+
aggregateBatching: config.aggregateBatching ?? {}
|
|
11517
|
+
});
|
|
11518
|
+
//#endregion
|
|
11519
|
+
//#region src/firebaseStorage.ts
|
|
11520
|
+
const firebaseStorage = (config) => {
|
|
11521
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
11522
|
+
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
11523
|
+
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
11524
|
+
const parseAndValidate = (storageUri) => {
|
|
11525
|
+
const parsed = parseStorageUri(storageUri, "gs");
|
|
11526
|
+
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
11527
|
+
return parsed;
|
|
11528
|
+
};
|
|
11529
|
+
return createStorageAdapter({
|
|
11530
|
+
name: "firebaseStorage",
|
|
11531
|
+
protocol: "gs",
|
|
11532
|
+
async put({ key, body, contentType }) {
|
|
11533
|
+
const storageKey = getStorageKey(key);
|
|
11534
|
+
const bytes = new Uint8Array(await new Response(body).arrayBuffer());
|
|
11535
|
+
await bucket.file(storageKey).save(bytes, { metadata: {
|
|
11536
|
+
contentType,
|
|
11537
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
11538
|
+
} });
|
|
11539
|
+
return { storageUri: createStorageUri({
|
|
11540
|
+
bucket: config.storageBucket,
|
|
11541
|
+
key: storageKey,
|
|
11542
|
+
protocol: "gs"
|
|
11543
|
+
}) };
|
|
11544
|
+
},
|
|
11545
|
+
async get({ storageUri }) {
|
|
11546
|
+
const { key } = parseAndValidate(storageUri);
|
|
11547
|
+
try {
|
|
11548
|
+
const file = bucket.file(key);
|
|
11549
|
+
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
11550
|
+
const headers = new Headers();
|
|
11551
|
+
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
11552
|
+
headers.set("content-length", String(metadata.size ?? body.byteLength));
|
|
11553
|
+
return { response: new Response(body, { headers }) };
|
|
11554
|
+
} catch (error) {
|
|
11555
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
|
|
11556
|
+
throw error;
|
|
11557
|
+
}
|
|
11558
|
+
},
|
|
11559
|
+
async getDownloadUrl({ storageUri }) {
|
|
11560
|
+
const { key } = parseAndValidate(storageUri);
|
|
11561
|
+
if (config.cdnUrl) {
|
|
11562
|
+
const storageUrl = new URL(storageUri);
|
|
11563
|
+
const downloadUrl = new URL(config.cdnUrl);
|
|
11564
|
+
downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
|
|
11565
|
+
downloadUrl.search = "";
|
|
11566
|
+
downloadUrl.hash = "";
|
|
11567
|
+
return { url: downloadUrl.toString() };
|
|
11568
|
+
}
|
|
11569
|
+
const [url] = await bucket.file(key).getSignedUrl({
|
|
11570
|
+
action: "read",
|
|
11571
|
+
expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
|
|
11572
|
+
});
|
|
11573
|
+
if (!url) throw new Error("Failed to generate Firebase download URL");
|
|
11574
|
+
return { url };
|
|
11575
|
+
},
|
|
11576
|
+
async exists({ storageUri }) {
|
|
11577
|
+
const { key } = parseAndValidate(storageUri);
|
|
11578
|
+
const [exists] = await bucket.file(key).exists();
|
|
11579
|
+
return { exists };
|
|
11580
|
+
},
|
|
11581
|
+
async delete({ storageUri }) {
|
|
11582
|
+
const { key } = parseAndValidate(storageUri);
|
|
11583
|
+
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
11584
|
+
return { deleted: true };
|
|
11585
|
+
}
|
|
11586
|
+
});
|
|
11587
|
+
};
|
|
11656
11588
|
//#endregion
|
|
11657
11589
|
//#region firebase/functions/index.ts
|
|
11658
11590
|
const HOT_UPDATER_BASE_PATH = "/";
|
|
@@ -11662,12 +11594,16 @@ const cdnUrl = process.env.HOT_UPDATER_CDN_URL;
|
|
|
11662
11594
|
if (!storageBucket) throw new Error("Firebase runtime requires storageBucket to read bundle manifests.");
|
|
11663
11595
|
const hotUpdater = createHotUpdater({
|
|
11664
11596
|
database: firebaseDatabase({ ...adminOptions }),
|
|
11665
|
-
plugins
|
|
11666
|
-
|
|
11597
|
+
plugins: [
|
|
11598
|
+
insights(),
|
|
11599
|
+
apiKeys(),
|
|
11600
|
+
remoteConfig()
|
|
11601
|
+
],
|
|
11602
|
+
storage: firebaseStorage({
|
|
11667
11603
|
...adminOptions,
|
|
11668
11604
|
storageBucket,
|
|
11669
11605
|
cdnUrl
|
|
11670
|
-
})
|
|
11606
|
+
})
|
|
11671
11607
|
});
|
|
11672
11608
|
const app = new hono.Hono();
|
|
11673
11609
|
app.get("/ping", (c) => {
|