@wopr-network/platform-core 1.31.1 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/crypto/btc/address-gen.d.ts +15 -4
- package/dist/billing/crypto/btc/address-gen.js +31 -8
- package/dist/billing/crypto/btc/index.d.ts +2 -1
- package/dist/billing/crypto/btc/index.js +1 -1
- package/dist/fleet/updater.d.ts +1 -1
- package/dist/fleet/updater.js +2 -2
- package/dist/fleet/updater.test.js +2 -2
- package/dist/node-agent/types.d.ts +2 -2
- package/dist/trpc/admin-fleet-update-router.d.ts +41 -0
- package/dist/trpc/admin-fleet-update-router.js +51 -0
- package/dist/trpc/index.d.ts +1 -0
- package/dist/trpc/index.js +1 -0
- package/package.json +1 -1
- package/src/billing/crypto/btc/address-gen.ts +42 -10
- package/src/billing/crypto/btc/index.ts +2 -1
- package/src/fleet/updater.test.ts +2 -2
- package/src/fleet/updater.ts +2 -2
- package/src/trpc/admin-fleet-update-router.ts +62 -0
- package/src/trpc/index.ts +1 -0
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
/** Supported UTXO chains for bech32 address derivation. */
|
|
2
|
+
export type UtxoChain = "bitcoin" | "litecoin";
|
|
3
|
+
/** Supported network types. */
|
|
4
|
+
export type UtxoNetwork = "mainnet" | "testnet" | "regtest";
|
|
1
5
|
/**
|
|
2
|
-
* Derive a native segwit (bech32
|
|
6
|
+
* Derive a native segwit (bech32) deposit address from an xpub at a given index.
|
|
7
|
+
* Works for BTC (bc1q...) and LTC (ltc1q...) — same HASH160 + bech32 encoding.
|
|
3
8
|
* Path: xpub / 0 / index (external chain).
|
|
4
9
|
* No private keys involved.
|
|
5
10
|
*/
|
|
6
|
-
export declare function
|
|
7
|
-
/** Derive the
|
|
8
|
-
export declare function
|
|
11
|
+
export declare function deriveAddress(xpub: string, index: number, network?: UtxoNetwork, chain?: UtxoChain): string;
|
|
12
|
+
/** Derive the treasury address (internal chain, index 0). */
|
|
13
|
+
export declare function deriveTreasury(xpub: string, network?: UtxoNetwork, chain?: UtxoChain): string;
|
|
14
|
+
/** @deprecated Use `deriveAddress` instead. */
|
|
15
|
+
export declare const deriveBtcAddress: typeof deriveAddress;
|
|
16
|
+
/** @deprecated Use `deriveTreasury` instead. */
|
|
17
|
+
export declare const deriveBtcTreasury: typeof deriveTreasury;
|
|
18
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
19
|
+
export declare function isValidXpub(key: string): boolean;
|
|
@@ -2,33 +2,56 @@ import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
3
|
import { bech32 } from "@scure/base";
|
|
4
4
|
import { HDKey } from "@scure/bip32";
|
|
5
|
+
/** Bech32 HRP (human-readable part) by chain and network. */
|
|
6
|
+
const BECH32_PREFIX = {
|
|
7
|
+
bitcoin: { mainnet: "bc", testnet: "tb", regtest: "bcrt" },
|
|
8
|
+
litecoin: { mainnet: "ltc", testnet: "tltc", regtest: "rltc" },
|
|
9
|
+
};
|
|
10
|
+
function getBech32Prefix(chain, network) {
|
|
11
|
+
return BECH32_PREFIX[chain][network];
|
|
12
|
+
}
|
|
5
13
|
/**
|
|
6
|
-
* Derive a native segwit (bech32
|
|
14
|
+
* Derive a native segwit (bech32) deposit address from an xpub at a given index.
|
|
15
|
+
* Works for BTC (bc1q...) and LTC (ltc1q...) — same HASH160 + bech32 encoding.
|
|
7
16
|
* Path: xpub / 0 / index (external chain).
|
|
8
17
|
* No private keys involved.
|
|
9
18
|
*/
|
|
10
|
-
export function
|
|
19
|
+
export function deriveAddress(xpub, index, network = "mainnet", chain = "bitcoin") {
|
|
11
20
|
if (!Number.isInteger(index) || index < 0)
|
|
12
21
|
throw new Error(`Invalid derivation index: ${index}`);
|
|
13
22
|
const master = HDKey.fromExtendedKey(xpub);
|
|
14
23
|
const child = master.deriveChild(0).deriveChild(index);
|
|
15
24
|
if (!child.publicKey)
|
|
16
25
|
throw new Error("Failed to derive public key");
|
|
17
|
-
// HASH160 = RIPEMD160(SHA256(compressedPubKey))
|
|
18
26
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
19
|
-
|
|
20
|
-
const prefix = network === "mainnet" ? "bc" : "tb";
|
|
27
|
+
const prefix = getBech32Prefix(chain, network);
|
|
21
28
|
const words = bech32.toWords(hash160);
|
|
22
29
|
return bech32.encode(prefix, [0, ...words]);
|
|
23
30
|
}
|
|
24
|
-
/** Derive the
|
|
25
|
-
export function
|
|
31
|
+
/** Derive the treasury address (internal chain, index 0). */
|
|
32
|
+
export function deriveTreasury(xpub, network = "mainnet", chain = "bitcoin") {
|
|
26
33
|
const master = HDKey.fromExtendedKey(xpub);
|
|
27
34
|
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
28
35
|
if (!child.publicKey)
|
|
29
36
|
throw new Error("Failed to derive public key");
|
|
30
37
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
31
|
-
const prefix = network
|
|
38
|
+
const prefix = getBech32Prefix(chain, network);
|
|
32
39
|
const words = bech32.toWords(hash160);
|
|
33
40
|
return bech32.encode(prefix, [0, ...words]);
|
|
34
41
|
}
|
|
42
|
+
/** @deprecated Use `deriveAddress` instead. */
|
|
43
|
+
export const deriveBtcAddress = deriveAddress;
|
|
44
|
+
/** @deprecated Use `deriveTreasury` instead. */
|
|
45
|
+
export const deriveBtcTreasury = deriveTreasury;
|
|
46
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
47
|
+
export function isValidXpub(key) {
|
|
48
|
+
if (!key.startsWith("xpub"))
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
HDKey.fromExtendedKey(key);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { UtxoChain, UtxoNetwork } from "./address-gen.js";
|
|
2
|
+
export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
|
|
2
3
|
export type { BtcCheckoutDeps, BtcCheckoutResult } from "./checkout.js";
|
|
3
4
|
export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
|
|
4
5
|
export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { deriveBtcAddress, deriveBtcTreasury } from "./address-gen.js";
|
|
1
|
+
export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
|
|
2
2
|
export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
|
|
3
3
|
export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
|
|
4
4
|
export { settleBtcPayment } from "./settler.js";
|
package/dist/fleet/updater.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class ContainerUpdater {
|
|
|
26
26
|
constructor(docker: Docker, store: IProfileStore, fleet: FleetManager, _poller: ImagePoller);
|
|
27
27
|
/**
|
|
28
28
|
* Update a bot's container to the latest image available for its release channel.
|
|
29
|
-
* Rolls back if the new container fails health checks within
|
|
29
|
+
* Rolls back if the new container fails health checks within 120s.
|
|
30
30
|
*/
|
|
31
31
|
updateBot(botId: string): Promise<UpdateResult>;
|
|
32
32
|
private doUpdateBot;
|
package/dist/fleet/updater.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from "../config/logger.js";
|
|
2
2
|
import { getContainerDigest } from "./image-poller.js";
|
|
3
3
|
/** How long to wait for a container to become healthy after update (ms) */
|
|
4
|
-
const HEALTH_CHECK_TIMEOUT_MS =
|
|
4
|
+
const HEALTH_CHECK_TIMEOUT_MS = 120_000;
|
|
5
5
|
/** How often to check container health during update verification (ms) */
|
|
6
6
|
const HEALTH_CHECK_POLL_MS = 5_000;
|
|
7
7
|
/**
|
|
@@ -22,7 +22,7 @@ export class ContainerUpdater {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Update a bot's container to the latest image available for its release channel.
|
|
25
|
-
* Rolls back if the new container fails health checks within
|
|
25
|
+
* Rolls back if the new container fails health checks within 120s.
|
|
26
26
|
*/
|
|
27
27
|
async updateBot(botId) {
|
|
28
28
|
if (this.updating.has(botId)) {
|
|
@@ -246,8 +246,8 @@ describe("ContainerUpdater", () => {
|
|
|
246
246
|
docker.listContainers.mockResolvedValue([{ Id: "container-123" }]);
|
|
247
247
|
docker.getContainer.mockReturnValue(startingContainer);
|
|
248
248
|
const promise = updater.updateBot("bot-1");
|
|
249
|
-
// Advance past the
|
|
250
|
-
for (let i = 0; i <
|
|
249
|
+
// Advance past the 120s timeout (120_000ms) in increments of poll interval (5_000ms)
|
|
250
|
+
for (let i = 0; i < 25; i++) {
|
|
251
251
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
252
252
|
}
|
|
253
253
|
const result = await promise;
|
|
@@ -44,14 +44,14 @@ export type CommandType = (typeof ALLOWED_COMMANDS)[number];
|
|
|
44
44
|
export declare const commandSchema: z.ZodObject<{
|
|
45
45
|
id: z.ZodString;
|
|
46
46
|
type: z.ZodEnum<{
|
|
47
|
+
"bot.logs": "bot.logs";
|
|
47
48
|
"bot.start": "bot.start";
|
|
48
49
|
"bot.stop": "bot.stop";
|
|
49
50
|
"bot.restart": "bot.restart";
|
|
51
|
+
"bot.remove": "bot.remove";
|
|
50
52
|
"bot.update": "bot.update";
|
|
51
53
|
"bot.export": "bot.export";
|
|
52
54
|
"bot.import": "bot.import";
|
|
53
|
-
"bot.remove": "bot.remove";
|
|
54
|
-
"bot.logs": "bot.logs";
|
|
55
55
|
"bot.inspect": "bot.inspect";
|
|
56
56
|
"backup.upload": "backup.upload";
|
|
57
57
|
"backup.download": "backup.download";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RolloutOrchestrator } from "../fleet/rollout-orchestrator.js";
|
|
2
|
+
import type { ITenantUpdateConfigRepository } from "../fleet/tenant-update-config-repository.js";
|
|
3
|
+
export declare function createAdminFleetUpdateRouter(getOrchestrator: () => RolloutOrchestrator, getConfigRepo: () => ITenantUpdateConfigRepository): import("@trpc/server").TRPCBuiltRouter<{
|
|
4
|
+
ctx: import("./init.js").TRPCContext;
|
|
5
|
+
meta: object;
|
|
6
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
7
|
+
transformer: false;
|
|
8
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
9
|
+
/** Get current rollout status */
|
|
10
|
+
rolloutStatus: import("@trpc/server").TRPCQueryProcedure<{
|
|
11
|
+
input: void;
|
|
12
|
+
output: {
|
|
13
|
+
isRolling: boolean;
|
|
14
|
+
};
|
|
15
|
+
meta: object;
|
|
16
|
+
}>;
|
|
17
|
+
/** Force trigger a rollout for all auto-update tenants */
|
|
18
|
+
forceRollout: import("@trpc/server").TRPCMutationProcedure<{
|
|
19
|
+
input: void;
|
|
20
|
+
output: {
|
|
21
|
+
triggered: boolean;
|
|
22
|
+
};
|
|
23
|
+
meta: object;
|
|
24
|
+
}>;
|
|
25
|
+
/** List all tenant update configs */
|
|
26
|
+
listTenantConfigs: import("@trpc/server").TRPCQueryProcedure<{
|
|
27
|
+
input: void;
|
|
28
|
+
output: import("../fleet/tenant-update-config-repository.js").TenantUpdateConfig[];
|
|
29
|
+
meta: object;
|
|
30
|
+
}>;
|
|
31
|
+
/** Override a specific tenant's update config */
|
|
32
|
+
setTenantConfig: import("@trpc/server").TRPCMutationProcedure<{
|
|
33
|
+
input: {
|
|
34
|
+
tenantId: string;
|
|
35
|
+
mode: "manual" | "auto";
|
|
36
|
+
preferredHourUtc?: number | undefined;
|
|
37
|
+
};
|
|
38
|
+
output: void;
|
|
39
|
+
meta: object;
|
|
40
|
+
}>;
|
|
41
|
+
}>>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../config/logger.js";
|
|
3
|
+
import { adminProcedure, router } from "./init.js";
|
|
4
|
+
export function createAdminFleetUpdateRouter(getOrchestrator, getConfigRepo) {
|
|
5
|
+
return router({
|
|
6
|
+
/** Get current rollout status */
|
|
7
|
+
rolloutStatus: adminProcedure.query(() => {
|
|
8
|
+
const orchestrator = getOrchestrator();
|
|
9
|
+
return {
|
|
10
|
+
isRolling: orchestrator.isRolling,
|
|
11
|
+
};
|
|
12
|
+
}),
|
|
13
|
+
/** Force trigger a rollout for all auto-update tenants */
|
|
14
|
+
forceRollout: adminProcedure.mutation(async () => {
|
|
15
|
+
const orchestrator = getOrchestrator();
|
|
16
|
+
logger.info("Admin: fleet.forceRollout");
|
|
17
|
+
// Fire and forget — don't block the admin request
|
|
18
|
+
orchestrator.rollout().catch((err) => {
|
|
19
|
+
logger.error("Force rollout failed", {
|
|
20
|
+
error: err.message,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return { triggered: true };
|
|
24
|
+
}),
|
|
25
|
+
/** List all tenant update configs */
|
|
26
|
+
listTenantConfigs: adminProcedure.query(async () => {
|
|
27
|
+
const repo = getConfigRepo();
|
|
28
|
+
return repo.listAutoEnabled();
|
|
29
|
+
}),
|
|
30
|
+
/** Override a specific tenant's update config */
|
|
31
|
+
setTenantConfig: adminProcedure
|
|
32
|
+
.input(z.object({
|
|
33
|
+
tenantId: z.string().min(1),
|
|
34
|
+
mode: z.enum(["auto", "manual"]),
|
|
35
|
+
preferredHourUtc: z.number().int().min(0).max(23).optional(),
|
|
36
|
+
}))
|
|
37
|
+
.mutation(async ({ input }) => {
|
|
38
|
+
const repo = getConfigRepo();
|
|
39
|
+
const existing = await repo.get(input.tenantId);
|
|
40
|
+
await repo.upsert(input.tenantId, {
|
|
41
|
+
mode: input.mode,
|
|
42
|
+
preferredHourUtc: input.preferredHourUtc ?? existing?.preferredHourUtc ?? 3,
|
|
43
|
+
});
|
|
44
|
+
logger.info("Admin: fleet.setTenantConfig", {
|
|
45
|
+
tenantId: input.tenantId,
|
|
46
|
+
mode: input.mode,
|
|
47
|
+
preferredHourUtc: input.preferredHourUtc ?? existing?.preferredHourUtc ?? 3,
|
|
48
|
+
});
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/trpc/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { createAdminFleetUpdateRouter } from "./admin-fleet-update-router.js";
|
|
1
2
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
2
3
|
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
|
|
3
4
|
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
package/dist/trpc/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { createAdminFleetUpdateRouter } from "./admin-fleet-update-router.js";
|
|
1
2
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
2
3
|
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
|
|
3
4
|
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
package/package.json
CHANGED
|
@@ -3,15 +3,33 @@ import { sha256 } from "@noble/hashes/sha2.js";
|
|
|
3
3
|
import { bech32 } from "@scure/base";
|
|
4
4
|
import { HDKey } from "@scure/bip32";
|
|
5
5
|
|
|
6
|
+
/** Supported UTXO chains for bech32 address derivation. */
|
|
7
|
+
export type UtxoChain = "bitcoin" | "litecoin";
|
|
8
|
+
|
|
9
|
+
/** Supported network types. */
|
|
10
|
+
export type UtxoNetwork = "mainnet" | "testnet" | "regtest";
|
|
11
|
+
|
|
12
|
+
/** Bech32 HRP (human-readable part) by chain and network. */
|
|
13
|
+
const BECH32_PREFIX = {
|
|
14
|
+
bitcoin: { mainnet: "bc", testnet: "tb", regtest: "bcrt" },
|
|
15
|
+
litecoin: { mainnet: "ltc", testnet: "tltc", regtest: "rltc" },
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
function getBech32Prefix(chain: UtxoChain, network: UtxoNetwork): string {
|
|
19
|
+
return BECH32_PREFIX[chain][network];
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
/**
|
|
7
|
-
* Derive a native segwit (bech32
|
|
23
|
+
* Derive a native segwit (bech32) deposit address from an xpub at a given index.
|
|
24
|
+
* Works for BTC (bc1q...) and LTC (ltc1q...) — same HASH160 + bech32 encoding.
|
|
8
25
|
* Path: xpub / 0 / index (external chain).
|
|
9
26
|
* No private keys involved.
|
|
10
27
|
*/
|
|
11
|
-
export function
|
|
28
|
+
export function deriveAddress(
|
|
12
29
|
xpub: string,
|
|
13
30
|
index: number,
|
|
14
|
-
network:
|
|
31
|
+
network: UtxoNetwork = "mainnet",
|
|
32
|
+
chain: UtxoChain = "bitcoin",
|
|
15
33
|
): string {
|
|
16
34
|
if (!Number.isInteger(index) || index < 0) throw new Error(`Invalid derivation index: ${index}`);
|
|
17
35
|
|
|
@@ -19,23 +37,37 @@ export function deriveBtcAddress(
|
|
|
19
37
|
const child = master.deriveChild(0).deriveChild(index);
|
|
20
38
|
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
21
39
|
|
|
22
|
-
// HASH160 = RIPEMD160(SHA256(compressedPubKey))
|
|
23
40
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
24
|
-
|
|
25
|
-
// Bech32 encode: witness version 0 + 20-byte hash
|
|
26
|
-
const prefix = network === "mainnet" ? "bc" : "tb";
|
|
41
|
+
const prefix = getBech32Prefix(chain, network);
|
|
27
42
|
const words = bech32.toWords(hash160);
|
|
28
43
|
return bech32.encode(prefix, [0, ...words]);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
/** Derive the
|
|
32
|
-
export function
|
|
46
|
+
/** Derive the treasury address (internal chain, index 0). */
|
|
47
|
+
export function deriveTreasury(xpub: string, network: UtxoNetwork = "mainnet", chain: UtxoChain = "bitcoin"): string {
|
|
33
48
|
const master = HDKey.fromExtendedKey(xpub);
|
|
34
49
|
const child = master.deriveChild(1).deriveChild(0); // internal chain
|
|
35
50
|
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
36
51
|
|
|
37
52
|
const hash160 = ripemd160(sha256(child.publicKey));
|
|
38
|
-
const prefix = network
|
|
53
|
+
const prefix = getBech32Prefix(chain, network);
|
|
39
54
|
const words = bech32.toWords(hash160);
|
|
40
55
|
return bech32.encode(prefix, [0, ...words]);
|
|
41
56
|
}
|
|
57
|
+
|
|
58
|
+
/** @deprecated Use `deriveAddress` instead. */
|
|
59
|
+
export const deriveBtcAddress = deriveAddress;
|
|
60
|
+
|
|
61
|
+
/** @deprecated Use `deriveTreasury` instead. */
|
|
62
|
+
export const deriveBtcTreasury = deriveTreasury;
|
|
63
|
+
|
|
64
|
+
/** Validate that a string is an xpub (not xprv). */
|
|
65
|
+
export function isValidXpub(key: string): boolean {
|
|
66
|
+
if (!key.startsWith("xpub")) return false;
|
|
67
|
+
try {
|
|
68
|
+
HDKey.fromExtendedKey(key);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { UtxoChain, UtxoNetwork } from "./address-gen.js";
|
|
2
|
+
export { deriveAddress, deriveBtcAddress, deriveBtcTreasury, deriveTreasury } from "./address-gen.js";
|
|
2
3
|
export type { BtcCheckoutDeps, BtcCheckoutResult } from "./checkout.js";
|
|
3
4
|
export { createBtcCheckout, MIN_BTC_USD } from "./checkout.js";
|
|
4
5
|
export { centsToSats, loadBitcoindConfig, satsToCents } from "./config.js";
|
|
@@ -287,8 +287,8 @@ describe("ContainerUpdater", () => {
|
|
|
287
287
|
docker.getContainer.mockReturnValue(startingContainer);
|
|
288
288
|
|
|
289
289
|
const promise = updater.updateBot("bot-1");
|
|
290
|
-
// Advance past the
|
|
291
|
-
for (let i = 0; i <
|
|
290
|
+
// Advance past the 120s timeout (120_000ms) in increments of poll interval (5_000ms)
|
|
291
|
+
for (let i = 0; i < 25; i++) {
|
|
292
292
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
293
293
|
}
|
|
294
294
|
const result = await promise;
|
package/src/fleet/updater.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { getContainerDigest } from "./image-poller.js";
|
|
|
6
6
|
import type { IProfileStore } from "./profile-store.js";
|
|
7
7
|
|
|
8
8
|
/** How long to wait for a container to become healthy after update (ms) */
|
|
9
|
-
const HEALTH_CHECK_TIMEOUT_MS =
|
|
9
|
+
const HEALTH_CHECK_TIMEOUT_MS = 120_000;
|
|
10
10
|
/** How often to check container health during update verification (ms) */
|
|
11
11
|
const HEALTH_CHECK_POLL_MS = 5_000;
|
|
12
12
|
|
|
@@ -41,7 +41,7 @@ export class ContainerUpdater {
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Update a bot's container to the latest image available for its release channel.
|
|
44
|
-
* Rolls back if the new container fails health checks within
|
|
44
|
+
* Rolls back if the new container fails health checks within 120s.
|
|
45
45
|
*/
|
|
46
46
|
async updateBot(botId: string): Promise<UpdateResult> {
|
|
47
47
|
if (this.updating.has(botId)) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../config/logger.js";
|
|
3
|
+
import type { RolloutOrchestrator } from "../fleet/rollout-orchestrator.js";
|
|
4
|
+
import type { ITenantUpdateConfigRepository } from "../fleet/tenant-update-config-repository.js";
|
|
5
|
+
import { adminProcedure, router } from "./init.js";
|
|
6
|
+
|
|
7
|
+
export function createAdminFleetUpdateRouter(
|
|
8
|
+
getOrchestrator: () => RolloutOrchestrator,
|
|
9
|
+
getConfigRepo: () => ITenantUpdateConfigRepository,
|
|
10
|
+
) {
|
|
11
|
+
return router({
|
|
12
|
+
/** Get current rollout status */
|
|
13
|
+
rolloutStatus: adminProcedure.query(() => {
|
|
14
|
+
const orchestrator = getOrchestrator();
|
|
15
|
+
return {
|
|
16
|
+
isRolling: orchestrator.isRolling,
|
|
17
|
+
};
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
/** Force trigger a rollout for all auto-update tenants */
|
|
21
|
+
forceRollout: adminProcedure.mutation(async () => {
|
|
22
|
+
const orchestrator = getOrchestrator();
|
|
23
|
+
logger.info("Admin: fleet.forceRollout");
|
|
24
|
+
// Fire and forget — don't block the admin request
|
|
25
|
+
orchestrator.rollout().catch((err: unknown) => {
|
|
26
|
+
logger.error("Force rollout failed", {
|
|
27
|
+
error: (err as Error).message,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
return { triggered: true };
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
/** List all tenant update configs */
|
|
34
|
+
listTenantConfigs: adminProcedure.query(async () => {
|
|
35
|
+
const repo = getConfigRepo();
|
|
36
|
+
return repo.listAutoEnabled();
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
/** Override a specific tenant's update config */
|
|
40
|
+
setTenantConfig: adminProcedure
|
|
41
|
+
.input(
|
|
42
|
+
z.object({
|
|
43
|
+
tenantId: z.string().min(1),
|
|
44
|
+
mode: z.enum(["auto", "manual"]),
|
|
45
|
+
preferredHourUtc: z.number().int().min(0).max(23).optional(),
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
.mutation(async ({ input }) => {
|
|
49
|
+
const repo = getConfigRepo();
|
|
50
|
+
const existing = await repo.get(input.tenantId);
|
|
51
|
+
await repo.upsert(input.tenantId, {
|
|
52
|
+
mode: input.mode,
|
|
53
|
+
preferredHourUtc: input.preferredHourUtc ?? existing?.preferredHourUtc ?? 3,
|
|
54
|
+
});
|
|
55
|
+
logger.info("Admin: fleet.setTenantConfig", {
|
|
56
|
+
tenantId: input.tenantId,
|
|
57
|
+
mode: input.mode,
|
|
58
|
+
preferredHourUtc: input.preferredHourUtc ?? existing?.preferredHourUtc ?? 3,
|
|
59
|
+
});
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/trpc/index.ts
CHANGED