@hot-updater/firebase 1.2.0-rc.11 → 1.2.0-rc.13
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 +160 -141
- package/dist/index.cjs +45 -41
- package/dist/index.d.cts +2 -3
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +45 -41
- package/package.json +7 -7
|
@@ -2102,7 +2102,10 @@ const CHANNEL_ID = {
|
|
|
2102
2102
|
* every database's migrator creates missing tables and then records the
|
|
2103
2103
|
* version, and none of them changes an existing table, so changing one
|
|
2104
2104
|
* needs a migration step of its own first. The schema fence answers 503 on
|
|
2105
|
-
* a database whose row differs, until it runs.
|
|
2105
|
+
* a database whose row differs, until it runs. Before a version is released,
|
|
2106
|
+
* its tables change in place in its own migration, and a database one of its
|
|
2107
|
+
* prereleases migrated takes the same change by hand: the fence compares this
|
|
2108
|
+
* row, not columns, so nothing reports it.
|
|
2106
2109
|
*/
|
|
2107
2110
|
const HOT_UPDATER_SCHEMA_VERSION = "1.2.0";
|
|
2108
2111
|
const bundles = defineTable({
|
|
@@ -2366,6 +2369,10 @@ const channels = defineTable({
|
|
|
2366
2369
|
* or `versionCode`. `submitted_to`, `submitted_at_ms` and
|
|
2367
2370
|
* `submitted_version` record the store track a build went to, when, and
|
|
2368
2371
|
* the store's label of the build; they stay null for a build no store took.
|
|
2372
|
+
* `store_build` is true on the build a person or a job says the store sells,
|
|
2373
|
+
* and null on every other: deploys patch from it over the scope's newest build.
|
|
2374
|
+
* It is never false, so `byStoreBuild` holds one entry per scope at most, on
|
|
2375
|
+
* every store, and an unmarked row, older than the column or not, holds none.
|
|
2369
2376
|
*/
|
|
2370
2377
|
const nativeBuilds = defineTable({
|
|
2371
2378
|
id: {
|
|
@@ -2442,6 +2449,10 @@ const nativeBuilds = defineTable({
|
|
|
2442
2449
|
type: "string",
|
|
2443
2450
|
maxLength: 255,
|
|
2444
2451
|
required: false
|
|
2452
|
+
},
|
|
2453
|
+
store_build: {
|
|
2454
|
+
type: "boolean",
|
|
2455
|
+
required: false
|
|
2445
2456
|
}
|
|
2446
2457
|
}, {
|
|
2447
2458
|
key: ["id"],
|
|
@@ -2454,6 +2465,11 @@ const nativeBuilds = defineTable({
|
|
|
2454
2465
|
eq: ["scope_key", "build_key"],
|
|
2455
2466
|
sort: ["id"]
|
|
2456
2467
|
},
|
|
2468
|
+
byStoreBuild: {
|
|
2469
|
+
eq: ["scope_key", "store_build"],
|
|
2470
|
+
sort: [],
|
|
2471
|
+
unique: true
|
|
2472
|
+
},
|
|
2457
2473
|
byChannel: {
|
|
2458
2474
|
eq: ["channel_id"],
|
|
2459
2475
|
sort: ["id"],
|
|
@@ -2674,15 +2690,8 @@ const LEASE_TABLE = "aggregate_lease";
|
|
|
2674
2690
|
const LEASE = { id: "compaction" };
|
|
2675
2691
|
/** How long a compaction holds the lease before another process may take it. */
|
|
2676
2692
|
const LEASE_MS = 3e4;
|
|
2677
|
-
/**
|
|
2678
|
-
|
|
2679
|
-
* first; memory mode flushes every 15, since its window is what a crash
|
|
2680
|
-
* loses.
|
|
2681
|
-
*/
|
|
2682
|
-
const WINDOW_MS$1 = {
|
|
2683
|
-
log: 6e4,
|
|
2684
|
-
memory: 15e3
|
|
2685
|
-
};
|
|
2693
|
+
/** A commit compacts once this has passed since the last compaction. */
|
|
2694
|
+
const WINDOW_MS$1 = 6e4;
|
|
2686
2695
|
/** Log rows a compaction reads from each log table for one group. */
|
|
2687
2696
|
const LOG_PAGE = 200;
|
|
2688
2697
|
/**
|
|
@@ -2696,8 +2705,6 @@ const GROUPS = {
|
|
|
2696
2705
|
};
|
|
2697
2706
|
/** Tries per write after a conflict or a transient error. */
|
|
2698
2707
|
const ATTEMPTS = 5;
|
|
2699
|
-
/** Aggregate rows a memory buffer holds before a commit waits for a flush. */
|
|
2700
|
-
const MAX_BUFFERED = 1e4;
|
|
2701
2708
|
const log = defineTable({
|
|
2702
2709
|
id: {
|
|
2703
2710
|
type: "string",
|
|
@@ -2747,7 +2754,7 @@ const valuesOf = ({ deltas, sketches }) => ({
|
|
|
2747
2754
|
/**
|
|
2748
2755
|
* Merges `values` into the change for `key`'s identity as `tx.aggregate`
|
|
2749
2756
|
* does: deltas add up and sketches merge. One constant `shardBy` keys each
|
|
2750
|
-
* identity to one entry;
|
|
2757
|
+
* identity to one entry; `compile` writes it to shard 0.
|
|
2751
2758
|
*/
|
|
2752
2759
|
const merge = (into, model, key, values) => recordAggregate(into, model, Object.fromEntries(shapeOf(model).key.map((field, at) => [field, key[at]])), values, "");
|
|
2753
2760
|
const mergeAll = (into, changes) => {
|
|
@@ -2781,7 +2788,7 @@ const increment = ({ model, deltas }, key) => Object.values(deltas).some((delta)
|
|
|
2781
2788
|
/**
|
|
2782
2789
|
* The guarded write that merges `change` into `current`: insert, patch, or
|
|
2783
2790
|
* delete at zero. Unlike a transaction's, a gauge may go below zero here: a
|
|
2784
|
-
*
|
|
2791
|
+
* group can hold a loss without the gain before it, and reads sum every shard.
|
|
2785
2792
|
*/
|
|
2786
2793
|
const rewrite = (change, key, current) => {
|
|
2787
2794
|
const { model } = change;
|
|
@@ -2819,12 +2826,12 @@ const rewrite = (change, key, current) => {
|
|
|
2819
2826
|
};
|
|
2820
2827
|
};
|
|
2821
2828
|
/**
|
|
2822
|
-
* The write for each change on
|
|
2823
|
-
* the row for counters, and a guarded rewrite
|
|
2824
|
-
* rows are read in one batch per table.
|
|
2829
|
+
* The write for each change on shard 0, since one compaction runs at a time:
|
|
2830
|
+
* a blind increment that creates the row for counters, and a guarded rewrite
|
|
2831
|
+
* for gauges and sketches, whose rows are read in one batch per table.
|
|
2825
2832
|
*/
|
|
2826
|
-
const compile = async (adapter, changes
|
|
2827
|
-
const keys = changes.map(({
|
|
2833
|
+
const compile = async (adapter, changes) => {
|
|
2834
|
+
const keys = changes.map(({ key }) => [...key.slice(0, -1), 0]);
|
|
2828
2835
|
const reads = /* @__PURE__ */ new Map();
|
|
2829
2836
|
changes.forEach((change, at) => {
|
|
2830
2837
|
if (rewrites(change)) reads.set(change.model, [...reads.get(change.model) ?? [], at]);
|
|
@@ -2878,14 +2885,13 @@ const encode$1 = async (changes) => {
|
|
|
2878
2885
|
return zipped !== void 0 && zipped.length < json.length ? `z${zipped}` : json;
|
|
2879
2886
|
};
|
|
2880
2887
|
/**
|
|
2881
|
-
* Batches changes to aggregates declared `batched`. A transaction
|
|
2882
|
-
* them
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
2885
|
-
*
|
|
2886
|
-
* applies exactly once. One lease row keeps compactions one at a time.
|
|
2888
|
+
* Batches changes to aggregates declared `batched`. A transaction writes
|
|
2889
|
+
* them as one log row in its own atomic write, in place of their rows; a
|
|
2890
|
+
* compaction later merges log rows into the aggregates and deletes them,
|
|
2891
|
+
* atomically again, so each applies exactly once. One lease row keeps
|
|
2892
|
+
* compactions one at a time.
|
|
2887
2893
|
*/
|
|
2888
|
-
const createAggregateBatches = ({ engine, adapter, schema, batching: {
|
|
2894
|
+
const createAggregateBatches = ({ engine, adapter, schema, batching: { windowMs = WINDOW_MS$1 }, now }) => {
|
|
2889
2895
|
if (!schema.models.has(LEASE_TABLE)) throw new DatabaseTransactionError("An engine that batches aggregates needs aggregateBatchingModule in its schema.");
|
|
2890
2896
|
const holder = crypto.randomUUID().replaceAll("-", "");
|
|
2891
2897
|
const table = (name) => schema.models.get(name).table;
|
|
@@ -2913,68 +2919,6 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
|
|
|
2913
2919
|
}
|
|
2914
2920
|
return changes;
|
|
2915
2921
|
};
|
|
2916
|
-
/**
|
|
2917
|
-
* Writes changes in atomic chunks within `fits`, and returns the ones it
|
|
2918
|
-
* could not apply. A write whose outcome is unknown is dropped, never
|
|
2919
|
-
* resent, so no change applies twice.
|
|
2920
|
-
*/
|
|
2921
|
-
const apply = async (changes, shard) => {
|
|
2922
|
-
let rest = changes;
|
|
2923
|
-
let ops = [];
|
|
2924
|
-
let read = true;
|
|
2925
|
-
for (let failures = 0; rest.length > 0 && failures < ATTEMPTS;) try {
|
|
2926
|
-
if (read) ops = await compile(adapter, rest, shard);
|
|
2927
|
-
let size = rest.length;
|
|
2928
|
-
const chunk = () => ops.slice(0, size).filter(isOp);
|
|
2929
|
-
while (size > 1 && !adapter.fits(chunk())) size = Math.ceil(size / 2);
|
|
2930
|
-
let result = { ok: true };
|
|
2931
|
-
if (chunk().length > 0) result = await adapter.write(chunk()).catch((error) => {
|
|
2932
|
-
warn("flush")(error);
|
|
2933
|
-
return { ok: true };
|
|
2934
|
-
});
|
|
2935
|
-
read = !result.ok && !("retry" in result);
|
|
2936
|
-
if (result.ok) {
|
|
2937
|
-
rest = rest.slice(size);
|
|
2938
|
-
ops = ops.slice(size);
|
|
2939
|
-
failures = 0;
|
|
2940
|
-
} else {
|
|
2941
|
-
failures += 1;
|
|
2942
|
-
if (!read) await sleep(failures);
|
|
2943
|
-
}
|
|
2944
|
-
} catch (error) {
|
|
2945
|
-
warn("flush")(error);
|
|
2946
|
-
return rest;
|
|
2947
|
-
}
|
|
2948
|
-
return rest;
|
|
2949
|
-
};
|
|
2950
|
-
let buffer = /* @__PURE__ */ new Map();
|
|
2951
|
-
let timer;
|
|
2952
|
-
let flushing;
|
|
2953
|
-
const shards = /* @__PURE__ */ new Map();
|
|
2954
|
-
/** Each process writes its own shard, so flushes of two processes rarely meet. */
|
|
2955
|
-
const processShard = (model) => {
|
|
2956
|
-
if (!shards.has(model)) shards.set(model, Math.floor(Math.random() * shapeOf(model).shards));
|
|
2957
|
-
return shards.get(model);
|
|
2958
|
-
};
|
|
2959
|
-
const schedule = () => {
|
|
2960
|
-
if (timer !== void 0 || buffer.size === 0) return;
|
|
2961
|
-
timer = setTimeout(() => {
|
|
2962
|
-
timer = void 0;
|
|
2963
|
-
flushBuffer();
|
|
2964
|
-
}, windowMs);
|
|
2965
|
-
timer.unref?.();
|
|
2966
|
-
};
|
|
2967
|
-
/** Never rejects: what it cannot apply goes back into the buffer. */
|
|
2968
|
-
const flushBuffer = () => flushing ??= (async () => {
|
|
2969
|
-
clearTimeout(timer);
|
|
2970
|
-
timer = void 0;
|
|
2971
|
-
const pending = [...buffer.values()];
|
|
2972
|
-
buffer = /* @__PURE__ */ new Map();
|
|
2973
|
-
mergeAll(buffer, await apply(pending, processShard));
|
|
2974
|
-
})().catch(warn("flush")).finally(() => {
|
|
2975
|
-
flushing = void 0;
|
|
2976
|
-
schedule();
|
|
2977
|
-
});
|
|
2978
2922
|
let nextDue = 0;
|
|
2979
2923
|
let compacting;
|
|
2980
2924
|
/** Log rows this process read, by `[log table's index, id]`, for the deletes after a group. */
|
|
@@ -3109,7 +3053,7 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
|
|
|
3109
3053
|
more
|
|
3110
3054
|
};
|
|
3111
3055
|
};
|
|
3112
|
-
const rowsFor = async (changes) => (await compile(adapter, changes
|
|
3056
|
+
const rowsFor = async (changes) => (await compile(adapter, changes)).filter(isOp);
|
|
3113
3057
|
/**
|
|
3114
3058
|
* Applies the oldest pending log rows whose merged changes fit in one
|
|
3115
3059
|
* atomic write with the lease, which lists them as applied; the rows are
|
|
@@ -3203,20 +3147,6 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
|
|
|
3203
3147
|
}))).then((pages) => pages.some((page) => page.rows.length > 0)).finally(() => {
|
|
3204
3148
|
checking = void 0;
|
|
3205
3149
|
});
|
|
3206
|
-
/** Takes a committed transaction's changes; never rejects, since the transaction already committed. */
|
|
3207
|
-
const committed = async (changes) => {
|
|
3208
|
-
if (mode === "log") {
|
|
3209
|
-
if (now() >= nextDue) await compact(false, GROUPS.commit);
|
|
3210
|
-
return;
|
|
3211
|
-
}
|
|
3212
|
-
mergeAll(buffer, changes.values());
|
|
3213
|
-
schedule();
|
|
3214
|
-
if (buffer.size >= MAX_BUFFERED) await flushBuffer();
|
|
3215
|
-
};
|
|
3216
|
-
const flush = async () => {
|
|
3217
|
-
await flushBuffer();
|
|
3218
|
-
if (mode === "log" && await hasPending()) await compact(true, GROUPS.flush);
|
|
3219
|
-
};
|
|
3220
3150
|
return {
|
|
3221
3151
|
async transaction(fn) {
|
|
3222
3152
|
let changes = /* @__PURE__ */ new Map();
|
|
@@ -3242,7 +3172,7 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
|
|
|
3242
3172
|
aggregate
|
|
3243
3173
|
});
|
|
3244
3174
|
if (failure) throw failure.error;
|
|
3245
|
-
if (
|
|
3175
|
+
if (attempt.size > 0) {
|
|
3246
3176
|
const id = `${now().toString(36).padStart(9, "0")}${holder.slice(0, 8)}${crypto.randomUUID().slice(0, 8)}`;
|
|
3247
3177
|
const shard = Math.floor(Math.random() * LOG_TABLES.length);
|
|
3248
3178
|
const merged = mergeAll(/* @__PURE__ */ new Map(), attempt.values());
|
|
@@ -3256,24 +3186,18 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
|
|
|
3256
3186
|
open = false;
|
|
3257
3187
|
}
|
|
3258
3188
|
});
|
|
3259
|
-
if (changes.size > 0) await
|
|
3189
|
+
if (changes.size > 0 && now() >= nextDue) await compact(false, GROUPS.commit);
|
|
3260
3190
|
return result;
|
|
3261
3191
|
},
|
|
3262
3192
|
async beforeRead(name) {
|
|
3263
3193
|
if (!isBatched(schema.models.get(name))) return;
|
|
3264
|
-
if (
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
return false;
|
|
3269
|
-
})) await compact(true, GROUPS.read);
|
|
3270
|
-
}
|
|
3194
|
+
if (await hasPending().catch((error) => {
|
|
3195
|
+
warn("read")(error);
|
|
3196
|
+
return false;
|
|
3197
|
+
})) await compact(true, GROUPS.read);
|
|
3271
3198
|
},
|
|
3272
|
-
flush
|
|
3273
|
-
|
|
3274
|
-
clearTimeout(timer);
|
|
3275
|
-
timer = void 0;
|
|
3276
|
-
await flush();
|
|
3199
|
+
async flush() {
|
|
3200
|
+
if (await hasPending()) await compact(true, GROUPS.flush);
|
|
3277
3201
|
}
|
|
3278
3202
|
};
|
|
3279
3203
|
};
|
|
@@ -4302,13 +4226,10 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
|
|
|
4302
4226
|
};
|
|
4303
4227
|
return {
|
|
4304
4228
|
...engine,
|
|
4305
|
-
/** Applies batched aggregate changes still
|
|
4229
|
+
/** Applies the batched aggregate changes still in the log. */
|
|
4306
4230
|
flush: async () => {
|
|
4307
4231
|
await batches?.flush();
|
|
4308
4232
|
},
|
|
4309
|
-
dispose: async () => {
|
|
4310
|
-
await batches?.dispose();
|
|
4311
|
-
},
|
|
4312
4233
|
database(module) {
|
|
4313
4234
|
const name = (model) => module.namespace ? `${module.namespace}_${model}` : model;
|
|
4314
4235
|
return {
|
|
@@ -4389,8 +4310,7 @@ const createEngine = (database, { plugins = [], now = Date.now } = {}) => {
|
|
|
4389
4310
|
if (module === void 0) throw new HotUpdaterConfigError(`Plugin "${plugin.id}" is not one of this engine's plugins.`);
|
|
4390
4311
|
return engine.database(module);
|
|
4391
4312
|
},
|
|
4392
|
-
flush: () => engine.flush()
|
|
4393
|
-
dispose: () => engine.dispose()
|
|
4313
|
+
flush: () => engine.flush()
|
|
4394
4314
|
};
|
|
4395
4315
|
};
|
|
4396
4316
|
//#endregion
|
|
@@ -4683,7 +4603,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
4683
4603
|
};
|
|
4684
4604
|
//#endregion
|
|
4685
4605
|
//#region ../../packages/server/dist/version.mjs
|
|
4686
|
-
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.
|
|
4606
|
+
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.13";
|
|
4687
4607
|
//#endregion
|
|
4688
4608
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
4689
4609
|
/**
|
|
@@ -5888,8 +5808,22 @@ const pick = (row) => ({
|
|
|
5888
5808
|
build_number: row.build_number ?? null,
|
|
5889
5809
|
submitted_to: row.submitted_to ?? null,
|
|
5890
5810
|
submitted_at_ms: row.submitted_at_ms ?? null,
|
|
5891
|
-
submitted_version: row.submitted_version ?? null
|
|
5811
|
+
submitted_version: row.submitted_version ?? null,
|
|
5812
|
+
store_build: row.store_build === true ? true : null
|
|
5892
5813
|
});
|
|
5814
|
+
/**
|
|
5815
|
+
* The channels only sandbox builds register on, which no store sells: the
|
|
5816
|
+
* registry's convention, as `@hot-updater/protocol/native-artifact` checks it
|
|
5817
|
+
* for a build's artifact.
|
|
5818
|
+
*/
|
|
5819
|
+
const SANDBOX_CHANNEL_PREFIX = "sandbox-";
|
|
5820
|
+
/**
|
|
5821
|
+
* How many older builds of a scope one read takes, looking for one older than
|
|
5822
|
+
* a bundle when a build made after the bundle is the newest.
|
|
5823
|
+
*/
|
|
5824
|
+
const OLDER_BUILDS_PAGE = 20;
|
|
5825
|
+
/** How many times a mark runs again after losing its scope's mark to another. */
|
|
5826
|
+
const MARK_ATTEMPTS = 8;
|
|
5893
5827
|
/** The store track each platform's builds are submitted to. */
|
|
5894
5828
|
const SUBMISSION_DESTINATIONS = {
|
|
5895
5829
|
android: "play-internal",
|
|
@@ -5966,7 +5900,8 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
|
|
|
5966
5900
|
build_number: registration.buildNumber ?? null,
|
|
5967
5901
|
submitted_to: null,
|
|
5968
5902
|
submitted_at_ms: null,
|
|
5969
|
-
submitted_version: null
|
|
5903
|
+
submitted_version: null,
|
|
5904
|
+
store_build: null
|
|
5970
5905
|
};
|
|
5971
5906
|
for (let attempt = 1;; attempt += 1) {
|
|
5972
5907
|
await insertChannel(db, {
|
|
@@ -6004,21 +5939,45 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
|
|
|
6004
5939
|
const scopeKey = nativeBuildScopeKey(scope);
|
|
6005
5940
|
const buildKey = options?.buildKey;
|
|
6006
5941
|
if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
where: { scope_key: scopeKey },
|
|
6010
|
-
order: "desc",
|
|
6011
|
-
limit: 1
|
|
6012
|
-
}) : await db.findMany("native_builds", {
|
|
6013
|
-
index: "byBuildKey",
|
|
6014
|
-
where: {
|
|
5942
|
+
if (options?.storeBuild) {
|
|
5943
|
+
const marked = await db.findOne("native_builds", {
|
|
6015
5944
|
scope_key: scopeKey,
|
|
6016
|
-
|
|
6017
|
-
}
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
5945
|
+
store_build: true
|
|
5946
|
+
});
|
|
5947
|
+
return marked === null ? null : pick(marked);
|
|
5948
|
+
}
|
|
5949
|
+
const read = (limit, before) => {
|
|
5950
|
+
const page = {
|
|
5951
|
+
order: "desc",
|
|
5952
|
+
limit,
|
|
5953
|
+
...before === void 0 ? {} : { range: { lt: before } }
|
|
5954
|
+
};
|
|
5955
|
+
return buildKey === void 0 ? db.findMany("native_builds", {
|
|
5956
|
+
index: "byScope",
|
|
5957
|
+
where: { scope_key: scopeKey },
|
|
5958
|
+
...page
|
|
5959
|
+
}) : db.findMany("native_builds", {
|
|
5960
|
+
index: "byBuildKey",
|
|
5961
|
+
where: {
|
|
5962
|
+
scope_key: scopeKey,
|
|
5963
|
+
build_key: buildKey
|
|
5964
|
+
},
|
|
5965
|
+
...page
|
|
5966
|
+
});
|
|
5967
|
+
};
|
|
5968
|
+
const { rows } = await read(1);
|
|
5969
|
+
const newest = rows[0];
|
|
5970
|
+
if (newest === void 0) return null;
|
|
5971
|
+
const bundleId = options?.builtInOlderThan;
|
|
5972
|
+
if (bundleId === void 0 || newest.min_bundle_id < bundleId) return pick(newest);
|
|
5973
|
+
for (let before = newest.id;;) {
|
|
5974
|
+
const { rows: older } = await read(OLDER_BUILDS_PAGE, before);
|
|
5975
|
+
const usable = older.find(({ min_bundle_id }) => min_bundle_id < bundleId);
|
|
5976
|
+
if (usable !== void 0) return pick(usable);
|
|
5977
|
+
const last = older.at(-1);
|
|
5978
|
+
if (older.length < OLDER_BUILDS_PAGE || last === void 0) return null;
|
|
5979
|
+
before = last.id;
|
|
5980
|
+
}
|
|
6022
5981
|
},
|
|
6023
5982
|
async recordNativeBuildSubmission(id, { destination, version }) {
|
|
6024
5983
|
if (version.trim().length === 0 || version.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
|
|
@@ -6039,6 +5998,49 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
|
|
|
6039
5998
|
};
|
|
6040
5999
|
});
|
|
6041
6000
|
},
|
|
6001
|
+
async markNativeBuild(id, { storeBuild }) {
|
|
6002
|
+
for (let attempt = 1;; attempt += 1) try {
|
|
6003
|
+
return await db.transaction(async (tx) => {
|
|
6004
|
+
const row = await tx.findOne("native_builds", { id });
|
|
6005
|
+
if (row === null) return null;
|
|
6006
|
+
const build = pick(row);
|
|
6007
|
+
if (!storeBuild) {
|
|
6008
|
+
if (build.store_build !== null) tx.update("native_builds", row, { store_build: null });
|
|
6009
|
+
return {
|
|
6010
|
+
nativeBuild: {
|
|
6011
|
+
...build,
|
|
6012
|
+
store_build: null
|
|
6013
|
+
},
|
|
6014
|
+
previous: null
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
6017
|
+
if (decodeChannelKey(build.channel_id.slice(8)).startsWith(SANDBOX_CHANNEL_PREFIX)) throw new NativeBuildInputError("A build on a sandbox channel can't be the store build.");
|
|
6018
|
+
if (build.store_build !== null) return {
|
|
6019
|
+
nativeBuild: build,
|
|
6020
|
+
previous: null
|
|
6021
|
+
};
|
|
6022
|
+
const holder = await tx.findOne("native_builds", {
|
|
6023
|
+
scope_key: build.scope_key,
|
|
6024
|
+
store_build: true
|
|
6025
|
+
});
|
|
6026
|
+
if (holder !== null) tx.update("native_builds", holder, { store_build: null });
|
|
6027
|
+
tx.update("native_builds", row, { store_build: true });
|
|
6028
|
+
return {
|
|
6029
|
+
nativeBuild: {
|
|
6030
|
+
...build,
|
|
6031
|
+
store_build: true
|
|
6032
|
+
},
|
|
6033
|
+
previous: holder === null ? null : {
|
|
6034
|
+
...pick(holder),
|
|
6035
|
+
store_build: null
|
|
6036
|
+
}
|
|
6037
|
+
};
|
|
6038
|
+
});
|
|
6039
|
+
} catch (error) {
|
|
6040
|
+
if (attempt < MARK_ATTEMPTS && error instanceof DatabaseConstraintError && error.reason === "unique") continue;
|
|
6041
|
+
throw error;
|
|
6042
|
+
}
|
|
6043
|
+
},
|
|
6042
6044
|
async deleteNativeBuild(id) {
|
|
6043
6045
|
return db.transaction(async (tx) => {
|
|
6044
6046
|
const row = await tx.findOne("native_builds", { id });
|
|
@@ -7121,7 +7123,13 @@ const createAdminRouteHandlers = () => ({
|
|
|
7121
7123
|
const url = new URL(request.url);
|
|
7122
7124
|
const scope = nativeBuildScopeOf(url);
|
|
7123
7125
|
const buildKey = url.searchParams.get("buildKey");
|
|
7124
|
-
|
|
7126
|
+
const builtInOlderThan = url.searchParams.get("builtInOlderThan");
|
|
7127
|
+
const options = {
|
|
7128
|
+
...buildKey === null ? {} : { buildKey },
|
|
7129
|
+
...url.searchParams.get("storeBuild") === "true" ? { storeBuild: true } : {},
|
|
7130
|
+
...builtInOlderThan === null ? {} : { builtInOlderThan }
|
|
7131
|
+
};
|
|
7132
|
+
return json$2({ data: await refusingInvalid(() => Object.keys(options).length === 0 ? core.findNativeBuild(scope) : core.findNativeBuild(scope, options)) });
|
|
7125
7133
|
}),
|
|
7126
7134
|
getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
|
|
7127
7135
|
listNativeBuilds: onCore(async (core, request) => {
|
|
@@ -7142,6 +7150,12 @@ const createAdminRouteHandlers = () => ({
|
|
|
7142
7150
|
};
|
|
7143
7151
|
return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
|
|
7144
7152
|
}),
|
|
7153
|
+
markNativeBuild: onCore(async (core, request, params) => {
|
|
7154
|
+
const input = await body(request);
|
|
7155
|
+
if (!isRecord$5(input) || typeof input.storeBuild !== "boolean") throw new HandlerBadRequestError("A native build's store build mark needs storeBuild, true or false.");
|
|
7156
|
+
const mark = { storeBuild: input.storeBuild };
|
|
7157
|
+
return json$2({ data: await refusingInvalid(() => core.markNativeBuild(requireRouteParam(params, "id"), mark)) });
|
|
7158
|
+
}),
|
|
7145
7159
|
deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
|
|
7146
7160
|
deleteBundles: onCore(async (core, request) => {
|
|
7147
7161
|
const { ids } = await body(request);
|
|
@@ -7287,6 +7301,11 @@ const ADMIN_ROUTES = [
|
|
|
7287
7301
|
path: "/native-builds/:id/submission",
|
|
7288
7302
|
handler: "recordNativeBuildSubmission"
|
|
7289
7303
|
},
|
|
7304
|
+
{
|
|
7305
|
+
method: "POST",
|
|
7306
|
+
path: "/native-builds/:id/store-build",
|
|
7307
|
+
handler: "markNativeBuild"
|
|
7308
|
+
},
|
|
7290
7309
|
{
|
|
7291
7310
|
method: "DELETE",
|
|
7292
7311
|
path: "/native-builds/:id",
|
package/dist/index.cjs
CHANGED
|
@@ -392,51 +392,62 @@ const crc32c = (bytes) => {
|
|
|
392
392
|
/** The version IDs the secret's annotation lists. */
|
|
393
393
|
const trackedVersions = (secret) => (secret.annotations?.[VERSIONS_ANNOTATION] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => VERSION_ID_PATTERN.test(entry));
|
|
394
394
|
const versionOf = (name) => typeof name === "string" ? name.slice(name.lastIndexOf("/") + 1) : "";
|
|
395
|
-
/** The widest line a message has: the CLI prints no wider. */
|
|
396
|
-
const LINE_WIDTH = 76;
|
|
397
|
-
/** `text`, such as Google's own message, in lines that fit, broken at spaces. */
|
|
398
|
-
const wrap = (text) => {
|
|
399
|
-
const lines = [];
|
|
400
|
-
let line = "";
|
|
401
|
-
for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
|
|
402
|
-
lines.push(line);
|
|
403
|
-
line = word;
|
|
404
|
-
} else line = line ? `${line} ${word}` : word;
|
|
405
|
-
if (line) lines.push(line);
|
|
406
|
-
return lines;
|
|
407
|
-
};
|
|
408
395
|
/** An error of one line each, for a terminal. */
|
|
409
396
|
const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
|
|
410
|
-
|
|
397
|
+
/**
|
|
398
|
+
* The one step that can fix the credentials in use, by the order they're
|
|
399
|
+
* tried: signing in with gcloud changes nothing while a variable comes first.
|
|
400
|
+
*/
|
|
401
|
+
const signInHint = (credentials) => {
|
|
402
|
+
if (credentials) return [`Check the credentials passed to ${ADAPTER_NAME}().`];
|
|
403
|
+
const variable = ACCESS_TOKEN_ENV_NAMES.find((name) => process.env[name]?.trim()) ?? (process.env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim() ? "GOOGLE_APPLICATION_CREDENTIALS" : void 0);
|
|
404
|
+
if (variable) return [`Fix or unset ${variable}.`];
|
|
405
|
+
return process.env["CI"] ? ["Use google-github-actions/auth."] : ["Sign in:", "gcloud auth application-default login"];
|
|
406
|
+
};
|
|
407
|
+
const hint = (response, permission, signIn) => {
|
|
411
408
|
const { status, error } = response;
|
|
412
409
|
if (error.details?.some((detail) => detail.reason === "SERVICE_DISABLED")) return ["Enable the Secret Manager API in the project:", "gcloud services enable secretmanager.googleapis.com --project <project>"];
|
|
413
|
-
if (status === 401) return [
|
|
414
|
-
|
|
415
|
-
"Sign in again with gcloud auth application-default login.",
|
|
416
|
-
"In CI, check the job's Workload Identity Federation."
|
|
417
|
-
];
|
|
418
|
-
if (status === 403) return wrap(`The Google identity needs ${permission}.`);
|
|
410
|
+
if (status === 401) return ["Google rejected the access token.", ...signIn];
|
|
411
|
+
if (status === 403) return [`The identity needs ${permission}.`];
|
|
419
412
|
if (status === 429) return ["Secret Manager's quota is exhausted: try again later."];
|
|
420
413
|
return [];
|
|
421
414
|
};
|
|
422
415
|
/**
|
|
423
|
-
* An error for a failed request on `resource`, a line each: the HTTP
|
|
424
|
-
*
|
|
425
|
-
* but never a value,
|
|
416
|
+
* An error for a failed request on `resource`, a line each: the HTTP status,
|
|
417
|
+
* the resource, `notes`, and what to fix, or Google's own status and message,
|
|
418
|
+
* which name resources but never a value, when nothing here says what to fix.
|
|
426
419
|
*/
|
|
427
|
-
const
|
|
420
|
+
const requestErrorOf = (signIn, action, resource, response, permission, notes = []) => {
|
|
428
421
|
const { status, message } = response.error;
|
|
429
|
-
const
|
|
422
|
+
const fix = hint(response, permission, signIn);
|
|
423
|
+
if (response.status === 401 && notes.length === 0) return failure(fix);
|
|
430
424
|
return failure([
|
|
431
425
|
`${ADAPTER_NAME} couldn't ${action} (${response.status}).`,
|
|
432
426
|
resourceLine(resource),
|
|
433
|
-
...wrap(detail),
|
|
434
427
|
...notes,
|
|
435
|
-
...
|
|
428
|
+
...fix.length > 0 ? fix : [[status, message?.slice(0, 300)].filter(Boolean).join(": ")]
|
|
436
429
|
]);
|
|
437
430
|
};
|
|
438
431
|
/** The line that names a secret: its resource name is a line's worth. */
|
|
439
432
|
const resourceLine = (secret) => `Secret: ${secret}`;
|
|
433
|
+
let metadataWarningFiltered = false;
|
|
434
|
+
/**
|
|
435
|
+
* Drops the one warning `gcp-metadata` emits when its probe for a Google
|
|
436
|
+
* runtime fails oddly: credentials are about to be reported as missing, and
|
|
437
|
+
* the warning has nothing to act on. It stays filtered for the process, as
|
|
438
|
+
* the library caches the probe; every other warning passes through.
|
|
439
|
+
* `METADATA_SERVER_DETECTION=none` would also silence it, but skips the probe,
|
|
440
|
+
* and with it the credentials of a Google runtime.
|
|
441
|
+
*/
|
|
442
|
+
const filterMetadataWarning = () => {
|
|
443
|
+
if (metadataWarningFiltered) return;
|
|
444
|
+
metadataWarningFiltered = true;
|
|
445
|
+
const emitWarning = process.emitWarning;
|
|
446
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
447
|
+
if (rest[0] === "MetadataLookupWarning") return;
|
|
448
|
+
Reflect.apply(emitWarning, process, [warning, ...rest]);
|
|
449
|
+
});
|
|
450
|
+
};
|
|
440
451
|
/** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
|
|
441
452
|
const mask = async (value) => {
|
|
442
453
|
const { registerSecretValue } = await import("@hot-updater/cli-tools");
|
|
@@ -472,17 +483,11 @@ const createAccessToken = (credentials, projectId) => {
|
|
|
472
483
|
throw error;
|
|
473
484
|
});
|
|
474
485
|
const client = await auth;
|
|
486
|
+
filterMetadataWarning();
|
|
475
487
|
try {
|
|
476
488
|
return await client.getAccessToken() ?? void 0;
|
|
477
489
|
} catch (error) {
|
|
478
|
-
throw failure([
|
|
479
|
-
`${ADAPTER_NAME} found no Google credentials.`,
|
|
480
|
-
...wrap(error instanceof Error ? error.message.split("\n")[0] : String(error)),
|
|
481
|
-
"On your machine, run gcloud auth application-default login.",
|
|
482
|
-
"In CI, use google-github-actions/auth.",
|
|
483
|
-
"Or set GOOGLE_APPLICATION_CREDENTIALS to a service account key.",
|
|
484
|
-
`Or set ${ACCESS_TOKEN_ENV_NAMES.join(" or ")} to a token.`
|
|
485
|
-
], error);
|
|
490
|
+
throw failure([`${ADAPTER_NAME} found no Google credentials.`, ...signInHint(void 0)], error);
|
|
486
491
|
}
|
|
487
492
|
};
|
|
488
493
|
return async () => {
|
|
@@ -504,6 +509,7 @@ const createAccessToken = (credentials, projectId) => {
|
|
|
504
509
|
*/
|
|
505
510
|
const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
506
511
|
const accessToken = createAccessToken(credentials, projectId);
|
|
512
|
+
const requestError = (action, resource, response, permission, notes) => requestErrorOf(signInHint(credentials), action, resource, response, permission, notes);
|
|
507
513
|
const projectPath = () => {
|
|
508
514
|
if (typeof projectId !== "string" || !PROJECT_PATTERN.test(projectId)) throw failure([`${ADAPTER_NAME} can't use the projectId ${JSON.stringify(projectId)}.`, "Set the Google Cloud project's ID or number."]);
|
|
509
515
|
if (!PREFIX_PATTERN.test(prefix)) throw failure([`${ADAPTER_NAME} can't use the prefix ${JSON.stringify(prefix)}.`, "It takes letters, digits, \"-\", and \"_\"."]);
|
|
@@ -617,10 +623,8 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
|
617
623
|
if (missing.length > 0) throw failure([
|
|
618
624
|
`${ADAPTER_NAME} can't store a value.`,
|
|
619
625
|
resourceLine(secret),
|
|
620
|
-
|
|
621
|
-
"
|
|
622
|
-
"Grant roles/secretmanager.admin, or a custom role with:",
|
|
623
|
-
...wrap("secretmanager.secrets.get, secretmanager.secrets.update, secretmanager.versions.add, secretmanager.versions.destroy, secretmanager.versions.access")
|
|
626
|
+
`The identity lacks ${missing.join(", ")} on it.`,
|
|
627
|
+
"Grant roles/secretmanager.admin."
|
|
624
628
|
]);
|
|
625
629
|
};
|
|
626
630
|
/**
|
|
@@ -713,7 +717,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
|
713
717
|
throw failure([
|
|
714
718
|
`${ADAPTER_NAME} couldn't ${notListed}.`,
|
|
715
719
|
resourceLine(secret),
|
|
716
|
-
|
|
720
|
+
error instanceof Error ? error.message : String(error),
|
|
717
721
|
notDestroyed
|
|
718
722
|
], error);
|
|
719
723
|
}
|
|
@@ -727,7 +731,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
|
727
731
|
},
|
|
728
732
|
describe: (name) => {
|
|
729
733
|
const id = `${prefix}${name}`;
|
|
730
|
-
return `
|
|
734
|
+
return `the secret ${JSON.stringify(id)} in Google Secret Manager (https://console.cloud.google.com/security/secret-manager/secret/${encodeURIComponent(id)}/versions?project=${encodeURIComponent(projectId)})`;
|
|
731
735
|
}
|
|
732
736
|
};
|
|
733
737
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -11,9 +11,8 @@ type FirebaseDatabaseConfig = AppOptions & {
|
|
|
11
11
|
readonly collection?: string;
|
|
12
12
|
/**
|
|
13
13
|
* Firestore bills each document write, so Insights' aggregates are
|
|
14
|
-
* batched: through log rows a compaction merges (the default)
|
|
15
|
-
*
|
|
16
|
-
* each event.
|
|
14
|
+
* batched: through log rows a compaction merges (the default). `false` commits
|
|
15
|
+
* them with each event.
|
|
17
16
|
*/
|
|
18
17
|
readonly aggregateBatching?: EngineDatabase["aggregateBatching"] | false;
|
|
19
18
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -11,9 +11,8 @@ type FirebaseDatabaseConfig = AppOptions & {
|
|
|
11
11
|
readonly collection?: string;
|
|
12
12
|
/**
|
|
13
13
|
* Firestore bills each document write, so Insights' aggregates are
|
|
14
|
-
* batched: through log rows a compaction merges (the default)
|
|
15
|
-
*
|
|
16
|
-
* each event.
|
|
14
|
+
* batched: through log rows a compaction merges (the default). `false` commits
|
|
15
|
+
* them with each event.
|
|
17
16
|
*/
|
|
18
17
|
readonly aggregateBatching?: EngineDatabase["aggregateBatching"] | false;
|
|
19
18
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -391,51 +391,62 @@ const crc32c = (bytes) => {
|
|
|
391
391
|
/** The version IDs the secret's annotation lists. */
|
|
392
392
|
const trackedVersions = (secret) => (secret.annotations?.[VERSIONS_ANNOTATION] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => VERSION_ID_PATTERN.test(entry));
|
|
393
393
|
const versionOf = (name) => typeof name === "string" ? name.slice(name.lastIndexOf("/") + 1) : "";
|
|
394
|
-
/** The widest line a message has: the CLI prints no wider. */
|
|
395
|
-
const LINE_WIDTH = 76;
|
|
396
|
-
/** `text`, such as Google's own message, in lines that fit, broken at spaces. */
|
|
397
|
-
const wrap = (text) => {
|
|
398
|
-
const lines = [];
|
|
399
|
-
let line = "";
|
|
400
|
-
for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
|
|
401
|
-
lines.push(line);
|
|
402
|
-
line = word;
|
|
403
|
-
} else line = line ? `${line} ${word}` : word;
|
|
404
|
-
if (line) lines.push(line);
|
|
405
|
-
return lines;
|
|
406
|
-
};
|
|
407
394
|
/** An error of one line each, for a terminal. */
|
|
408
395
|
const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
|
|
409
|
-
|
|
396
|
+
/**
|
|
397
|
+
* The one step that can fix the credentials in use, by the order they're
|
|
398
|
+
* tried: signing in with gcloud changes nothing while a variable comes first.
|
|
399
|
+
*/
|
|
400
|
+
const signInHint = (credentials) => {
|
|
401
|
+
if (credentials) return [`Check the credentials passed to ${ADAPTER_NAME}().`];
|
|
402
|
+
const variable = ACCESS_TOKEN_ENV_NAMES.find((name) => process.env[name]?.trim()) ?? (process.env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim() ? "GOOGLE_APPLICATION_CREDENTIALS" : void 0);
|
|
403
|
+
if (variable) return [`Fix or unset ${variable}.`];
|
|
404
|
+
return process.env["CI"] ? ["Use google-github-actions/auth."] : ["Sign in:", "gcloud auth application-default login"];
|
|
405
|
+
};
|
|
406
|
+
const hint = (response, permission, signIn) => {
|
|
410
407
|
const { status, error } = response;
|
|
411
408
|
if (error.details?.some((detail) => detail.reason === "SERVICE_DISABLED")) return ["Enable the Secret Manager API in the project:", "gcloud services enable secretmanager.googleapis.com --project <project>"];
|
|
412
|
-
if (status === 401) return [
|
|
413
|
-
|
|
414
|
-
"Sign in again with gcloud auth application-default login.",
|
|
415
|
-
"In CI, check the job's Workload Identity Federation."
|
|
416
|
-
];
|
|
417
|
-
if (status === 403) return wrap(`The Google identity needs ${permission}.`);
|
|
409
|
+
if (status === 401) return ["Google rejected the access token.", ...signIn];
|
|
410
|
+
if (status === 403) return [`The identity needs ${permission}.`];
|
|
418
411
|
if (status === 429) return ["Secret Manager's quota is exhausted: try again later."];
|
|
419
412
|
return [];
|
|
420
413
|
};
|
|
421
414
|
/**
|
|
422
|
-
* An error for a failed request on `resource`, a line each: the HTTP
|
|
423
|
-
*
|
|
424
|
-
* but never a value,
|
|
415
|
+
* An error for a failed request on `resource`, a line each: the HTTP status,
|
|
416
|
+
* the resource, `notes`, and what to fix, or Google's own status and message,
|
|
417
|
+
* which name resources but never a value, when nothing here says what to fix.
|
|
425
418
|
*/
|
|
426
|
-
const
|
|
419
|
+
const requestErrorOf = (signIn, action, resource, response, permission, notes = []) => {
|
|
427
420
|
const { status, message } = response.error;
|
|
428
|
-
const
|
|
421
|
+
const fix = hint(response, permission, signIn);
|
|
422
|
+
if (response.status === 401 && notes.length === 0) return failure(fix);
|
|
429
423
|
return failure([
|
|
430
424
|
`${ADAPTER_NAME} couldn't ${action} (${response.status}).`,
|
|
431
425
|
resourceLine(resource),
|
|
432
|
-
...wrap(detail),
|
|
433
426
|
...notes,
|
|
434
|
-
...
|
|
427
|
+
...fix.length > 0 ? fix : [[status, message?.slice(0, 300)].filter(Boolean).join(": ")]
|
|
435
428
|
]);
|
|
436
429
|
};
|
|
437
430
|
/** The line that names a secret: its resource name is a line's worth. */
|
|
438
431
|
const resourceLine = (secret) => `Secret: ${secret}`;
|
|
432
|
+
let metadataWarningFiltered = false;
|
|
433
|
+
/**
|
|
434
|
+
* Drops the one warning `gcp-metadata` emits when its probe for a Google
|
|
435
|
+
* runtime fails oddly: credentials are about to be reported as missing, and
|
|
436
|
+
* the warning has nothing to act on. It stays filtered for the process, as
|
|
437
|
+
* the library caches the probe; every other warning passes through.
|
|
438
|
+
* `METADATA_SERVER_DETECTION=none` would also silence it, but skips the probe,
|
|
439
|
+
* and with it the credentials of a Google runtime.
|
|
440
|
+
*/
|
|
441
|
+
const filterMetadataWarning = () => {
|
|
442
|
+
if (metadataWarningFiltered) return;
|
|
443
|
+
metadataWarningFiltered = true;
|
|
444
|
+
const emitWarning = process.emitWarning;
|
|
445
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
446
|
+
if (rest[0] === "MetadataLookupWarning") return;
|
|
447
|
+
Reflect.apply(emitWarning, process, [warning, ...rest]);
|
|
448
|
+
});
|
|
449
|
+
};
|
|
439
450
|
/** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
|
|
440
451
|
const mask = async (value) => {
|
|
441
452
|
const { registerSecretValue } = await import("@hot-updater/cli-tools");
|
|
@@ -471,17 +482,11 @@ const createAccessToken = (credentials, projectId) => {
|
|
|
471
482
|
throw error;
|
|
472
483
|
});
|
|
473
484
|
const client = await auth;
|
|
485
|
+
filterMetadataWarning();
|
|
474
486
|
try {
|
|
475
487
|
return await client.getAccessToken() ?? void 0;
|
|
476
488
|
} catch (error) {
|
|
477
|
-
throw failure([
|
|
478
|
-
`${ADAPTER_NAME} found no Google credentials.`,
|
|
479
|
-
...wrap(error instanceof Error ? error.message.split("\n")[0] : String(error)),
|
|
480
|
-
"On your machine, run gcloud auth application-default login.",
|
|
481
|
-
"In CI, use google-github-actions/auth.",
|
|
482
|
-
"Or set GOOGLE_APPLICATION_CREDENTIALS to a service account key.",
|
|
483
|
-
`Or set ${ACCESS_TOKEN_ENV_NAMES.join(" or ")} to a token.`
|
|
484
|
-
], error);
|
|
489
|
+
throw failure([`${ADAPTER_NAME} found no Google credentials.`, ...signInHint(void 0)], error);
|
|
485
490
|
}
|
|
486
491
|
};
|
|
487
492
|
return async () => {
|
|
@@ -503,6 +508,7 @@ const createAccessToken = (credentials, projectId) => {
|
|
|
503
508
|
*/
|
|
504
509
|
const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
505
510
|
const accessToken = createAccessToken(credentials, projectId);
|
|
511
|
+
const requestError = (action, resource, response, permission, notes) => requestErrorOf(signInHint(credentials), action, resource, response, permission, notes);
|
|
506
512
|
const projectPath = () => {
|
|
507
513
|
if (typeof projectId !== "string" || !PROJECT_PATTERN.test(projectId)) throw failure([`${ADAPTER_NAME} can't use the projectId ${JSON.stringify(projectId)}.`, "Set the Google Cloud project's ID or number."]);
|
|
508
514
|
if (!PREFIX_PATTERN.test(prefix)) throw failure([`${ADAPTER_NAME} can't use the prefix ${JSON.stringify(prefix)}.`, "It takes letters, digits, \"-\", and \"_\"."]);
|
|
@@ -616,10 +622,8 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
|
616
622
|
if (missing.length > 0) throw failure([
|
|
617
623
|
`${ADAPTER_NAME} can't store a value.`,
|
|
618
624
|
resourceLine(secret),
|
|
619
|
-
|
|
620
|
-
"
|
|
621
|
-
"Grant roles/secretmanager.admin, or a custom role with:",
|
|
622
|
-
...wrap("secretmanager.secrets.get, secretmanager.secrets.update, secretmanager.versions.add, secretmanager.versions.destroy, secretmanager.versions.access")
|
|
625
|
+
`The identity lacks ${missing.join(", ")} on it.`,
|
|
626
|
+
"Grant roles/secretmanager.admin."
|
|
623
627
|
]);
|
|
624
628
|
};
|
|
625
629
|
/**
|
|
@@ -712,7 +716,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
|
712
716
|
throw failure([
|
|
713
717
|
`${ADAPTER_NAME} couldn't ${notListed}.`,
|
|
714
718
|
resourceLine(secret),
|
|
715
|
-
|
|
719
|
+
error instanceof Error ? error.message : String(error),
|
|
716
720
|
notDestroyed
|
|
717
721
|
], error);
|
|
718
722
|
}
|
|
@@ -726,7 +730,7 @@ const secretManager = ({ projectId, prefix = "hot-updater-", credentials }) => {
|
|
|
726
730
|
},
|
|
727
731
|
describe: (name) => {
|
|
728
732
|
const id = `${prefix}${name}`;
|
|
729
|
-
return `
|
|
733
|
+
return `the secret ${JSON.stringify(id)} in Google Secret Manager (https://console.cloud.google.com/security/secret-manager/secret/${encodeURIComponent(id)}/versions?project=${encodeURIComponent(projectId)})`;
|
|
730
734
|
}
|
|
731
735
|
};
|
|
732
736
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/firebase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.0-rc.
|
|
4
|
+
"version": "1.2.0-rc.13",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"types": "dist/index.d.cts",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"hono": "4.12.34",
|
|
40
40
|
"google-auth-library": "10.7.0",
|
|
41
|
-
"@hot-updater/cli-tools": "1.2.0-rc.
|
|
42
|
-
"@hot-updater/
|
|
43
|
-
"@hot-updater/
|
|
44
|
-
"@hot-updater/
|
|
41
|
+
"@hot-updater/cli-tools": "1.2.0-rc.13",
|
|
42
|
+
"@hot-updater/plugin-core": "1.2.0-rc.13",
|
|
43
|
+
"@hot-updater/server": "1.2.0-rc.13",
|
|
44
|
+
"@hot-updater/protocol": "1.2.0-rc.13"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"firebase-tools": "15.29.0",
|
|
57
57
|
"fkill": "^9.0.0",
|
|
58
58
|
"mime": "^4.0.4",
|
|
59
|
-
"@hot-updater/mock": "1.2.0-rc.
|
|
60
|
-
"@hot-updater/test-utils": "1.2.0-rc.
|
|
59
|
+
"@hot-updater/mock": "1.2.0-rc.13",
|
|
60
|
+
"@hot-updater/test-utils": "1.2.0-rc.13"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"firebase-admin": ">=11.10.0",
|