@wopr-network/platform-core 1.72.3 → 1.73.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/server/lifecycle.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This module provides a standard interface for starting and stopping
|
|
6
6
|
* those tasks so bootPlatformServer can manage them uniformly.
|
|
7
7
|
*/
|
|
8
|
+
import { logger } from "../config/logger.js";
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// startBackgroundServices
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -36,6 +37,45 @@ export async function startBackgroundServices(container) {
|
|
|
36
37
|
// Non-fatal — pool will be empty but claiming falls back to cold create
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
// Runtime billing cron — daily $0.17/bot deduction (requires fleet + creditLedger)
|
|
41
|
+
if (container.fleet && container.creditLedger) {
|
|
42
|
+
try {
|
|
43
|
+
const { DrizzleBotInstanceRepository } = await import("../fleet/drizzle-bot-instance-repository.js");
|
|
44
|
+
const { DrizzleTenantAddonRepository } = await import("../monetization/addons/addon-repository.js");
|
|
45
|
+
const { startRuntimeScheduler } = await import("../monetization/credits/runtime-scheduler.js");
|
|
46
|
+
const botInstanceRepo = new DrizzleBotInstanceRepository(container.db);
|
|
47
|
+
const tenantAddonRepo = new DrizzleTenantAddonRepository(container.db);
|
|
48
|
+
const scheduler = startRuntimeScheduler({
|
|
49
|
+
ledger: container.creditLedger,
|
|
50
|
+
botInstanceRepo,
|
|
51
|
+
tenantAddonRepo,
|
|
52
|
+
});
|
|
53
|
+
handles.unsubscribes.push(scheduler.stop);
|
|
54
|
+
// Run immediately on startup (idempotent — skips if already billed today)
|
|
55
|
+
const { runRuntimeDeductions, buildResourceTierCosts } = await import("../monetization/credits/runtime-cron.js");
|
|
56
|
+
const { buildAddonCosts } = await import("../monetization/addons/addon-cron.js");
|
|
57
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
58
|
+
void runRuntimeDeductions({
|
|
59
|
+
ledger: container.creditLedger,
|
|
60
|
+
date: today,
|
|
61
|
+
getActiveBotCount: async (tenantId) => {
|
|
62
|
+
const bots = await botInstanceRepo.listByTenant(tenantId);
|
|
63
|
+
return bots.filter((b) => b.billingState === "active").length;
|
|
64
|
+
},
|
|
65
|
+
getResourceTierCosts: buildResourceTierCosts(botInstanceRepo, async (tenantId) => {
|
|
66
|
+
const bots = await botInstanceRepo.listByTenant(tenantId);
|
|
67
|
+
return bots.filter((b) => b.billingState === "active").map((b) => b.id);
|
|
68
|
+
}),
|
|
69
|
+
getAddonCosts: buildAddonCosts(tenantAddonRepo),
|
|
70
|
+
})
|
|
71
|
+
.then((result) => logger.info("Initial runtime deductions complete", result))
|
|
72
|
+
.catch((err) => logger.error("Initial runtime deductions failed", { error: String(err) }));
|
|
73
|
+
logger.info("Runtime billing scheduler started (daily $0.17/bot deduction)");
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
logger.warn("Failed to start runtime billing scheduler (non-fatal)", { error: String(err) });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
39
79
|
return handles;
|
|
40
80
|
}
|
|
41
81
|
// ---------------------------------------------------------------------------
|
|
@@ -82,17 +82,26 @@ export function buildUpstreamHeaders(incoming, user, tenantSubdomain) {
|
|
|
82
82
|
return headers;
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
-
* Resolve the upstream container URL for a tenant subdomain
|
|
86
|
-
*
|
|
85
|
+
* Resolve the upstream container URL for a tenant subdomain.
|
|
86
|
+
*
|
|
87
|
+
* First checks the in-memory proxy route table (populated during
|
|
88
|
+
* provisioning). Falls back to deriving from the persistent profile
|
|
89
|
+
* data so that routing survives server restarts without needing
|
|
90
|
+
* Caddy or in-memory state.
|
|
87
91
|
*/
|
|
88
|
-
function resolveContainerUrl(container, subdomain) {
|
|
92
|
+
function resolveContainerUrl(container, subdomain, profile) {
|
|
89
93
|
if (!container.fleet)
|
|
90
94
|
return null;
|
|
95
|
+
// Fast path: in-memory route table (populated during provisioning)
|
|
91
96
|
const routes = container.fleet.proxy.getRoutes();
|
|
92
97
|
const route = routes.find((r) => r.subdomain === subdomain);
|
|
93
|
-
if (
|
|
94
|
-
return
|
|
95
|
-
|
|
98
|
+
if (route?.healthy) {
|
|
99
|
+
return `http://${route.upstreamHost}:${route.upstreamPort}`;
|
|
100
|
+
}
|
|
101
|
+
// Fallback: derive from persistent profile data (survives restarts)
|
|
102
|
+
const containerName = `wopr-${profile.name.replace(/_/g, "-")}`;
|
|
103
|
+
const port = profile.env?.PORT || "3100";
|
|
104
|
+
return `http://${containerName}:${port}`;
|
|
96
105
|
}
|
|
97
106
|
/**
|
|
98
107
|
* Create a tenant subdomain proxy middleware.
|
|
@@ -134,10 +143,10 @@ export function createTenantProxyMiddleware(container, config) {
|
|
|
134
143
|
if (!hasAccess) {
|
|
135
144
|
return c.json({ error: "Forbidden: not a member of this tenant" }, 403);
|
|
136
145
|
}
|
|
137
|
-
// Resolve fleet container URL
|
|
138
|
-
const upstream = resolveContainerUrl(container, subdomain);
|
|
146
|
+
// Resolve fleet container URL (route table or profile fallback)
|
|
147
|
+
const upstream = resolveContainerUrl(container, subdomain, profile);
|
|
139
148
|
if (!upstream) {
|
|
140
|
-
return c.json({ error: "
|
|
149
|
+
return c.json({ error: "Container unavailable" }, 503);
|
|
141
150
|
}
|
|
142
151
|
const url = new URL(c.req.url);
|
|
143
152
|
const targetUrl = `${upstream}${url.pathname}${url.search}`;
|
package/package.json
CHANGED
package/src/server/lifecycle.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* those tasks so bootPlatformServer can manage them uniformly.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { logger } from "../config/logger.js";
|
|
9
10
|
import type { PlatformContainer } from "./container.js";
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -50,6 +51,49 @@ export async function startBackgroundServices(container: PlatformContainer): Pro
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Runtime billing cron — daily $0.17/bot deduction (requires fleet + creditLedger)
|
|
55
|
+
if (container.fleet && container.creditLedger) {
|
|
56
|
+
try {
|
|
57
|
+
const { DrizzleBotInstanceRepository } = await import("../fleet/drizzle-bot-instance-repository.js");
|
|
58
|
+
const { DrizzleTenantAddonRepository } = await import("../monetization/addons/addon-repository.js");
|
|
59
|
+
const { startRuntimeScheduler } = await import("../monetization/credits/runtime-scheduler.js");
|
|
60
|
+
|
|
61
|
+
const botInstanceRepo = new DrizzleBotInstanceRepository(container.db);
|
|
62
|
+
const tenantAddonRepo = new DrizzleTenantAddonRepository(container.db);
|
|
63
|
+
|
|
64
|
+
const scheduler = startRuntimeScheduler({
|
|
65
|
+
ledger: container.creditLedger,
|
|
66
|
+
botInstanceRepo,
|
|
67
|
+
tenantAddonRepo,
|
|
68
|
+
});
|
|
69
|
+
handles.unsubscribes.push(scheduler.stop);
|
|
70
|
+
|
|
71
|
+
// Run immediately on startup (idempotent — skips if already billed today)
|
|
72
|
+
const { runRuntimeDeductions, buildResourceTierCosts } = await import("../monetization/credits/runtime-cron.js");
|
|
73
|
+
const { buildAddonCosts } = await import("../monetization/addons/addon-cron.js");
|
|
74
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
75
|
+
void runRuntimeDeductions({
|
|
76
|
+
ledger: container.creditLedger,
|
|
77
|
+
date: today,
|
|
78
|
+
getActiveBotCount: async (tenantId) => {
|
|
79
|
+
const bots = await botInstanceRepo.listByTenant(tenantId);
|
|
80
|
+
return bots.filter((b) => b.billingState === "active").length;
|
|
81
|
+
},
|
|
82
|
+
getResourceTierCosts: buildResourceTierCosts(botInstanceRepo, async (tenantId) => {
|
|
83
|
+
const bots = await botInstanceRepo.listByTenant(tenantId);
|
|
84
|
+
return bots.filter((b) => b.billingState === "active").map((b) => b.id);
|
|
85
|
+
}),
|
|
86
|
+
getAddonCosts: buildAddonCosts(tenantAddonRepo),
|
|
87
|
+
})
|
|
88
|
+
.then((result) => logger.info("Initial runtime deductions complete", result))
|
|
89
|
+
.catch((err) => logger.error("Initial runtime deductions failed", { error: String(err) }));
|
|
90
|
+
|
|
91
|
+
logger.info("Runtime billing scheduler started (daily $0.17/bot deduction)");
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger.warn("Failed to start runtime billing scheduler (non-fatal)", { error: String(err) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
return handles;
|
|
54
98
|
}
|
|
55
99
|
|
|
@@ -103,15 +103,31 @@ export function buildUpstreamHeaders(incoming: Headers, user: ProxyUserInfo, ten
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* Resolve the upstream container URL for a tenant subdomain
|
|
107
|
-
*
|
|
106
|
+
* Resolve the upstream container URL for a tenant subdomain.
|
|
107
|
+
*
|
|
108
|
+
* First checks the in-memory proxy route table (populated during
|
|
109
|
+
* provisioning). Falls back to deriving from the persistent profile
|
|
110
|
+
* data so that routing survives server restarts without needing
|
|
111
|
+
* Caddy or in-memory state.
|
|
108
112
|
*/
|
|
109
|
-
function resolveContainerUrl(
|
|
113
|
+
function resolveContainerUrl(
|
|
114
|
+
container: PlatformContainer,
|
|
115
|
+
subdomain: string,
|
|
116
|
+
profile: { name: string; env?: Record<string, string> },
|
|
117
|
+
): string | null {
|
|
110
118
|
if (!container.fleet) return null;
|
|
119
|
+
|
|
120
|
+
// Fast path: in-memory route table (populated during provisioning)
|
|
111
121
|
const routes = container.fleet.proxy.getRoutes();
|
|
112
122
|
const route = routes.find((r) => r.subdomain === subdomain);
|
|
113
|
-
if (
|
|
114
|
-
|
|
123
|
+
if (route?.healthy) {
|
|
124
|
+
return `http://${route.upstreamHost}:${route.upstreamPort}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback: derive from persistent profile data (survives restarts)
|
|
128
|
+
const containerName = `wopr-${profile.name.replace(/_/g, "-")}`;
|
|
129
|
+
const port = profile.env?.PORT || "3100";
|
|
130
|
+
return `http://${containerName}:${port}`;
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
/**
|
|
@@ -162,10 +178,10 @@ export function createTenantProxyMiddleware(
|
|
|
162
178
|
return c.json({ error: "Forbidden: not a member of this tenant" }, 403);
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
// Resolve fleet container URL
|
|
166
|
-
const upstream = resolveContainerUrl(container, subdomain);
|
|
181
|
+
// Resolve fleet container URL (route table or profile fallback)
|
|
182
|
+
const upstream = resolveContainerUrl(container, subdomain, profile);
|
|
167
183
|
if (!upstream) {
|
|
168
|
-
return c.json({ error: "
|
|
184
|
+
return c.json({ error: "Container unavailable" }, 503);
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
const url = new URL(c.req.url);
|