@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
@@ -9,8 +9,8 @@ export type SnapshotTrigger = z.infer<typeof snapshotTriggerSchema>;
9
9
  /** Subscription tiers per WOP-440 */
10
10
  export declare const tierSchema: z.ZodEnum<{
11
11
  free: "free";
12
- starter: "starter";
13
12
  pro: "pro";
13
+ starter: "starter";
14
14
  enterprise: "enterprise";
15
15
  }>;
16
16
  export type Tier = z.infer<typeof tierSchema>;
@@ -0,0 +1,41 @@
1
+ export declare const poolConfig: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "pool_config";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgColumn<{
6
+ name: "id";
7
+ tableName: "pool_config";
8
+ dataType: "number";
9
+ columnType: "PgInteger";
10
+ data: number;
11
+ driverParam: string | number;
12
+ notNull: true;
13
+ hasDefault: true;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: undefined;
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ poolSize: import("drizzle-orm/pg-core").PgColumn<{
23
+ name: "pool_size";
24
+ tableName: "pool_config";
25
+ dataType: "number";
26
+ columnType: "PgInteger";
27
+ data: number;
28
+ driverParam: string | number;
29
+ notNull: true;
30
+ hasDefault: true;
31
+ isPrimaryKey: false;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: undefined;
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ };
40
+ dialect: "pg";
41
+ }>;
@@ -0,0 +1,5 @@
1
+ import { integer, pgTable } from "drizzle-orm/pg-core";
2
+ export const poolConfig = pgTable("pool_config", {
3
+ id: integer("id").primaryKey().default(1),
4
+ poolSize: integer("pool_size").notNull().default(2),
5
+ });
@@ -0,0 +1,126 @@
1
+ export declare const poolInstances: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "pool_instances";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgColumn<{
6
+ name: "id";
7
+ tableName: "pool_instances";
8
+ dataType: "string";
9
+ columnType: "PgText";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: false;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: [string, ...string[]];
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ containerId: import("drizzle-orm/pg-core").PgColumn<{
23
+ name: "container_id";
24
+ tableName: "pool_instances";
25
+ dataType: "string";
26
+ columnType: "PgText";
27
+ data: string;
28
+ driverParam: string;
29
+ notNull: true;
30
+ hasDefault: false;
31
+ isPrimaryKey: false;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: [string, ...string[]];
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ status: import("drizzle-orm/pg-core").PgColumn<{
40
+ name: "status";
41
+ tableName: "pool_instances";
42
+ dataType: "string";
43
+ columnType: "PgText";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: true;
47
+ hasDefault: true;
48
+ isPrimaryKey: false;
49
+ isAutoincrement: false;
50
+ hasRuntimeDefault: false;
51
+ enumValues: [string, ...string[]];
52
+ baseColumn: never;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ }, {}, {}>;
56
+ tenantId: import("drizzle-orm/pg-core").PgColumn<{
57
+ name: "tenant_id";
58
+ tableName: "pool_instances";
59
+ dataType: "string";
60
+ columnType: "PgText";
61
+ data: string;
62
+ driverParam: string;
63
+ notNull: false;
64
+ hasDefault: false;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: [string, ...string[]];
69
+ baseColumn: never;
70
+ identity: undefined;
71
+ generated: undefined;
72
+ }, {}, {}>;
73
+ name: import("drizzle-orm/pg-core").PgColumn<{
74
+ name: "name";
75
+ tableName: "pool_instances";
76
+ dataType: "string";
77
+ columnType: "PgText";
78
+ data: string;
79
+ driverParam: string;
80
+ notNull: false;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: [string, ...string[]];
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "created_at";
92
+ tableName: "pool_instances";
93
+ dataType: "date";
94
+ columnType: "PgTimestamp";
95
+ data: Date;
96
+ driverParam: string;
97
+ notNull: true;
98
+ hasDefault: true;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ claimedAt: import("drizzle-orm/pg-core").PgColumn<{
108
+ name: "claimed_at";
109
+ tableName: "pool_instances";
110
+ dataType: "date";
111
+ columnType: "PgTimestamp";
112
+ data: Date;
113
+ driverParam: string;
114
+ notNull: false;
115
+ hasDefault: false;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: undefined;
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {}>;
124
+ };
125
+ dialect: "pg";
126
+ }>;
@@ -0,0 +1,10 @@
1
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+ export const poolInstances = pgTable("pool_instances", {
3
+ id: text("id").primaryKey(),
4
+ containerId: text("container_id").notNull(),
5
+ status: text("status").notNull().default("warm"),
6
+ tenantId: text("tenant_id"),
7
+ name: text("name"),
8
+ createdAt: timestamp("created_at").notNull().defaultNow(),
9
+ claimedAt: timestamp("claimed_at"),
10
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,339 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ // ---------------------------------------------------------------------------
3
+ // Mocks — all class mocks use real classes (not arrow fns) so `new` works.
4
+ // ---------------------------------------------------------------------------
5
+ // Mock pg.Pool
6
+ const mockPoolEnd = vi.fn().mockResolvedValue(undefined);
7
+ const poolConstructorCalls = [];
8
+ class MockPoolClass {
9
+ connectionString;
10
+ end = mockPoolEnd;
11
+ query = vi.fn().mockResolvedValue({ rows: [] });
12
+ constructor(opts) {
13
+ this.connectionString = opts.connectionString;
14
+ poolConstructorCalls.push(opts);
15
+ }
16
+ }
17
+ vi.mock("pg", () => ({ Pool: MockPoolClass }));
18
+ // Mock drizzle db creation
19
+ const mockDb = { __brand: "drizzle-db" };
20
+ vi.mock("../../db/index.js", () => ({
21
+ createDb: vi.fn(() => mockDb),
22
+ }));
23
+ // Mock drizzle migrator
24
+ const mockMigrate = vi.fn().mockResolvedValue(undefined);
25
+ vi.mock("drizzle-orm/node-postgres/migrator", () => ({
26
+ migrate: mockMigrate,
27
+ }));
28
+ // Mock platformBoot
29
+ const mockProductConfig = {
30
+ product: {
31
+ slug: "test",
32
+ brandName: "Test",
33
+ domain: "test.dev",
34
+ appDomain: "app.test.dev",
35
+ fromEmail: "hi@test.dev",
36
+ emailSupport: "support@test.dev",
37
+ },
38
+ navItems: [],
39
+ domains: [],
40
+ features: null,
41
+ fleet: null,
42
+ billing: null,
43
+ };
44
+ const mockPlatformBoot = vi.fn().mockResolvedValue({
45
+ service: {},
46
+ config: mockProductConfig,
47
+ corsOrigins: ["https://test.dev"],
48
+ seeded: false,
49
+ });
50
+ vi.mock("../../product-config/boot.js", () => ({
51
+ platformBoot: (...args) => mockPlatformBoot(...args),
52
+ }));
53
+ // Mock DrizzleLedger
54
+ const mockSeedSystemAccounts = vi.fn().mockResolvedValue(undefined);
55
+ class MockDrizzleLedgerClass {
56
+ seedSystemAccounts = mockSeedSystemAccounts;
57
+ balance = vi.fn().mockResolvedValue(0);
58
+ credit = vi.fn();
59
+ debit = vi.fn();
60
+ post = vi.fn();
61
+ hasReferenceId = vi.fn().mockResolvedValue(false);
62
+ history = vi.fn().mockResolvedValue([]);
63
+ tenantsWithBalance = vi.fn().mockResolvedValue([]);
64
+ memberUsage = vi.fn().mockResolvedValue([]);
65
+ lifetimeSpend = vi.fn().mockResolvedValue(0);
66
+ lifetimeSpendBatch = vi.fn().mockResolvedValue(new Map());
67
+ expiredCredits = vi.fn().mockResolvedValue([]);
68
+ trialBalance = vi.fn().mockResolvedValue({ balanced: true });
69
+ accountBalance = vi.fn().mockResolvedValue(0);
70
+ existsByReferenceIdLike = vi.fn().mockResolvedValue(false);
71
+ sumPurchasesForPeriod = vi.fn().mockResolvedValue(0);
72
+ getActiveTenantIdsInWindow = vi.fn().mockResolvedValue([]);
73
+ debitCapped = vi.fn().mockResolvedValue(null);
74
+ }
75
+ vi.mock("../../credits/ledger.js", async (importOriginal) => {
76
+ const orig = await importOriginal();
77
+ return { ...orig, DrizzleLedger: MockDrizzleLedgerClass };
78
+ });
79
+ // Mock org/tenancy
80
+ class MockDrizzleOrgMemberRepositoryClass {
81
+ listMembers = vi.fn().mockResolvedValue([]);
82
+ addMember = vi.fn();
83
+ }
84
+ vi.mock("../../tenancy/org-member-repository.js", () => ({
85
+ DrizzleOrgMemberRepository: MockDrizzleOrgMemberRepositoryClass,
86
+ }));
87
+ class MockDrizzleOrgRepositoryClass {
88
+ }
89
+ vi.mock("../../tenancy/drizzle-org-repository.js", () => ({
90
+ DrizzleOrgRepository: MockDrizzleOrgRepositoryClass,
91
+ }));
92
+ const orgServiceConstructorCalls = [];
93
+ class MockOrgServiceClass {
94
+ getOrCreatePersonalOrg = vi.fn();
95
+ constructor(...args) {
96
+ orgServiceConstructorCalls.push(args);
97
+ }
98
+ }
99
+ vi.mock("../../tenancy/org-service.js", () => ({
100
+ OrgService: MockOrgServiceClass,
101
+ }));
102
+ // Mock auth
103
+ class MockBetterAuthUserRepositoryClass {
104
+ }
105
+ vi.mock("../../db/auth-user-repository.js", () => ({
106
+ BetterAuthUserRepository: MockBetterAuthUserRepositoryClass,
107
+ }));
108
+ class MockDrizzleUserRoleRepositoryClass {
109
+ isPlatformAdmin = vi.fn().mockResolvedValue(false);
110
+ listRolesByUser = vi.fn().mockResolvedValue([]);
111
+ getTenantIdByUserId = vi.fn().mockResolvedValue(null);
112
+ grantRole = vi.fn();
113
+ revokeRole = vi.fn().mockResolvedValue(false);
114
+ listUsersByRole = vi.fn().mockResolvedValue([]);
115
+ }
116
+ vi.mock("../../auth/user-role-repository.js", () => ({
117
+ DrizzleUserRoleRepository: MockDrizzleUserRoleRepositoryClass,
118
+ }));
119
+ // Mock fleet deps (only imported when fleet is enabled)
120
+ class MockFleetManagerClass {
121
+ __brand = "fleet-manager";
122
+ }
123
+ vi.mock("../../fleet/fleet-manager.js", () => ({
124
+ FleetManager: MockFleetManagerClass,
125
+ }));
126
+ class MockProfileStoreClass {
127
+ __brand = "profile-store";
128
+ }
129
+ vi.mock("../../fleet/profile-store.js", () => ({
130
+ ProfileStore: MockProfileStoreClass,
131
+ }));
132
+ class MockProxyManagerClass {
133
+ __brand = "proxy-manager";
134
+ }
135
+ vi.mock("../../proxy/manager.js", () => ({
136
+ ProxyManager: MockProxyManagerClass,
137
+ }));
138
+ class MockDrizzleServiceKeyRepositoryClass {
139
+ __brand = "service-key-repo";
140
+ }
141
+ vi.mock("../../gateway/service-key-repository.js", () => ({
142
+ DrizzleServiceKeyRepository: MockDrizzleServiceKeyRepositoryClass,
143
+ }));
144
+ class MockDockerClass {
145
+ __brand = "docker";
146
+ }
147
+ vi.mock("dockerode", () => ({
148
+ default: MockDockerClass,
149
+ }));
150
+ // Mock crypto deps
151
+ class MockDrizzleCryptoChargeRepositoryClass {
152
+ __brand = "charge-repo";
153
+ }
154
+ vi.mock("../../billing/crypto/charge-store.js", () => ({
155
+ DrizzleCryptoChargeRepository: MockDrizzleCryptoChargeRepositoryClass,
156
+ }));
157
+ class MockDrizzleWebhookSeenRepositoryClass {
158
+ __brand = "webhook-seen-repo";
159
+ }
160
+ vi.mock("../../billing/drizzle-webhook-seen-repository.js", () => ({
161
+ DrizzleWebhookSeenRepository: MockDrizzleWebhookSeenRepositoryClass,
162
+ }));
163
+ // Mock stripe deps
164
+ class MockStripeClass {
165
+ __brand = "stripe-client";
166
+ }
167
+ vi.mock("stripe", () => ({
168
+ default: MockStripeClass,
169
+ }));
170
+ class MockDrizzleTenantCustomerRepositoryClass {
171
+ __brand = "tenant-customer-repo";
172
+ }
173
+ vi.mock("../../billing/stripe/tenant-store.js", () => ({
174
+ DrizzleTenantCustomerRepository: MockDrizzleTenantCustomerRepositoryClass,
175
+ }));
176
+ vi.mock("../../billing/stripe/credit-prices.js", () => ({
177
+ loadCreditPriceMap: vi.fn(() => new Map()),
178
+ }));
179
+ class MockStripePaymentProcessorClass {
180
+ __brand = "stripe-processor";
181
+ handleWebhook = vi.fn();
182
+ }
183
+ vi.mock("../../billing/stripe/stripe-payment-processor.js", () => ({
184
+ StripePaymentProcessor: MockStripePaymentProcessorClass,
185
+ }));
186
+ // ---------------------------------------------------------------------------
187
+ // Helpers
188
+ // ---------------------------------------------------------------------------
189
+ function baseBootConfig(overrides) {
190
+ return {
191
+ slug: "test-product",
192
+ databaseUrl: "postgres://localhost:5432/testdb",
193
+ provisionSecret: "test-secret",
194
+ features: {
195
+ fleet: false,
196
+ crypto: false,
197
+ stripe: false,
198
+ gateway: false,
199
+ hotPool: false,
200
+ },
201
+ ...overrides,
202
+ };
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // Tests
206
+ // ---------------------------------------------------------------------------
207
+ describe("buildContainer", () => {
208
+ // Dynamic import so mocks are registered before the module loads
209
+ async function loadBuildContainer() {
210
+ const mod = await import("../container.js");
211
+ return mod.buildContainer;
212
+ }
213
+ it("throws on empty databaseUrl", async () => {
214
+ const buildContainer = await loadBuildContainer();
215
+ const config = baseBootConfig({ databaseUrl: "" });
216
+ await expect(buildContainer(config)).rejects.toThrow("databaseUrl is required");
217
+ });
218
+ it("creates pool with correct connectionString", async () => {
219
+ const buildContainer = await loadBuildContainer();
220
+ const config = baseBootConfig();
221
+ poolConstructorCalls.length = 0;
222
+ const container = await buildContainer(config);
223
+ expect(poolConstructorCalls).toContainEqual({
224
+ connectionString: "postgres://localhost:5432/testdb",
225
+ });
226
+ expect(container.pool).toBeDefined();
227
+ });
228
+ it("calls platformBoot with correct slug", async () => {
229
+ const buildContainer = await loadBuildContainer();
230
+ const config = baseBootConfig({ slug: "paperclip" });
231
+ await buildContainer(config);
232
+ expect(mockPlatformBoot).toHaveBeenCalledWith(expect.objectContaining({ slug: "paperclip" }));
233
+ });
234
+ it("seeds system accounts after creating ledger", async () => {
235
+ const buildContainer = await loadBuildContainer();
236
+ const config = baseBootConfig();
237
+ await buildContainer(config);
238
+ expect(mockSeedSystemAccounts).toHaveBeenCalled();
239
+ });
240
+ it("core services are always present", async () => {
241
+ const buildContainer = await loadBuildContainer();
242
+ const config = baseBootConfig();
243
+ const container = await buildContainer(config);
244
+ expect(container.db).toBeDefined();
245
+ expect(container.pool).toBeDefined();
246
+ expect(container.productConfig).toBeDefined();
247
+ expect(container.creditLedger).toBeDefined();
248
+ expect(container.orgMemberRepo).toBeDefined();
249
+ expect(container.orgService).toBeDefined();
250
+ expect(container.userRoleRepo).toBeDefined();
251
+ });
252
+ it("returns null for all disabled features", async () => {
253
+ const buildContainer = await loadBuildContainer();
254
+ const config = baseBootConfig();
255
+ const container = await buildContainer(config);
256
+ expect(container.fleet).toBeNull();
257
+ expect(container.crypto).toBeNull();
258
+ expect(container.stripe).toBeNull();
259
+ expect(container.gateway).toBeNull();
260
+ expect(container.hotPool).toBeNull();
261
+ });
262
+ it("builds fleet services when feature is enabled", async () => {
263
+ const buildContainer = await loadBuildContainer();
264
+ const config = baseBootConfig({
265
+ features: { fleet: true, crypto: false, stripe: false, gateway: false, hotPool: false },
266
+ });
267
+ const container = await buildContainer(config);
268
+ expect(container.fleet).not.toBeNull();
269
+ expect(container.fleet?.manager).toBeDefined();
270
+ expect(container.fleet?.docker).toBeDefined();
271
+ expect(container.fleet?.proxy).toBeDefined();
272
+ expect(container.fleet?.profileStore).toBeDefined();
273
+ expect(container.fleet?.serviceKeyRepo).toBeDefined();
274
+ });
275
+ it("builds crypto services when feature is enabled", async () => {
276
+ const buildContainer = await loadBuildContainer();
277
+ const config = baseBootConfig({
278
+ features: { fleet: false, crypto: true, stripe: false, gateway: false, hotPool: false },
279
+ });
280
+ const container = await buildContainer(config);
281
+ expect(container.crypto).not.toBeNull();
282
+ expect(container.crypto?.chargeRepo).toBeDefined();
283
+ expect(container.crypto?.webhookSeenRepo).toBeDefined();
284
+ });
285
+ it("builds stripe services when feature is enabled and key provided", async () => {
286
+ const buildContainer = await loadBuildContainer();
287
+ const config = baseBootConfig({
288
+ features: { fleet: false, crypto: false, stripe: true, gateway: false, hotPool: false },
289
+ stripeSecretKey: "sk_test_123",
290
+ stripeWebhookSecret: "whsec_test_456",
291
+ });
292
+ const container = await buildContainer(config);
293
+ expect(container.stripe).not.toBeNull();
294
+ expect(container.stripe?.stripe).toBeDefined();
295
+ expect(container.stripe?.webhookSecret).toBe("whsec_test_456");
296
+ expect(container.stripe?.customerRepo).toBeDefined();
297
+ expect(container.stripe?.processor).toBeDefined();
298
+ });
299
+ it("returns null stripe when feature enabled but no secret key", async () => {
300
+ const buildContainer = await loadBuildContainer();
301
+ const config = baseBootConfig({
302
+ features: { fleet: false, crypto: false, stripe: true, gateway: false, hotPool: false },
303
+ // stripeSecretKey intentionally omitted
304
+ });
305
+ const container = await buildContainer(config);
306
+ expect(container.stripe).toBeNull();
307
+ });
308
+ it("builds gateway services when feature is enabled", async () => {
309
+ const buildContainer = await loadBuildContainer();
310
+ const config = baseBootConfig({
311
+ features: { fleet: false, crypto: false, stripe: false, gateway: true, hotPool: false },
312
+ });
313
+ const container = await buildContainer(config);
314
+ expect(container.gateway).not.toBeNull();
315
+ expect(container.gateway?.serviceKeyRepo).toBeDefined();
316
+ });
317
+ it("runs migrations before building services", async () => {
318
+ const buildContainer = await loadBuildContainer();
319
+ const config = baseBootConfig();
320
+ await buildContainer(config);
321
+ expect(mockMigrate).toHaveBeenCalled();
322
+ // Migration should be called before platformBoot
323
+ const migrateOrder = mockMigrate.mock.invocationCallOrder[0];
324
+ const bootOrder = mockPlatformBoot.mock.invocationCallOrder[0];
325
+ expect(migrateOrder).toBeLessThan(bootOrder);
326
+ });
327
+ it("constructs OrgService with orgRepo, memberRepo, db, and authUserRepo", async () => {
328
+ const buildContainer = await loadBuildContainer();
329
+ const config = baseBootConfig();
330
+ orgServiceConstructorCalls.length = 0;
331
+ await buildContainer(config);
332
+ expect(orgServiceConstructorCalls.length).toBeGreaterThan(0);
333
+ const [orgRepo, memberRepo, db, options] = orgServiceConstructorCalls[0];
334
+ expect(orgRepo).toBeInstanceOf(MockDrizzleOrgRepositoryClass);
335
+ expect(memberRepo).toBeInstanceOf(MockDrizzleOrgMemberRepositoryClass);
336
+ expect(db).toBe(mockDb);
337
+ expect(options).toEqual(expect.objectContaining({ userRepo: expect.any(MockBetterAuthUserRepositoryClass) }));
338
+ });
339
+ });
@@ -0,0 +1 @@
1
+ export {};