@wopr-network/platform-core 1.68.0 → 1.70.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.
Files changed (93) hide show
  1. package/dist/backup/types.d.ts +1 -1
  2. package/dist/db/schema/pool-config.d.ts +41 -0
  3. package/dist/db/schema/pool-config.js +5 -0
  4. package/dist/db/schema/pool-instances.d.ts +126 -0
  5. package/dist/db/schema/pool-instances.js +10 -0
  6. package/dist/server/__tests__/build-container.test.d.ts +1 -0
  7. package/dist/server/__tests__/build-container.test.js +339 -0
  8. package/dist/server/__tests__/container.test.d.ts +1 -0
  9. package/dist/server/__tests__/container.test.js +173 -0
  10. package/dist/server/__tests__/lifecycle.test.d.ts +1 -0
  11. package/dist/server/__tests__/lifecycle.test.js +90 -0
  12. package/dist/server/__tests__/mount-routes.test.d.ts +1 -0
  13. package/dist/server/__tests__/mount-routes.test.js +151 -0
  14. package/dist/server/boot-config.d.ts +51 -0
  15. package/dist/server/boot-config.js +7 -0
  16. package/dist/server/container.d.ts +97 -0
  17. package/dist/server/container.js +148 -0
  18. package/dist/server/index.d.ts +33 -0
  19. package/dist/server/index.js +66 -0
  20. package/dist/server/lifecycle.d.ts +25 -0
  21. package/dist/server/lifecycle.js +56 -0
  22. package/dist/server/middleware/__tests__/admin-auth.test.d.ts +1 -0
  23. package/dist/server/middleware/__tests__/admin-auth.test.js +59 -0
  24. package/dist/server/middleware/__tests__/tenant-proxy.test.d.ts +1 -0
  25. package/dist/server/middleware/__tests__/tenant-proxy.test.js +268 -0
  26. package/dist/server/middleware/admin-auth.d.ts +18 -0
  27. package/dist/server/middleware/admin-auth.js +38 -0
  28. package/dist/server/middleware/tenant-proxy.d.ts +56 -0
  29. package/dist/server/middleware/tenant-proxy.js +162 -0
  30. package/dist/server/mount-routes.d.ts +30 -0
  31. package/dist/server/mount-routes.js +74 -0
  32. package/dist/server/routes/__tests__/admin.test.d.ts +1 -0
  33. package/dist/server/routes/__tests__/admin.test.js +267 -0
  34. package/dist/server/routes/__tests__/crypto-webhook.test.d.ts +1 -0
  35. package/dist/server/routes/__tests__/crypto-webhook.test.js +137 -0
  36. package/dist/server/routes/__tests__/provision-webhook.test.d.ts +1 -0
  37. package/dist/server/routes/__tests__/provision-webhook.test.js +212 -0
  38. package/dist/server/routes/__tests__/stripe-webhook.test.d.ts +1 -0
  39. package/dist/server/routes/__tests__/stripe-webhook.test.js +65 -0
  40. package/dist/server/routes/admin.d.ts +129 -0
  41. package/dist/server/routes/admin.js +294 -0
  42. package/dist/server/routes/crypto-webhook.d.ts +23 -0
  43. package/dist/server/routes/crypto-webhook.js +82 -0
  44. package/dist/server/routes/provision-webhook.d.ts +38 -0
  45. package/dist/server/routes/provision-webhook.js +160 -0
  46. package/dist/server/routes/stripe-webhook.d.ts +10 -0
  47. package/dist/server/routes/stripe-webhook.js +29 -0
  48. package/dist/server/services/hot-pool-claim.d.ts +30 -0
  49. package/dist/server/services/hot-pool-claim.js +92 -0
  50. package/dist/server/services/hot-pool.d.ts +25 -0
  51. package/dist/server/services/hot-pool.js +129 -0
  52. package/dist/server/services/pool-repository.d.ts +44 -0
  53. package/dist/server/services/pool-repository.js +72 -0
  54. package/dist/server/test-container.d.ts +15 -0
  55. package/dist/server/test-container.js +103 -0
  56. package/dist/trpc/auth-helpers.d.ts +17 -0
  57. package/dist/trpc/auth-helpers.js +26 -0
  58. package/dist/trpc/container-factories.d.ts +300 -0
  59. package/dist/trpc/container-factories.js +80 -0
  60. package/dist/trpc/index.d.ts +2 -0
  61. package/dist/trpc/index.js +2 -0
  62. package/drizzle/migrations/0025_hot_pool_tables.sql +29 -0
  63. package/package.json +5 -1
  64. package/src/db/schema/pool-config.ts +6 -0
  65. package/src/db/schema/pool-instances.ts +11 -0
  66. package/src/server/__tests__/build-container.test.ts +402 -0
  67. package/src/server/__tests__/container.test.ts +207 -0
  68. package/src/server/__tests__/lifecycle.test.ts +106 -0
  69. package/src/server/__tests__/mount-routes.test.ts +169 -0
  70. package/src/server/boot-config.ts +84 -0
  71. package/src/server/container.ts +264 -0
  72. package/src/server/index.ts +92 -0
  73. package/src/server/lifecycle.ts +72 -0
  74. package/src/server/middleware/__tests__/admin-auth.test.ts +67 -0
  75. package/src/server/middleware/__tests__/tenant-proxy.test.ts +308 -0
  76. package/src/server/middleware/admin-auth.ts +51 -0
  77. package/src/server/middleware/tenant-proxy.ts +192 -0
  78. package/src/server/mount-routes.ts +113 -0
  79. package/src/server/routes/__tests__/admin.test.ts +320 -0
  80. package/src/server/routes/__tests__/crypto-webhook.test.ts +167 -0
  81. package/src/server/routes/__tests__/provision-webhook.test.ts +323 -0
  82. package/src/server/routes/__tests__/stripe-webhook.test.ts +73 -0
  83. package/src/server/routes/admin.ts +360 -0
  84. package/src/server/routes/crypto-webhook.ts +110 -0
  85. package/src/server/routes/provision-webhook.ts +212 -0
  86. package/src/server/routes/stripe-webhook.ts +36 -0
  87. package/src/server/services/hot-pool-claim.ts +130 -0
  88. package/src/server/services/hot-pool.ts +174 -0
  89. package/src/server/services/pool-repository.ts +107 -0
  90. package/src/server/test-container.ts +120 -0
  91. package/src/trpc/auth-helpers.ts +28 -0
  92. package/src/trpc/container-factories.ts +114 -0
  93. package/src/trpc/index.ts +9 -0
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { gracefulShutdown, startBackgroundServices } from "../lifecycle.js";
3
+ import { createTestContainer } from "../test-container.js";
4
+
5
+ describe("startBackgroundServices", () => {
6
+ it("returns a BackgroundHandles object", async () => {
7
+ const container = createTestContainer();
8
+ const handles = await startBackgroundServices(container);
9
+ expect(handles).toHaveProperty("intervals");
10
+ expect(handles).toHaveProperty("unsubscribes");
11
+ expect(Array.isArray(handles.intervals)).toBe(true);
12
+ expect(Array.isArray(handles.unsubscribes)).toBe(true);
13
+ });
14
+
15
+ it("calls proxy.start when fleet is enabled", async () => {
16
+ const startFn = vi.fn().mockResolvedValue(undefined);
17
+ const container = createTestContainer({
18
+ fleet: {
19
+ manager: {} as never,
20
+ docker: {} as never,
21
+ proxy: {
22
+ start: startFn,
23
+ addRoute: async () => {},
24
+ removeRoute: () => {},
25
+ getRoutes: () => [],
26
+ } as never,
27
+ profileStore: { list: async () => [] } as never,
28
+ serviceKeyRepo: {} as never,
29
+ },
30
+ });
31
+ await startBackgroundServices(container);
32
+ expect(startFn).toHaveBeenCalledOnce();
33
+ });
34
+
35
+ it("does not throw when proxy.start fails", async () => {
36
+ const container = createTestContainer({
37
+ fleet: {
38
+ manager: {} as never,
39
+ docker: {} as never,
40
+ proxy: {
41
+ start: async () => {
42
+ throw new Error("proxy start failed");
43
+ },
44
+ addRoute: async () => {},
45
+ removeRoute: () => {},
46
+ getRoutes: () => [],
47
+ } as never,
48
+ profileStore: { list: async () => [] } as never,
49
+ serviceKeyRepo: {} as never,
50
+ },
51
+ });
52
+ // Should not throw
53
+ const handles = await startBackgroundServices(container);
54
+ expect(handles).toBeDefined();
55
+ });
56
+ });
57
+
58
+ describe("gracefulShutdown", () => {
59
+ it("clears all intervals", async () => {
60
+ const container = createTestContainer();
61
+ const clearSpy = vi.spyOn(global, "clearInterval");
62
+
63
+ const interval1 = setInterval(() => {}, 10000);
64
+ const interval2 = setInterval(() => {}, 10000);
65
+
66
+ const handles = {
67
+ intervals: [interval1, interval2],
68
+ unsubscribes: [],
69
+ };
70
+
71
+ await gracefulShutdown(container, handles);
72
+
73
+ expect(clearSpy).toHaveBeenCalledWith(interval1);
74
+ expect(clearSpy).toHaveBeenCalledWith(interval2);
75
+ clearSpy.mockRestore();
76
+ });
77
+
78
+ it("calls all unsubscribe functions", async () => {
79
+ const container = createTestContainer();
80
+ const unsub1 = vi.fn();
81
+ const unsub2 = vi.fn();
82
+
83
+ const handles = {
84
+ intervals: [],
85
+ unsubscribes: [unsub1, unsub2],
86
+ };
87
+
88
+ await gracefulShutdown(container, handles);
89
+
90
+ expect(unsub1).toHaveBeenCalledOnce();
91
+ expect(unsub2).toHaveBeenCalledOnce();
92
+ });
93
+
94
+ it("calls pool.end()", async () => {
95
+ const endFn = vi.fn().mockResolvedValue(undefined);
96
+ const container = createTestContainer({
97
+ pool: { end: endFn } as never,
98
+ });
99
+
100
+ const handles = { intervals: [], unsubscribes: [] };
101
+
102
+ await gracefulShutdown(container, handles);
103
+
104
+ expect(endFn).toHaveBeenCalledOnce();
105
+ });
106
+ });
@@ -0,0 +1,169 @@
1
+ import { Hono } from "hono";
2
+ import { describe, expect, it } from "vitest";
3
+ import type { PlatformContainer } from "../container.js";
4
+ import { mountRoutes } from "../mount-routes.js";
5
+ import { createTestContainer } from "../test-container.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function defaultMountConfig() {
12
+ return {
13
+ provisionSecret: "test-secret",
14
+ cryptoServiceKey: "test-crypto-key",
15
+ platformDomain: "example.com",
16
+ };
17
+ }
18
+
19
+ function makeApp(container: PlatformContainer) {
20
+ const app = new Hono();
21
+ mountRoutes(app, container, defaultMountConfig());
22
+ return app;
23
+ }
24
+
25
+ async function req(app: Hono, method: string, path: string, opts?: RequestInit) {
26
+ const request = new Request(`http://localhost${path}`, { method, ...opts });
27
+ return app.request(request);
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Minimal stubs for feature sub-containers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function stubCrypto(): PlatformContainer["crypto"] {
35
+ return {
36
+ chargeRepo: {} as never,
37
+ webhookSeenRepo: {} as never,
38
+ };
39
+ }
40
+
41
+ function stubStripe(): PlatformContainer["stripe"] {
42
+ return {
43
+ stripe: {} as never,
44
+ webhookSecret: "whsec_test",
45
+ customerRepo: {} as never,
46
+ processor: {
47
+ handleWebhook: async () => ({ ok: true }),
48
+ },
49
+ };
50
+ }
51
+
52
+ function stubFleet(): PlatformContainer["fleet"] {
53
+ return {
54
+ manager: {} as never,
55
+ docker: {} as never,
56
+ proxy: {
57
+ start: async () => {},
58
+ addRoute: async () => {},
59
+ removeRoute: () => {},
60
+ getRoutes: () => [],
61
+ } as never,
62
+ profileStore: { list: async () => [] } as never,
63
+ serviceKeyRepo: {} as never,
64
+ };
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Tests
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe("mountRoutes", () => {
72
+ // 1. Health endpoint always available
73
+ it("mounts /health endpoint", async () => {
74
+ const app = makeApp(createTestContainer());
75
+ const res = await req(app, "GET", "/health");
76
+ expect(res.status).toBe(200);
77
+ const body = await res.json();
78
+ expect(body).toEqual({ ok: true });
79
+ });
80
+
81
+ // 2. Crypto webhook mounted when crypto enabled
82
+ it("mounts crypto webhook when crypto enabled", async () => {
83
+ const container = createTestContainer({ crypto: stubCrypto() });
84
+ const app = makeApp(container);
85
+ const res = await req(app, "POST", "/api/webhooks/crypto", {
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({}),
88
+ });
89
+ // Should return 401 (no auth), NOT 404 (not found)
90
+ expect(res.status).toBe(401);
91
+ });
92
+
93
+ // 3. Crypto webhook NOT mounted when crypto disabled
94
+ it("does not mount crypto webhook when crypto disabled", async () => {
95
+ const container = createTestContainer({ crypto: null });
96
+ const app = makeApp(container);
97
+ const res = await req(app, "POST", "/api/webhooks/crypto", {
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({}),
100
+ });
101
+ expect(res.status).toBe(404);
102
+ });
103
+
104
+ // 4. Stripe webhook mounted when stripe enabled
105
+ it("mounts stripe webhook when stripe enabled", async () => {
106
+ const container = createTestContainer({ stripe: stubStripe() });
107
+ const app = makeApp(container);
108
+ const res = await req(app, "POST", "/api/webhooks/stripe", {
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({}),
111
+ });
112
+ // Should return 400 (missing stripe-signature), NOT 404
113
+ expect(res.status).toBe(400);
114
+ });
115
+
116
+ // 5. Stripe webhook NOT mounted when stripe disabled
117
+ it("does not mount stripe webhook when stripe disabled", async () => {
118
+ const container = createTestContainer({ stripe: null });
119
+ const app = makeApp(container);
120
+ const res = await req(app, "POST", "/api/webhooks/stripe", {
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({}),
123
+ });
124
+ expect(res.status).toBe(404);
125
+ });
126
+
127
+ // 6. Provision webhook mounted when fleet enabled
128
+ it("mounts provision webhook when fleet enabled", async () => {
129
+ const container = createTestContainer({ fleet: stubFleet() });
130
+ const app = makeApp(container);
131
+ const res = await req(app, "POST", "/api/provision/create", {
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({}),
134
+ });
135
+ // Should return 401 (no auth), NOT 404
136
+ expect(res.status).toBe(401);
137
+ });
138
+
139
+ // 7. Provision webhook NOT mounted when fleet disabled
140
+ it("does not mount provision webhook when fleet disabled", async () => {
141
+ const container = createTestContainer({ fleet: null });
142
+ const app = makeApp(container);
143
+ const res = await req(app, "POST", "/api/provision/create", {
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify({}),
146
+ });
147
+ expect(res.status).toBe(404);
148
+ });
149
+
150
+ // 8. Product-specific route plugins
151
+ it("mounts product-specific route plugins", async () => {
152
+ const container = createTestContainer();
153
+ const app = new Hono();
154
+ mountRoutes(app, container, defaultMountConfig(), [
155
+ {
156
+ path: "/api/custom",
157
+ handler: () => {
158
+ const sub = new Hono();
159
+ sub.get("/ping", (c) => c.json({ pong: true }));
160
+ return sub;
161
+ },
162
+ },
163
+ ]);
164
+ const res = await req(app, "GET", "/api/custom/ping");
165
+ expect(res.status).toBe(200);
166
+ const body = await res.json();
167
+ expect(body).toEqual({ pong: true });
168
+ });
169
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * BootConfig — declarative configuration for platformBoot().
3
+ *
4
+ * Products pass a BootConfig describing which features to enable and
5
+ * receive back a fully-wired Hono app + PlatformContainer.
6
+ */
7
+
8
+ import type { Hono } from "hono";
9
+ import type { PlatformContainer } from "./container.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Feature flags
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface FeatureFlags {
16
+ fleet: boolean;
17
+ crypto: boolean;
18
+ stripe: boolean;
19
+ gateway: boolean;
20
+ hotPool: boolean;
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Route plugins
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface RoutePlugin {
28
+ path: string;
29
+ handler: (container: PlatformContainer) => Hono;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Boot config
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export interface BootConfig {
37
+ /** Short product identifier (e.g. "paperclip", "wopr", "holyship"). */
38
+ slug: string;
39
+
40
+ /** PostgreSQL connection string. */
41
+ databaseUrl: string;
42
+
43
+ /** Bind host (default "0.0.0.0"). */
44
+ host?: string;
45
+
46
+ /** Bind port (default 3001). */
47
+ port?: number;
48
+
49
+ /** Which optional feature slices to wire up. */
50
+ features: FeatureFlags;
51
+
52
+ /** Additional Hono sub-apps mounted after core routes. */
53
+ routes?: RoutePlugin[];
54
+
55
+ /** Required when features.stripe is true. */
56
+ stripeSecretKey?: string;
57
+
58
+ /** Required when features.stripe is true. */
59
+ stripeWebhookSecret?: string;
60
+
61
+ /** Service key for the crypto chain server webhook endpoint. */
62
+ cryptoServiceKey?: string;
63
+
64
+ /** Shared secret used to authenticate provision requests. */
65
+ provisionSecret: string;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Boot result
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export interface BootResult {
73
+ /** The fully-wired Hono application. */
74
+ app: Hono;
75
+
76
+ /** The assembled DI container — useful for tests and ad-hoc access. */
77
+ container: PlatformContainer;
78
+
79
+ /** Start listening. Uses BootConfig.port unless overridden. */
80
+ start: (port?: number) => Promise<void>;
81
+
82
+ /** Graceful shutdown: drain connections, close pool. */
83
+ stop: () => Promise<void>;
84
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * PlatformContainer — the central DI container for platform-core.
3
+ *
4
+ * Products compose a container at boot time, enabling only the feature
5
+ * slices they need. Nullable sub-containers (fleet, crypto, stripe,
6
+ * gateway, hotPool) let each product opt in without pulling unused deps.
7
+ */
8
+
9
+ import type Docker from "dockerode";
10
+ import type { Pool } from "pg";
11
+ import type Stripe from "stripe";
12
+ import type { IUserRoleRepository } from "../auth/user-role-repository.js";
13
+ import type { ICryptoChargeRepository } from "../billing/crypto/charge-store.js";
14
+ import type { IWebhookSeenRepository } from "../billing/webhook-seen-repository.js";
15
+ import type { ILedger } from "../credits/ledger.js";
16
+ import type { ITenantCustomerRepository } from "../credits/tenant-customer-repository.js";
17
+ import type { DrizzleDb } from "../db/index.js";
18
+ import type { FleetManager } from "../fleet/fleet-manager.js";
19
+ import type { IProfileStore } from "../fleet/profile-store.js";
20
+ import type { IServiceKeyRepository } from "../gateway/service-key-repository.js";
21
+ import type { ProductConfig } from "../product-config/repository-types.js";
22
+ import type { ProxyManagerInterface } from "../proxy/types.js";
23
+ import type { IOrgMemberRepository } from "../tenancy/org-member-repository.js";
24
+ import type { OrgService } from "../tenancy/org-service.js";
25
+ import type { BootConfig } from "./boot-config.js";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Feature sub-containers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface FleetServices {
32
+ manager: FleetManager;
33
+ docker: Docker;
34
+ proxy: ProxyManagerInterface;
35
+ profileStore: IProfileStore;
36
+ serviceKeyRepo: IServiceKeyRepository;
37
+ }
38
+
39
+ export interface CryptoServices {
40
+ chargeRepo: ICryptoChargeRepository;
41
+ webhookSeenRepo: IWebhookSeenRepository;
42
+ }
43
+
44
+ export interface StripeServices {
45
+ stripe: Stripe;
46
+ webhookSecret: string;
47
+ customerRepo: ITenantCustomerRepository;
48
+ processor: { handleWebhook(payload: Buffer, signature: string): Promise<unknown> };
49
+ }
50
+
51
+ export interface GatewayServices {
52
+ serviceKeyRepo: IServiceKeyRepository;
53
+ }
54
+
55
+ export interface HotPoolServices {
56
+ /** Start the pool manager (replenish loop + cleanup). */
57
+ start: () => Promise<{ stop: () => void }>;
58
+ /** Claim a warm instance from the pool. Returns null if empty. */
59
+ claim: (
60
+ name: string,
61
+ tenantId: string,
62
+ adminUser: { id: string; email: string; name: string },
63
+ ) => Promise<{ id: string; name: string; subdomain: string } | null>;
64
+ /** Get current pool size from DB. */
65
+ getPoolSize: () => Promise<number>;
66
+ /** Set pool size in DB. */
67
+ setPoolSize: (size: number) => Promise<void>;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Main container
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export interface PlatformContainer {
75
+ db: DrizzleDb;
76
+ pool: Pool;
77
+ productConfig: ProductConfig;
78
+ creditLedger: ILedger;
79
+ orgMemberRepo: IOrgMemberRepository;
80
+ orgService: OrgService;
81
+ userRoleRepo: IUserRoleRepository;
82
+
83
+ /** Null when the product does not use fleet management. */
84
+ fleet: FleetServices | null;
85
+ /** Null when the product does not accept crypto payments. */
86
+ crypto: CryptoServices | null;
87
+ /** Null when the product does not use Stripe billing. */
88
+ stripe: StripeServices | null;
89
+ /** Null when the product does not expose a metered inference gateway. */
90
+ gateway: GatewayServices | null;
91
+ /** Null when the product does not use a hot-pool of pre-provisioned instances. */
92
+ hotPool: HotPoolServices | null;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // buildContainer — construct a PlatformContainer from a BootConfig
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Build a fully-wired PlatformContainer from a declarative BootConfig.
101
+ *
102
+ * Construction order mirrors the proven boot sequence from product index.ts
103
+ * files: DB pool -> Drizzle -> migrations -> productConfig -> credit ledger
104
+ * -> org repos -> org service -> user role repo -> feature services.
105
+ *
106
+ * Feature sub-containers (fleet, crypto, stripe, gateway) are only
107
+ * constructed when their corresponding feature flag is enabled in
108
+ * `bootConfig.features`. Disabled features yield `null`.
109
+ */
110
+ export async function buildContainer(bootConfig: BootConfig): Promise<PlatformContainer> {
111
+ if (!bootConfig.databaseUrl) {
112
+ throw new Error("buildContainer: databaseUrl is required");
113
+ }
114
+
115
+ // 1. Database pool
116
+ const { Pool: PgPool } = await import("pg");
117
+ const pool: Pool = new PgPool({ connectionString: bootConfig.databaseUrl });
118
+
119
+ // 2. Drizzle ORM instance
120
+ const { createDb } = await import("../db/index.js");
121
+ const db = createDb(pool);
122
+
123
+ // 3. Run Drizzle migrations
124
+ const { migrate } = await import("drizzle-orm/node-postgres/migrator");
125
+ const path = await import("node:path");
126
+ const migrationsFolder = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../drizzle");
127
+ await migrate(db as never, { migrationsFolder });
128
+
129
+ // 4. Bootstrap product config from DB (auto-seeds from presets if needed)
130
+ const { platformBoot } = await import("../product-config/boot.js");
131
+ const { config: productConfig } = await platformBoot({ slug: bootConfig.slug, db });
132
+
133
+ // 5. Credit ledger
134
+ const { DrizzleLedger } = await import("../credits/ledger.js");
135
+ const creditLedger: ILedger = new DrizzleLedger(db as never);
136
+ await creditLedger.seedSystemAccounts();
137
+
138
+ // 6. Org repositories + OrgService
139
+ const { DrizzleOrgMemberRepository } = await import("../tenancy/org-member-repository.js");
140
+ const { DrizzleOrgRepository } = await import("../tenancy/drizzle-org-repository.js");
141
+ const { OrgService: OrgServiceClass } = await import("../tenancy/org-service.js");
142
+ const { BetterAuthUserRepository } = await import("../db/auth-user-repository.js");
143
+
144
+ const orgMemberRepo: IOrgMemberRepository = new DrizzleOrgMemberRepository(db as never);
145
+ const orgRepo = new DrizzleOrgRepository(db as never);
146
+ const authUserRepo = new BetterAuthUserRepository(pool);
147
+ const orgService = new OrgServiceClass(orgRepo, orgMemberRepo, db as never, {
148
+ userRepo: authUserRepo,
149
+ });
150
+
151
+ // 7. User role repository
152
+ const { DrizzleUserRoleRepository } = await import("../auth/user-role-repository.js");
153
+ const userRoleRepo: IUserRoleRepository = new DrizzleUserRoleRepository(db as never);
154
+
155
+ // 8. Fleet services (when enabled)
156
+ let fleet: FleetServices | null = null;
157
+ if (bootConfig.features.fleet) {
158
+ const { FleetManager: FleetManagerClass } = await import("../fleet/fleet-manager.js");
159
+ const { ProfileStore } = await import("../fleet/profile-store.js");
160
+ const { ProxyManager } = await import("../proxy/manager.js");
161
+ const { DrizzleServiceKeyRepository } = await import("../gateway/service-key-repository.js");
162
+ const DockerModule = await import("dockerode");
163
+ const DockerClass = DockerModule.default ?? DockerModule;
164
+
165
+ const docker: Docker = new (DockerClass as new () => Docker)();
166
+ const fleetDataDir = productConfig.fleet?.fleetDataDir ?? "/data/fleet";
167
+ const profileStore: IProfileStore = new ProfileStore(fleetDataDir);
168
+ const proxy: ProxyManagerInterface = new ProxyManager();
169
+ const serviceKeyRepo: IServiceKeyRepository = new DrizzleServiceKeyRepository(db as never);
170
+ const manager: FleetManager = new FleetManagerClass(
171
+ docker,
172
+ profileStore,
173
+ undefined, // platformDiscovery
174
+ undefined, // networkPolicy
175
+ proxy,
176
+ );
177
+
178
+ fleet = { manager, docker, proxy, profileStore, serviceKeyRepo };
179
+ }
180
+
181
+ // 9. Crypto services (when enabled)
182
+ let crypto: CryptoServices | null = null;
183
+ if (bootConfig.features.crypto) {
184
+ const { DrizzleCryptoChargeRepository } = await import("../billing/crypto/charge-store.js");
185
+ const { DrizzleWebhookSeenRepository } = await import("../billing/drizzle-webhook-seen-repository.js");
186
+
187
+ const chargeRepo: ICryptoChargeRepository = new DrizzleCryptoChargeRepository(db as never);
188
+ const webhookSeenRepo: IWebhookSeenRepository = new DrizzleWebhookSeenRepository(db as never);
189
+
190
+ crypto = { chargeRepo, webhookSeenRepo };
191
+ }
192
+
193
+ // 10. Stripe services (when enabled)
194
+ let stripe: StripeServices | null = null;
195
+ if (bootConfig.features.stripe && bootConfig.stripeSecretKey) {
196
+ const StripeModule = await import("stripe");
197
+ const StripeClass = StripeModule.default;
198
+ const stripeClient: Stripe = new StripeClass(bootConfig.stripeSecretKey);
199
+
200
+ const { DrizzleTenantCustomerRepository } = await import("../billing/stripe/tenant-store.js");
201
+ const { loadCreditPriceMap } = await import("../billing/stripe/credit-prices.js");
202
+ const { StripePaymentProcessor } = await import("../billing/stripe/stripe-payment-processor.js");
203
+
204
+ const customerRepo = new DrizzleTenantCustomerRepository(db as never);
205
+ const priceMap = loadCreditPriceMap();
206
+ const processor = new StripePaymentProcessor({
207
+ stripe: stripeClient,
208
+ tenantRepo: customerRepo,
209
+ webhookSecret: bootConfig.stripeWebhookSecret ?? "",
210
+ priceMap,
211
+ creditLedger,
212
+ });
213
+
214
+ stripe = {
215
+ stripe: stripeClient,
216
+ webhookSecret: bootConfig.stripeWebhookSecret ?? "",
217
+ customerRepo,
218
+ processor,
219
+ };
220
+ }
221
+
222
+ // 11. Gateway services (when enabled)
223
+ let gateway: GatewayServices | null = null;
224
+ if (bootConfig.features.gateway) {
225
+ const { DrizzleServiceKeyRepository } = await import("../gateway/service-key-repository.js");
226
+ const serviceKeyRepo: IServiceKeyRepository = new DrizzleServiceKeyRepository(db as never);
227
+ gateway = { serviceKeyRepo };
228
+ }
229
+
230
+ // 12. Build the container (hotPool bound after construction)
231
+ const result: PlatformContainer = {
232
+ db,
233
+ pool,
234
+ productConfig,
235
+ creditLedger,
236
+ orgMemberRepo,
237
+ orgService,
238
+ userRoleRepo,
239
+ fleet,
240
+ crypto,
241
+ stripe,
242
+ gateway,
243
+ hotPool: null,
244
+ };
245
+
246
+ // Bind hot pool after container construction (closures need the full container)
247
+ if (bootConfig.features.hotPool && fleet) {
248
+ const { startHotPool, setPoolSize: setSize, getPoolSize: getSize } = await import("./services/hot-pool.js");
249
+ const { claimPoolInstance } = await import("./services/hot-pool-claim.js");
250
+ const { DrizzlePoolRepository } = await import("./services/pool-repository.js");
251
+ const poolRepo = new DrizzlePoolRepository(pool);
252
+
253
+ const hotPoolConfig = { provisionSecret: bootConfig.provisionSecret };
254
+ result.hotPool = {
255
+ start: () => startHotPool(result, poolRepo, hotPoolConfig),
256
+ claim: (name, tenantId, adminUser) =>
257
+ claimPoolInstance(result, poolRepo, name, tenantId, adminUser, hotPoolConfig),
258
+ getPoolSize: () => getSize(poolRepo),
259
+ setPoolSize: (size) => setSize(poolRepo, size),
260
+ };
261
+ }
262
+
263
+ return result;
264
+ }