@hot-updater/firebase 1.0.0-rc.44 → 1.0.0-rc.46
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 +381 -394
- package/dist/index.cjs +0 -13
- package/dist/index.d.cts +2 -1016
- package/dist/index.d.mts +2 -1016
- package/dist/index.mjs +1 -13
- package/dist/init/index.cjs +18 -20
- package/dist/init/index.mjs +19 -21
- package/package.json +7 -7
|
@@ -216,6 +216,13 @@ const isDatabaseJsonValue = (value) => {
|
|
|
216
216
|
const isDatabaseJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && isDatabaseJsonValue(value);
|
|
217
217
|
const isDatabaseMetadataObject = (value) => isDatabaseJsonObject(value) && (!Object.hasOwn(value, "app_version") || typeof value["app_version"] === "string");
|
|
218
218
|
//#endregion
|
|
219
|
+
//#region ../plugin-core/dist/databaseMetadata.mjs
|
|
220
|
+
const bundleMetadataToRow = (metadata) => {
|
|
221
|
+
if (metadata === void 0) return {};
|
|
222
|
+
if (!isDatabaseMetadataObject(metadata)) throw new DatabaseAdapterInputError("invalid-data");
|
|
223
|
+
return metadata;
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
219
226
|
//#region ../../packages/protocol/dist/index.mjs
|
|
220
227
|
const LETTER_DASH_NUMBER$2 = "[a-zA-Z0-9-]";
|
|
221
228
|
const NUMERIC_IDENTIFIER$2 = String.raw`0|[1-9]\d*`;
|
|
@@ -329,7 +336,6 @@ function canonicalizeAppVersion(appVersion) {
|
|
|
329
336
|
const version = coerce$1(appVersion);
|
|
330
337
|
return version ? normalizeFull(version) : null;
|
|
331
338
|
}
|
|
332
|
-
const stripBundleArtifactMetadata = (metadata) => metadata;
|
|
333
339
|
const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
|
|
334
340
|
const getManifestFileHash = (bundle) => bundle.manifestFileHash;
|
|
335
341
|
const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
|
|
@@ -576,14 +582,6 @@ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
|
576
582
|
const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
577
583
|
const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
|
|
578
584
|
//#endregion
|
|
579
|
-
//#region ../plugin-core/dist/databaseMetadata.mjs
|
|
580
|
-
const bundleMetadataToRow = (metadata) => {
|
|
581
|
-
const value = stripBundleArtifactMetadata(metadata);
|
|
582
|
-
if (value === void 0) return {};
|
|
583
|
-
if (!isDatabaseMetadataObject(value)) throw new DatabaseAdapterInputError("invalid-data");
|
|
584
|
-
return value;
|
|
585
|
-
};
|
|
586
|
-
//#endregion
|
|
587
585
|
//#region ../plugin-core/dist/databaseRows.mjs
|
|
588
586
|
const releaseRowToRelease = (row) => ({
|
|
589
587
|
id: row.id,
|
|
@@ -641,7 +639,7 @@ const rowToBundle = (row, patchRows = []) => {
|
|
|
641
639
|
id: row.id,
|
|
642
640
|
platform: row.platform,
|
|
643
641
|
gitCommitHash: row.git_commit_hash,
|
|
644
|
-
metadata:
|
|
642
|
+
metadata: row.metadata,
|
|
645
643
|
manifestStorageUri: row.manifest_storage_uri,
|
|
646
644
|
manifestFileHash: row.manifest_file_hash,
|
|
647
645
|
assetBaseStorageUri: row.asset_base_storage_uri,
|
|
@@ -649,6 +647,35 @@ const rowToBundle = (row, patchRows = []) => {
|
|
|
649
647
|
};
|
|
650
648
|
};
|
|
651
649
|
//#endregion
|
|
650
|
+
//#region ../plugin-core/dist/uuidv7.mjs
|
|
651
|
+
const extractTimestampFromUUIDv7 = (uuid) => {
|
|
652
|
+
if (!isUUIDv7(uuid)) throw new TypeError("Expected a canonical lowercase UUIDv7");
|
|
653
|
+
const timestampHex = uuid.split("-").join("").slice(0, 12);
|
|
654
|
+
return Number.parseInt(timestampHex, 16);
|
|
655
|
+
};
|
|
656
|
+
function createUUIDv7FromTimestampHex(timestampHex) {
|
|
657
|
+
const randomBytes = /* @__PURE__ */ new Uint8Array(10);
|
|
658
|
+
crypto.getRandomValues(randomBytes);
|
|
659
|
+
const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
660
|
+
const randA = randomHex.slice(0, 3);
|
|
661
|
+
const randBHex = randomHex.slice(3, 19);
|
|
662
|
+
const versionAndRandA = `7${randA}`;
|
|
663
|
+
const variantAndFirstRandB = (128 | parseInt(randBHex.slice(0, 2), 16) & 63).toString(16).padStart(2, "0");
|
|
664
|
+
return [
|
|
665
|
+
timestampHex.slice(0, 8),
|
|
666
|
+
timestampHex.slice(8, 12),
|
|
667
|
+
versionAndRandA,
|
|
668
|
+
variantAndFirstRandB + randBHex.slice(2, 4),
|
|
669
|
+
randBHex.slice(4, 16)
|
|
670
|
+
].join("-");
|
|
671
|
+
}
|
|
672
|
+
const createUUIDv7 = () => createUUIDv7FromTimestampHex(Date.now().toString(16).padStart(12, "0"));
|
|
673
|
+
const createUUIDv7After = (floor, nowMs = Date.now()) => {
|
|
674
|
+
const timestamp = Math.max(nowMs, floor === null ? 0 : extractTimestampFromUUIDv7(floor) + 1);
|
|
675
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp >= 2 ** 48) throw new RangeError("UUIDv7 timestamp is out of range");
|
|
676
|
+
return createUUIDv7FromTimestampHex(timestamp.toString(16).padStart(12, "0"));
|
|
677
|
+
};
|
|
678
|
+
//#endregion
|
|
652
679
|
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.mjs
|
|
653
680
|
const LETTER_DASH_NUMBER$1 = "[a-zA-Z0-9-]";
|
|
654
681
|
const NUMERIC_IDENTIFIER$1 = String.raw`0|[1-9]\d*`;
|
|
@@ -995,42 +1022,6 @@ function coerce(value, options = {}) {
|
|
|
995
1022
|
return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
|
|
996
1023
|
}
|
|
997
1024
|
//#endregion
|
|
998
|
-
//#region ../plugin-core/dist/semverSatisfies.mjs
|
|
999
|
-
const semverSatisfies = (targetAppVersion, currentVersion) => {
|
|
1000
|
-
const currentCoerce = coerce(currentVersion);
|
|
1001
|
-
if (!currentCoerce) return false;
|
|
1002
|
-
return satisfies(currentCoerce, targetAppVersion);
|
|
1003
|
-
};
|
|
1004
|
-
//#endregion
|
|
1005
|
-
//#region ../plugin-core/dist/uuidv7.mjs
|
|
1006
|
-
const extractTimestampFromUUIDv7 = (uuid) => {
|
|
1007
|
-
if (!isUUIDv7(uuid)) throw new TypeError("Expected a canonical lowercase UUIDv7");
|
|
1008
|
-
const timestampHex = uuid.split("-").join("").slice(0, 12);
|
|
1009
|
-
return Number.parseInt(timestampHex, 16);
|
|
1010
|
-
};
|
|
1011
|
-
function createUUIDv7FromTimestampHex(timestampHex) {
|
|
1012
|
-
const randomBytes = /* @__PURE__ */ new Uint8Array(10);
|
|
1013
|
-
crypto.getRandomValues(randomBytes);
|
|
1014
|
-
const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1015
|
-
const randA = randomHex.slice(0, 3);
|
|
1016
|
-
const randBHex = randomHex.slice(3, 19);
|
|
1017
|
-
const versionAndRandA = `7${randA}`;
|
|
1018
|
-
const variantAndFirstRandB = (128 | parseInt(randBHex.slice(0, 2), 16) & 63).toString(16).padStart(2, "0");
|
|
1019
|
-
return [
|
|
1020
|
-
timestampHex.slice(0, 8),
|
|
1021
|
-
timestampHex.slice(8, 12),
|
|
1022
|
-
versionAndRandA,
|
|
1023
|
-
variantAndFirstRandB + randBHex.slice(2, 4),
|
|
1024
|
-
randBHex.slice(4, 16)
|
|
1025
|
-
].join("-");
|
|
1026
|
-
}
|
|
1027
|
-
const createUUIDv7 = () => createUUIDv7FromTimestampHex(Date.now().toString(16).padStart(12, "0"));
|
|
1028
|
-
const createUUIDv7After = (floor, nowMs = Date.now()) => {
|
|
1029
|
-
const timestamp = Math.max(nowMs, floor === null ? 0 : extractTimestampFromUUIDv7(floor) + 1);
|
|
1030
|
-
if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp >= 2 ** 48) throw new RangeError("UUIDv7 timestamp is out of range");
|
|
1031
|
-
return createUUIDv7FromTimestampHex(timestamp.toString(16).padStart(12, "0"));
|
|
1032
|
-
};
|
|
1033
|
-
//#endregion
|
|
1034
1025
|
//#region ../plugin-core/dist/releaseCatalogCompiler.mjs
|
|
1035
1026
|
var ReleaseCatalogCompilationError = class extends Error {
|
|
1036
1027
|
code;
|
|
@@ -1426,6 +1417,13 @@ var ReleaseCatalogMutationError = class extends Error {
|
|
|
1426
1417
|
}
|
|
1427
1418
|
};
|
|
1428
1419
|
//#endregion
|
|
1420
|
+
//#region ../plugin-core/dist/semverSatisfies.mjs
|
|
1421
|
+
const semverSatisfies = (targetAppVersion, currentVersion) => {
|
|
1422
|
+
const currentCoerce = coerce(currentVersion);
|
|
1423
|
+
if (!currentCoerce) return false;
|
|
1424
|
+
return satisfies(currentCoerce, targetAppVersion);
|
|
1425
|
+
};
|
|
1426
|
+
//#endregion
|
|
1429
1427
|
//#region ../plugin-core/dist/serverPlugin/definePlugin.mjs
|
|
1430
1428
|
/** Declares a plugin; Hot Updater's own plugins and third-party ones use the same contract. */
|
|
1431
1429
|
const definePlugin = (plugin) => plugin;
|
|
@@ -2373,9 +2371,6 @@ const getEmptyMigrationResult = () => ({
|
|
|
2373
2371
|
execute: async () => {},
|
|
2374
2372
|
getSQL: () => ""
|
|
2375
2373
|
});
|
|
2376
|
-
const assertSupportedMigrationMode = (options) => {
|
|
2377
|
-
if (options.mode === "from-database") throw new Error("Hot Updater migrations support only mode: 'from-schema'.");
|
|
2378
|
-
};
|
|
2379
2374
|
//#endregion
|
|
2380
2375
|
//#region ../plugin-core/dist/engine/db/settingsMigrator.mjs
|
|
2381
2376
|
/** `schema.core`, or a v0 database's `version`. */
|
|
@@ -3459,7 +3454,6 @@ const resolveSchema = (modules) => {
|
|
|
3459
3454
|
const createEngineMigrator = (options) => {
|
|
3460
3455
|
const getVersion = async () => storedSchemaVersion(await options.readSettings());
|
|
3461
3456
|
const makeResult = async (migrate = {}) => {
|
|
3462
|
-
assertSupportedMigrationMode(migrate);
|
|
3463
3457
|
const stored = await options.readSettings();
|
|
3464
3458
|
refusePreEngineDatabase(options.adapterName, stored);
|
|
3465
3459
|
if (Object.entries(options.settings).every(([key, value]) => stored.get(key) === value)) return getEmptyMigrationResult();
|
|
@@ -4566,7 +4560,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
4566
4560
|
};
|
|
4567
4561
|
//#endregion
|
|
4568
4562
|
//#region ../../packages/server/dist/version.mjs
|
|
4569
|
-
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.
|
|
4563
|
+
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.46";
|
|
4570
4564
|
//#endregion
|
|
4571
4565
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
4572
4566
|
/**
|
|
@@ -7112,7 +7106,7 @@ const databaseOf = (value) => {
|
|
|
7112
7106
|
if (isAdapter(value.adapter)) return value;
|
|
7113
7107
|
if ("core" in value && "fetchAdmin" in value) throw new HotUpdaterConfigError("standaloneRepository reaches a server's admin API from the CLI and console. createHotUpdater needs the server's own database, such as kyselyAdapter(...).");
|
|
7114
7108
|
}
|
|
7115
|
-
throw new HotUpdaterConfigError("database must be a Hot Updater
|
|
7109
|
+
throw new HotUpdaterConfigError("database must be a Hot Updater database adapter, such as kyselyAdapter(...) or postgres(...).");
|
|
7116
7110
|
};
|
|
7117
7111
|
/** The configured storage, when it is a storage adapter. */
|
|
7118
7112
|
const storageOf = (value) => {
|
|
@@ -7179,340 +7173,92 @@ function createHotUpdater(options) {
|
|
|
7179
7173
|
};
|
|
7180
7174
|
}
|
|
7181
7175
|
//#endregion
|
|
7182
|
-
//#region
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
const
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
const
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
keyBytes: {
|
|
7200
|
-
pk: 1500,
|
|
7201
|
-
sk: 1500
|
|
7176
|
+
//#region ../api-keys/dist/server/apiKeys.mjs
|
|
7177
|
+
const API_KEY_HEADER_NAME = "x-api-key";
|
|
7178
|
+
const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
7179
|
+
const bytesToBase64Url = (bytes) => {
|
|
7180
|
+
let binary = "";
|
|
7181
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
7182
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
7183
|
+
};
|
|
7184
|
+
const isApiKey = (value) => {
|
|
7185
|
+
if (!BASE64URL_32_PATTERN.test(value)) return false;
|
|
7186
|
+
try {
|
|
7187
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
7188
|
+
const binary = atob(`${base64}=`);
|
|
7189
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
7190
|
+
return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
|
|
7191
|
+
} catch {
|
|
7192
|
+
return false;
|
|
7202
7193
|
}
|
|
7203
7194
|
};
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
const
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
7214
|
-
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
7215
|
-
/**
|
|
7216
|
-
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
7217
|
-
* of a row that expires, which Firestore's TTL policy deletes.
|
|
7218
|
-
*/
|
|
7219
|
-
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
7220
|
-
const toDocument = (key, row, expiresAt) => ({
|
|
7221
|
-
pk: key.pk,
|
|
7222
|
-
sk: key.sk,
|
|
7223
|
-
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
7224
|
-
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
7225
|
-
});
|
|
7226
|
-
const toRow$1 = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
7227
|
-
const holds = (snapshot, condition) => {
|
|
7228
|
-
if (condition === void 0) return true;
|
|
7229
|
-
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
7230
|
-
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
7195
|
+
const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
|
|
7196
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7197
|
+
try {
|
|
7198
|
+
const headers = new Headers();
|
|
7199
|
+
headers.set(value, "");
|
|
7200
|
+
return headers.keys().next().value;
|
|
7201
|
+
} catch {
|
|
7202
|
+
throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7203
|
+
}
|
|
7231
7204
|
};
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
const
|
|
7243
|
-
|
|
7205
|
+
const hashApiKey = async (apiKey) => {
|
|
7206
|
+
if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
|
|
7207
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
|
|
7208
|
+
return bytesToBase64Url(new Uint8Array(digest));
|
|
7209
|
+
};
|
|
7210
|
+
const apiKeyId = () => {
|
|
7211
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
7212
|
+
crypto.getRandomValues(bytes);
|
|
7213
|
+
return `api-${bytesToBase64Url(bytes)}`;
|
|
7214
|
+
};
|
|
7215
|
+
const normalizeApiKeyName = (name) => {
|
|
7216
|
+
const normalized = name.trim();
|
|
7217
|
+
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.");
|
|
7218
|
+
return normalized;
|
|
7219
|
+
};
|
|
7220
|
+
const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
|
|
7221
|
+
const registerApiKey = async (input) => {
|
|
7222
|
+
const hash = await hashApiKey(input.apiKey);
|
|
7223
|
+
const createdAtMs = input.createdAtMs ?? Date.now();
|
|
7224
|
+
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) throw new TypeError("API key creation time must be non-negative.");
|
|
7225
|
+
const record = Object.freeze({
|
|
7226
|
+
created_at_ms: createdAtMs,
|
|
7227
|
+
hash,
|
|
7228
|
+
id: apiKeyId(),
|
|
7229
|
+
name: normalizeApiKeyName(input.name),
|
|
7230
|
+
prefix: input.apiKey.slice(0, 6),
|
|
7231
|
+
revoked_at_ms: null,
|
|
7232
|
+
role: "client"
|
|
7233
|
+
});
|
|
7234
|
+
if (await input.apiKeys.create(record) === "existing") {
|
|
7235
|
+
const existing = await input.apiKeys.findByHash(hash);
|
|
7236
|
+
if (existing === null || existing.revoked_at_ms !== null) throw new Error("The API key has been revoked.");
|
|
7237
|
+
return Object.freeze({
|
|
7238
|
+
apiKey: input.apiKey,
|
|
7239
|
+
record: toApiKeyMetadata(existing)
|
|
7240
|
+
});
|
|
7241
|
+
}
|
|
7242
|
+
return Object.freeze({
|
|
7243
|
+
apiKey: input.apiKey,
|
|
7244
|
+
record: toApiKeyMetadata(record)
|
|
7245
|
+
});
|
|
7246
|
+
};
|
|
7247
|
+
const createApiKey = (input) => {
|
|
7248
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
7249
|
+
crypto.getRandomValues(bytes);
|
|
7250
|
+
return registerApiKey({
|
|
7251
|
+
apiKey: bytesToBase64Url(bytes),
|
|
7252
|
+
apiKeys: input.apiKeys,
|
|
7253
|
+
name: input.name
|
|
7254
|
+
});
|
|
7244
7255
|
};
|
|
7245
7256
|
/**
|
|
7246
|
-
*
|
|
7247
|
-
*
|
|
7248
|
-
*
|
|
7249
|
-
*
|
|
7250
|
-
|
|
7251
|
-
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
7252
|
-
const items = firestore.collection(collection);
|
|
7253
|
-
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
7254
|
-
return {
|
|
7255
|
-
id: "firestore",
|
|
7256
|
-
limits: FIRESTORE_LIMITS,
|
|
7257
|
-
async get(keys) {
|
|
7258
|
-
const found = /* @__PURE__ */ new Map();
|
|
7259
|
-
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
7260
|
-
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
7261
|
-
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
7262
|
-
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow$1(snapshot.data()));
|
|
7263
|
-
}
|
|
7264
|
-
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
7265
|
-
},
|
|
7266
|
-
async query({ pk, gte, lt, order, limit, after }) {
|
|
7267
|
-
let query = items.where("pk", "==", pk);
|
|
7268
|
-
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
7269
|
-
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
7270
|
-
query = query.orderBy("sk", order);
|
|
7271
|
-
if (after !== void 0) query = query.startAfter(after);
|
|
7272
|
-
const size = Math.min(limit, nativePageSize ?? limit);
|
|
7273
|
-
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
7274
|
-
return {
|
|
7275
|
-
items: docs.map((document) => ({
|
|
7276
|
-
sk: document.get("sk"),
|
|
7277
|
-
value: toRow$1(document.data())
|
|
7278
|
-
})),
|
|
7279
|
-
more: docs.length === size
|
|
7280
|
-
};
|
|
7281
|
-
},
|
|
7282
|
-
async write(ops) {
|
|
7283
|
-
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
7284
|
-
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
7285
|
-
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
7286
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
7287
|
-
if (read.length > 0) {
|
|
7288
|
-
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
7289
|
-
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
7290
|
-
}
|
|
7291
|
-
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
7292
|
-
if (failed !== void 0) {
|
|
7293
|
-
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
7294
|
-
return {
|
|
7295
|
-
ok: false,
|
|
7296
|
-
failedOp: failed
|
|
7297
|
-
};
|
|
7298
|
-
}
|
|
7299
|
-
for (const [position, op] of ops.entries()) {
|
|
7300
|
-
const document = reference(op.key);
|
|
7301
|
-
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
7302
|
-
else if (op.type === "delete") transaction.delete(document);
|
|
7303
|
-
else if (op.type === "add") {
|
|
7304
|
-
if (snapshots.get(position)?.exists === false) {
|
|
7305
|
-
const created = { ...op.init };
|
|
7306
|
-
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
7307
|
-
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
7308
|
-
} else {
|
|
7309
|
-
const [[name, delta], ...more] = Object.entries(op.by);
|
|
7310
|
-
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
7311
|
-
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);
|
|
7312
|
-
}
|
|
7313
|
-
}
|
|
7314
|
-
}
|
|
7315
|
-
return { ok: true };
|
|
7316
|
-
}, { maxAttempts: 5 });
|
|
7317
|
-
try {
|
|
7318
|
-
try {
|
|
7319
|
-
const result = await attempt(false);
|
|
7320
|
-
if (result !== null) return result;
|
|
7321
|
-
} catch (error) {
|
|
7322
|
-
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
7323
|
-
}
|
|
7324
|
-
return await attempt(true);
|
|
7325
|
-
} catch (error) {
|
|
7326
|
-
if (TRANSIENT.has(codeOf(error))) return {
|
|
7327
|
-
ok: false,
|
|
7328
|
-
retry: true
|
|
7329
|
-
};
|
|
7330
|
-
return missingDatabase(error);
|
|
7331
|
-
}
|
|
7332
|
-
},
|
|
7333
|
-
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
7334
|
-
async deleteConsumed(keys) {
|
|
7335
|
-
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
7336
|
-
const batch = firestore.batch();
|
|
7337
|
-
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
7338
|
-
await batch.commit().catch(missingDatabase);
|
|
7339
|
-
}
|
|
7340
|
-
},
|
|
7341
|
-
async dispose() {}
|
|
7342
|
-
};
|
|
7343
|
-
};
|
|
7344
|
-
//#endregion
|
|
7345
|
-
//#region src/firebaseDatabase.ts
|
|
7346
|
-
const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
|
|
7347
|
-
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
|
|
7348
|
-
return createKvAdapter({ store: createFirestoreStore({
|
|
7349
|
-
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
7350
|
-
collection
|
|
7351
|
-
}) });
|
|
7352
|
-
};
|
|
7353
|
-
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
7354
|
-
const firebaseDatabase = (config) => createEngineDatabase({
|
|
7355
|
-
name: "firebaseDatabase",
|
|
7356
|
-
adapter: adapterOf(config),
|
|
7357
|
-
aggregateBatching: config.aggregateBatching ?? {}
|
|
7358
|
-
});
|
|
7359
|
-
//#endregion
|
|
7360
|
-
//#region src/firebaseStorage.ts
|
|
7361
|
-
const firebaseStorage = (config) => {
|
|
7362
|
-
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
7363
|
-
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
7364
|
-
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
7365
|
-
const parseAndValidate = (storageUri) => {
|
|
7366
|
-
const parsed = parseStorageUri(storageUri, "gs");
|
|
7367
|
-
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
7368
|
-
return parsed;
|
|
7369
|
-
};
|
|
7370
|
-
return createStorageAdapter({
|
|
7371
|
-
name: "firebaseStorage",
|
|
7372
|
-
protocol: "gs",
|
|
7373
|
-
async put({ key, body, contentType }) {
|
|
7374
|
-
const storageKey = getStorageKey(key);
|
|
7375
|
-
const bytes = new Uint8Array(await new Response(body).arrayBuffer());
|
|
7376
|
-
await bucket.file(storageKey).save(bytes, { metadata: {
|
|
7377
|
-
contentType,
|
|
7378
|
-
cacheControl: "public, max-age=31536000, immutable"
|
|
7379
|
-
} });
|
|
7380
|
-
return { storageUri: createStorageUri({
|
|
7381
|
-
bucket: config.storageBucket,
|
|
7382
|
-
key: storageKey,
|
|
7383
|
-
protocol: "gs"
|
|
7384
|
-
}) };
|
|
7385
|
-
},
|
|
7386
|
-
async get({ storageUri }) {
|
|
7387
|
-
const { key } = parseAndValidate(storageUri);
|
|
7388
|
-
try {
|
|
7389
|
-
const file = bucket.file(key);
|
|
7390
|
-
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
7391
|
-
const headers = new Headers();
|
|
7392
|
-
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
7393
|
-
headers.set("content-length", String(metadata.size ?? body.byteLength));
|
|
7394
|
-
return { response: new Response(body, { headers }) };
|
|
7395
|
-
} catch (error) {
|
|
7396
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
|
|
7397
|
-
throw error;
|
|
7398
|
-
}
|
|
7399
|
-
},
|
|
7400
|
-
async getDownloadUrl({ storageUri }) {
|
|
7401
|
-
const { key } = parseAndValidate(storageUri);
|
|
7402
|
-
if (config.cdnUrl) {
|
|
7403
|
-
const storageUrl = new URL(storageUri);
|
|
7404
|
-
const downloadUrl = new URL(config.cdnUrl);
|
|
7405
|
-
downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
|
|
7406
|
-
downloadUrl.search = "";
|
|
7407
|
-
downloadUrl.hash = "";
|
|
7408
|
-
return { url: downloadUrl.toString() };
|
|
7409
|
-
}
|
|
7410
|
-
const [url] = await bucket.file(key).getSignedUrl({
|
|
7411
|
-
action: "read",
|
|
7412
|
-
expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
|
|
7413
|
-
});
|
|
7414
|
-
if (!url) throw new Error("Failed to generate Firebase download URL");
|
|
7415
|
-
return { url };
|
|
7416
|
-
},
|
|
7417
|
-
async exists({ storageUri }) {
|
|
7418
|
-
const { key } = parseAndValidate(storageUri);
|
|
7419
|
-
const [exists] = await bucket.file(key).exists();
|
|
7420
|
-
return { exists };
|
|
7421
|
-
},
|
|
7422
|
-
async delete({ storageUri }) {
|
|
7423
|
-
const { key } = parseAndValidate(storageUri);
|
|
7424
|
-
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
7425
|
-
return { deleted: true };
|
|
7426
|
-
}
|
|
7427
|
-
});
|
|
7428
|
-
};
|
|
7429
|
-
//#endregion
|
|
7430
|
-
//#region ../api-keys/dist/server/apiKeys.mjs
|
|
7431
|
-
const API_KEY_HEADER_NAME = "x-api-key";
|
|
7432
|
-
const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
7433
|
-
const bytesToBase64Url = (bytes) => {
|
|
7434
|
-
let binary = "";
|
|
7435
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
7436
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
7437
|
-
};
|
|
7438
|
-
const isApiKey = (value) => {
|
|
7439
|
-
if (!BASE64URL_32_PATTERN.test(value)) return false;
|
|
7440
|
-
try {
|
|
7441
|
-
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
7442
|
-
const binary = atob(`${base64}=`);
|
|
7443
|
-
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
7444
|
-
return bytes.byteLength === 32 && bytesToBase64Url(bytes) === value;
|
|
7445
|
-
} catch {
|
|
7446
|
-
return false;
|
|
7447
|
-
}
|
|
7448
|
-
};
|
|
7449
|
-
const normalizeApiKeyHeaderName = (value = API_KEY_HEADER_NAME) => {
|
|
7450
|
-
if (typeof value !== "string" || value.length === 0) throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7451
|
-
try {
|
|
7452
|
-
const headers = new Headers();
|
|
7453
|
-
headers.set(value, "");
|
|
7454
|
-
return headers.keys().next().value;
|
|
7455
|
-
} catch {
|
|
7456
|
-
throw new TypeError("apiKeys({ headerName }) must name a valid header.");
|
|
7457
|
-
}
|
|
7458
|
-
};
|
|
7459
|
-
const hashApiKey = async (apiKey) => {
|
|
7460
|
-
if (!isApiKey(apiKey)) throw new TypeError("API key must be a canonical 32-byte base64url value.");
|
|
7461
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey));
|
|
7462
|
-
return bytesToBase64Url(new Uint8Array(digest));
|
|
7463
|
-
};
|
|
7464
|
-
const apiKeyId = () => {
|
|
7465
|
-
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
7466
|
-
crypto.getRandomValues(bytes);
|
|
7467
|
-
return `api-${bytesToBase64Url(bytes)}`;
|
|
7468
|
-
};
|
|
7469
|
-
const normalizeApiKeyName = (name) => {
|
|
7470
|
-
const normalized = name.trim();
|
|
7471
|
-
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.");
|
|
7472
|
-
return normalized;
|
|
7473
|
-
};
|
|
7474
|
-
const toApiKeyMetadata = ({ hash: _hash, ...metadata }) => Object.freeze(metadata);
|
|
7475
|
-
const registerApiKey = async (input) => {
|
|
7476
|
-
const hash = await hashApiKey(input.apiKey);
|
|
7477
|
-
const createdAtMs = input.createdAtMs ?? Date.now();
|
|
7478
|
-
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) throw new TypeError("API key creation time must be non-negative.");
|
|
7479
|
-
const record = Object.freeze({
|
|
7480
|
-
created_at_ms: createdAtMs,
|
|
7481
|
-
hash,
|
|
7482
|
-
id: apiKeyId(),
|
|
7483
|
-
name: normalizeApiKeyName(input.name),
|
|
7484
|
-
prefix: input.apiKey.slice(0, 6),
|
|
7485
|
-
revoked_at_ms: null,
|
|
7486
|
-
role: "client"
|
|
7487
|
-
});
|
|
7488
|
-
if (await input.apiKeys.create(record) === "existing") {
|
|
7489
|
-
const existing = await input.apiKeys.findByHash(hash);
|
|
7490
|
-
if (existing === null || existing.revoked_at_ms !== null) throw new Error("The API key has been revoked.");
|
|
7491
|
-
return Object.freeze({
|
|
7492
|
-
apiKey: input.apiKey,
|
|
7493
|
-
record: toApiKeyMetadata(existing)
|
|
7494
|
-
});
|
|
7495
|
-
}
|
|
7496
|
-
return Object.freeze({
|
|
7497
|
-
apiKey: input.apiKey,
|
|
7498
|
-
record: toApiKeyMetadata(record)
|
|
7499
|
-
});
|
|
7500
|
-
};
|
|
7501
|
-
const createApiKey = (input) => {
|
|
7502
|
-
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
7503
|
-
crypto.getRandomValues(bytes);
|
|
7504
|
-
return registerApiKey({
|
|
7505
|
-
apiKey: bytesToBase64Url(bytes),
|
|
7506
|
-
apiKeys: input.apiKeys,
|
|
7507
|
-
name: input.name
|
|
7508
|
-
});
|
|
7509
|
-
};
|
|
7510
|
-
/**
|
|
7511
|
-
* Provisions the client API key used by managed init.
|
|
7512
|
-
*
|
|
7513
|
-
* A saved plaintext key is registered idempotently; otherwise a new key is
|
|
7514
|
-
* created. Callers must persist or display the returned plaintext because the
|
|
7515
|
-
* database stores only its hash.
|
|
7257
|
+
* Provisions the client API key used by managed init.
|
|
7258
|
+
*
|
|
7259
|
+
* A saved plaintext key is registered idempotently; otherwise a new key is
|
|
7260
|
+
* created. Callers must persist or display the returned plaintext because the
|
|
7261
|
+
* database stores only its hash.
|
|
7516
7262
|
*/
|
|
7517
7263
|
const provisionApiKey = (input) => {
|
|
7518
7264
|
const existingApiKey = input.existingApiKey?.trim();
|
|
@@ -7576,7 +7322,7 @@ const apiKeysCli = (headerName) => ({ clientCredential: {
|
|
|
7576
7322
|
} });
|
|
7577
7323
|
//#endregion
|
|
7578
7324
|
//#region ../api-keys/dist/server/model.mjs
|
|
7579
|
-
const toRow = (row) => ({
|
|
7325
|
+
const toRow$1 = (row) => ({
|
|
7580
7326
|
id: row.id,
|
|
7581
7327
|
hash: row.hash,
|
|
7582
7328
|
name: row.name,
|
|
@@ -7601,7 +7347,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7601
7347
|
},
|
|
7602
7348
|
async findByHash(hash) {
|
|
7603
7349
|
const row = await db.findOne("api_keys", { hash });
|
|
7604
|
-
return row === null ? null : toRow(row);
|
|
7350
|
+
return row === null ? null : toRow$1(row);
|
|
7605
7351
|
},
|
|
7606
7352
|
/** Every key, newest first, then by id; the whole list is the result. */
|
|
7607
7353
|
async list() {
|
|
@@ -7615,7 +7361,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7615
7361
|
limit: 500,
|
|
7616
7362
|
...cursor === void 0 ? {} : { cursor }
|
|
7617
7363
|
});
|
|
7618
|
-
rows.push(...page.rows.map(toRow));
|
|
7364
|
+
rows.push(...page.rows.map(toRow$1));
|
|
7619
7365
|
cursor = page.next;
|
|
7620
7366
|
} while (cursor !== void 0);
|
|
7621
7367
|
return rows.sort((left, right) => right.created_at_ms - left.created_at_ms || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
@@ -7624,7 +7370,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7624
7370
|
const row = await tx.findOne("api_keys", { id });
|
|
7625
7371
|
if (row === null) return null;
|
|
7626
7372
|
tx.update("api_keys", row, { revoked_at_ms: revokedAtMs });
|
|
7627
|
-
return toRow({
|
|
7373
|
+
return toRow$1({
|
|
7628
7374
|
...row,
|
|
7629
7375
|
revoked_at_ms: revokedAtMs
|
|
7630
7376
|
});
|
|
@@ -11586,16 +11332,253 @@ const remoteConfig = () => {
|
|
|
11586
11332
|
return markOfficial(plugin);
|
|
11587
11333
|
};
|
|
11588
11334
|
//#endregion
|
|
11589
|
-
//#region src/
|
|
11335
|
+
//#region src/firebaseInfrastructureNames.ts
|
|
11590
11336
|
/**
|
|
11591
|
-
* The
|
|
11592
|
-
*
|
|
11337
|
+
* The one collection the storage engine keeps every item in. It namespaces
|
|
11338
|
+
* the data, so item partitions are table names with no prefix.
|
|
11593
11339
|
*/
|
|
11594
|
-
const
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11340
|
+
const FIREBASE_V1_COLLECTION = "hot_updater_v1";
|
|
11341
|
+
//#endregion
|
|
11342
|
+
//#region src/firestoreStore.ts
|
|
11343
|
+
/**
|
|
11344
|
+
* Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
|
|
11345
|
+
* transaction stays a conservative bound. Indexed values over 1,500 bytes are
|
|
11346
|
+
* truncated, so longer keys are refused rather than read inconsistently.
|
|
11347
|
+
*/
|
|
11348
|
+
const FIRESTORE_LIMITS = {
|
|
11349
|
+
items: 500,
|
|
11350
|
+
bytes: 1e7,
|
|
11351
|
+
itemBytes: 1e6,
|
|
11352
|
+
keyBytes: {
|
|
11353
|
+
pk: 1500,
|
|
11354
|
+
sk: 1500
|
|
11355
|
+
}
|
|
11356
|
+
};
|
|
11357
|
+
/** `getAll` reads at most this many documents a call. */
|
|
11358
|
+
const GET_BATCH = 300;
|
|
11359
|
+
/** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
|
|
11360
|
+
const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
|
|
11361
|
+
/**
|
|
11362
|
+
* A document is `{ pk, sk, row }`. Row values that are maps or arrays become
|
|
11363
|
+
* JSON text inside a map, since Firestore has no nested arrays and does not
|
|
11364
|
+
* keep map key order; `row` is exempt from single-field indexing.
|
|
11365
|
+
*/
|
|
11366
|
+
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
11367
|
+
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
11368
|
+
/**
|
|
11369
|
+
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
11370
|
+
* of a row that expires, which Firestore's TTL policy deletes.
|
|
11371
|
+
*/
|
|
11372
|
+
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
11373
|
+
const toDocument = (key, row, expiresAt) => ({
|
|
11374
|
+
pk: key.pk,
|
|
11375
|
+
sk: key.sk,
|
|
11376
|
+
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
11377
|
+
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
11378
|
+
});
|
|
11379
|
+
const toRow = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
11380
|
+
const holds = (snapshot, condition) => {
|
|
11381
|
+
if (condition === void 0) return true;
|
|
11382
|
+
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
11383
|
+
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
11384
|
+
};
|
|
11385
|
+
/** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
|
|
11386
|
+
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
11387
|
+
4,
|
|
11388
|
+
8,
|
|
11389
|
+
10,
|
|
11390
|
+
13,
|
|
11391
|
+
14
|
|
11392
|
+
]);
|
|
11393
|
+
const codeOf = (error) => error.code;
|
|
11394
|
+
/** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
|
|
11395
|
+
const missingDatabase = (error) => {
|
|
11396
|
+
throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
11397
|
+
};
|
|
11398
|
+
/**
|
|
11399
|
+
* The key-value store over one Firestore collection of `{ pk, sk, row }`
|
|
11400
|
+
* documents, queried through the composite indexes on (pk, sk) in both
|
|
11401
|
+
* directions. A write is one transaction that reads only the documents its
|
|
11402
|
+
* ops guard.
|
|
11403
|
+
*/
|
|
11404
|
+
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
11405
|
+
const items = firestore.collection(collection);
|
|
11406
|
+
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
11407
|
+
return {
|
|
11408
|
+
id: "firestore",
|
|
11409
|
+
limits: FIRESTORE_LIMITS,
|
|
11410
|
+
async get(keys) {
|
|
11411
|
+
const found = /* @__PURE__ */ new Map();
|
|
11412
|
+
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
11413
|
+
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
11414
|
+
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
11415
|
+
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow(snapshot.data()));
|
|
11416
|
+
}
|
|
11417
|
+
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
11418
|
+
},
|
|
11419
|
+
async query({ pk, gte, lt, order, limit, after }) {
|
|
11420
|
+
let query = items.where("pk", "==", pk);
|
|
11421
|
+
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
11422
|
+
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
11423
|
+
query = query.orderBy("sk", order);
|
|
11424
|
+
if (after !== void 0) query = query.startAfter(after);
|
|
11425
|
+
const size = Math.min(limit, nativePageSize ?? limit);
|
|
11426
|
+
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
11427
|
+
return {
|
|
11428
|
+
items: docs.map((document) => ({
|
|
11429
|
+
sk: document.get("sk"),
|
|
11430
|
+
value: toRow(document.data())
|
|
11431
|
+
})),
|
|
11432
|
+
more: docs.length === size
|
|
11433
|
+
};
|
|
11434
|
+
},
|
|
11435
|
+
async write(ops) {
|
|
11436
|
+
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
11437
|
+
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
11438
|
+
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
11439
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
11440
|
+
if (read.length > 0) {
|
|
11441
|
+
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
11442
|
+
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
11443
|
+
}
|
|
11444
|
+
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
11445
|
+
if (failed !== void 0) {
|
|
11446
|
+
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
11447
|
+
return {
|
|
11448
|
+
ok: false,
|
|
11449
|
+
failedOp: failed
|
|
11450
|
+
};
|
|
11451
|
+
}
|
|
11452
|
+
for (const [position, op] of ops.entries()) {
|
|
11453
|
+
const document = reference(op.key);
|
|
11454
|
+
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
11455
|
+
else if (op.type === "delete") transaction.delete(document);
|
|
11456
|
+
else if (op.type === "add") {
|
|
11457
|
+
if (snapshots.get(position)?.exists === false) {
|
|
11458
|
+
const created = { ...op.init };
|
|
11459
|
+
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
11460
|
+
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
11461
|
+
} else {
|
|
11462
|
+
const [[name, delta], ...more] = Object.entries(op.by);
|
|
11463
|
+
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
11464
|
+
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);
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11467
|
+
}
|
|
11468
|
+
return { ok: true };
|
|
11469
|
+
}, { maxAttempts: 5 });
|
|
11470
|
+
try {
|
|
11471
|
+
try {
|
|
11472
|
+
const result = await attempt(false);
|
|
11473
|
+
if (result !== null) return result;
|
|
11474
|
+
} catch (error) {
|
|
11475
|
+
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
11476
|
+
}
|
|
11477
|
+
return await attempt(true);
|
|
11478
|
+
} catch (error) {
|
|
11479
|
+
if (TRANSIENT.has(codeOf(error))) return {
|
|
11480
|
+
ok: false,
|
|
11481
|
+
retry: true
|
|
11482
|
+
};
|
|
11483
|
+
return missingDatabase(error);
|
|
11484
|
+
}
|
|
11485
|
+
},
|
|
11486
|
+
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
11487
|
+
async deleteConsumed(keys) {
|
|
11488
|
+
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
11489
|
+
const batch = firestore.batch();
|
|
11490
|
+
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
11491
|
+
await batch.commit().catch(missingDatabase);
|
|
11492
|
+
}
|
|
11493
|
+
},
|
|
11494
|
+
async dispose() {}
|
|
11495
|
+
};
|
|
11496
|
+
};
|
|
11497
|
+
//#endregion
|
|
11498
|
+
//#region src/firebaseDatabase.ts
|
|
11499
|
+
const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
|
|
11500
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
|
|
11501
|
+
return createKvAdapter({ store: createFirestoreStore({
|
|
11502
|
+
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
11503
|
+
collection
|
|
11504
|
+
}) });
|
|
11505
|
+
};
|
|
11506
|
+
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
11507
|
+
const firebaseDatabase = (config) => createEngineDatabase({
|
|
11508
|
+
name: "firebaseDatabase",
|
|
11509
|
+
adapter: adapterOf(config),
|
|
11510
|
+
aggregateBatching: config.aggregateBatching ?? {}
|
|
11511
|
+
});
|
|
11512
|
+
//#endregion
|
|
11513
|
+
//#region src/firebaseStorage.ts
|
|
11514
|
+
const firebaseStorage = (config) => {
|
|
11515
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
11516
|
+
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
11517
|
+
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
11518
|
+
const parseAndValidate = (storageUri) => {
|
|
11519
|
+
const parsed = parseStorageUri(storageUri, "gs");
|
|
11520
|
+
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
11521
|
+
return parsed;
|
|
11522
|
+
};
|
|
11523
|
+
return createStorageAdapter({
|
|
11524
|
+
name: "firebaseStorage",
|
|
11525
|
+
protocol: "gs",
|
|
11526
|
+
async put({ key, body, contentType }) {
|
|
11527
|
+
const storageKey = getStorageKey(key);
|
|
11528
|
+
const bytes = new Uint8Array(await new Response(body).arrayBuffer());
|
|
11529
|
+
await bucket.file(storageKey).save(bytes, { metadata: {
|
|
11530
|
+
contentType,
|
|
11531
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
11532
|
+
} });
|
|
11533
|
+
return { storageUri: createStorageUri({
|
|
11534
|
+
bucket: config.storageBucket,
|
|
11535
|
+
key: storageKey,
|
|
11536
|
+
protocol: "gs"
|
|
11537
|
+
}) };
|
|
11538
|
+
},
|
|
11539
|
+
async get({ storageUri }) {
|
|
11540
|
+
const { key } = parseAndValidate(storageUri);
|
|
11541
|
+
try {
|
|
11542
|
+
const file = bucket.file(key);
|
|
11543
|
+
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
11544
|
+
const headers = new Headers();
|
|
11545
|
+
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
11546
|
+
headers.set("content-length", String(metadata.size ?? body.byteLength));
|
|
11547
|
+
return { response: new Response(body, { headers }) };
|
|
11548
|
+
} catch (error) {
|
|
11549
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
|
|
11550
|
+
throw error;
|
|
11551
|
+
}
|
|
11552
|
+
},
|
|
11553
|
+
async getDownloadUrl({ storageUri }) {
|
|
11554
|
+
const { key } = parseAndValidate(storageUri);
|
|
11555
|
+
if (config.cdnUrl) {
|
|
11556
|
+
const storageUrl = new URL(storageUri);
|
|
11557
|
+
const downloadUrl = new URL(config.cdnUrl);
|
|
11558
|
+
downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
|
|
11559
|
+
downloadUrl.search = "";
|
|
11560
|
+
downloadUrl.hash = "";
|
|
11561
|
+
return { url: downloadUrl.toString() };
|
|
11562
|
+
}
|
|
11563
|
+
const [url] = await bucket.file(key).getSignedUrl({
|
|
11564
|
+
action: "read",
|
|
11565
|
+
expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
|
|
11566
|
+
});
|
|
11567
|
+
if (!url) throw new Error("Failed to generate Firebase download URL");
|
|
11568
|
+
return { url };
|
|
11569
|
+
},
|
|
11570
|
+
async exists({ storageUri }) {
|
|
11571
|
+
const { key } = parseAndValidate(storageUri);
|
|
11572
|
+
const [exists] = await bucket.file(key).exists();
|
|
11573
|
+
return { exists };
|
|
11574
|
+
},
|
|
11575
|
+
async delete({ storageUri }) {
|
|
11576
|
+
const { key } = parseAndValidate(storageUri);
|
|
11577
|
+
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
11578
|
+
return { deleted: true };
|
|
11579
|
+
}
|
|
11580
|
+
});
|
|
11581
|
+
};
|
|
11599
11582
|
//#endregion
|
|
11600
11583
|
//#region firebase/functions/index.ts
|
|
11601
11584
|
const HOT_UPDATER_BASE_PATH = "/";
|
|
@@ -11605,7 +11588,11 @@ const cdnUrl = process.env.HOT_UPDATER_CDN_URL;
|
|
|
11605
11588
|
if (!storageBucket) throw new Error("Firebase runtime requires storageBucket to read bundle manifests.");
|
|
11606
11589
|
const hotUpdater = createHotUpdater({
|
|
11607
11590
|
database: firebaseDatabase({ ...adminOptions }),
|
|
11608
|
-
plugins
|
|
11591
|
+
plugins: [
|
|
11592
|
+
insights(),
|
|
11593
|
+
apiKeys(),
|
|
11594
|
+
remoteConfig()
|
|
11595
|
+
],
|
|
11609
11596
|
storage: firebaseStorage({
|
|
11610
11597
|
...adminOptions,
|
|
11611
11598
|
storageBucket,
|