@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.
@@ -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 from the
86
- * proxy route table. Returns null if no route exists or is unhealthy.
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 (!route || !route.healthy)
94
- return null;
95
- return `http://${route.upstreamHost}:${route.upstreamPort}`;
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 via proxy route table
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: "Tenant not found" }, 404);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.72.3",
3
+ "version": "1.73.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 from the
107
- * proxy route table. Returns null if no route exists or is unhealthy.
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(container: PlatformContainer, subdomain: string): string | null {
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 (!route || !route.healthy) return null;
114
- return `http://${route.upstreamHost}:${route.upstreamPort}`;
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 via proxy route table
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: "Tenant not found" }, 404);
184
+ return c.json({ error: "Container unavailable" }, 503);
169
185
  }
170
186
 
171
187
  const url = new URL(c.req.url);