@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.
- package/dist/backup/types.d.ts +1 -1
- package/dist/db/schema/pool-config.d.ts +41 -0
- package/dist/db/schema/pool-config.js +5 -0
- package/dist/db/schema/pool-instances.d.ts +126 -0
- package/dist/db/schema/pool-instances.js +10 -0
- package/dist/server/__tests__/build-container.test.d.ts +1 -0
- package/dist/server/__tests__/build-container.test.js +339 -0
- package/dist/server/__tests__/container.test.d.ts +1 -0
- package/dist/server/__tests__/container.test.js +173 -0
- package/dist/server/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/server/__tests__/lifecycle.test.js +90 -0
- package/dist/server/__tests__/mount-routes.test.d.ts +1 -0
- package/dist/server/__tests__/mount-routes.test.js +151 -0
- package/dist/server/boot-config.d.ts +51 -0
- package/dist/server/boot-config.js +7 -0
- package/dist/server/container.d.ts +97 -0
- package/dist/server/container.js +148 -0
- package/dist/server/index.d.ts +33 -0
- package/dist/server/index.js +66 -0
- package/dist/server/lifecycle.d.ts +25 -0
- package/dist/server/lifecycle.js +56 -0
- package/dist/server/middleware/__tests__/admin-auth.test.d.ts +1 -0
- package/dist/server/middleware/__tests__/admin-auth.test.js +59 -0
- package/dist/server/middleware/__tests__/tenant-proxy.test.d.ts +1 -0
- package/dist/server/middleware/__tests__/tenant-proxy.test.js +268 -0
- package/dist/server/middleware/admin-auth.d.ts +18 -0
- package/dist/server/middleware/admin-auth.js +38 -0
- package/dist/server/middleware/tenant-proxy.d.ts +56 -0
- package/dist/server/middleware/tenant-proxy.js +162 -0
- package/dist/server/mount-routes.d.ts +30 -0
- package/dist/server/mount-routes.js +74 -0
- package/dist/server/routes/__tests__/admin.test.d.ts +1 -0
- package/dist/server/routes/__tests__/admin.test.js +267 -0
- package/dist/server/routes/__tests__/crypto-webhook.test.d.ts +1 -0
- package/dist/server/routes/__tests__/crypto-webhook.test.js +137 -0
- package/dist/server/routes/__tests__/provision-webhook.test.d.ts +1 -0
- package/dist/server/routes/__tests__/provision-webhook.test.js +212 -0
- package/dist/server/routes/__tests__/stripe-webhook.test.d.ts +1 -0
- package/dist/server/routes/__tests__/stripe-webhook.test.js +65 -0
- package/dist/server/routes/admin.d.ts +129 -0
- package/dist/server/routes/admin.js +294 -0
- package/dist/server/routes/crypto-webhook.d.ts +23 -0
- package/dist/server/routes/crypto-webhook.js +82 -0
- package/dist/server/routes/provision-webhook.d.ts +38 -0
- package/dist/server/routes/provision-webhook.js +160 -0
- package/dist/server/routes/stripe-webhook.d.ts +10 -0
- package/dist/server/routes/stripe-webhook.js +29 -0
- package/dist/server/services/hot-pool-claim.d.ts +30 -0
- package/dist/server/services/hot-pool-claim.js +92 -0
- package/dist/server/services/hot-pool.d.ts +25 -0
- package/dist/server/services/hot-pool.js +129 -0
- package/dist/server/services/pool-repository.d.ts +44 -0
- package/dist/server/services/pool-repository.js +72 -0
- package/dist/server/test-container.d.ts +15 -0
- package/dist/server/test-container.js +103 -0
- package/dist/trpc/auth-helpers.d.ts +17 -0
- package/dist/trpc/auth-helpers.js +26 -0
- package/dist/trpc/container-factories.d.ts +300 -0
- package/dist/trpc/container-factories.js +80 -0
- package/dist/trpc/index.d.ts +2 -0
- package/dist/trpc/index.js +2 -0
- package/drizzle/migrations/0025_hot_pool_tables.sql +29 -0
- package/package.json +5 -1
- package/src/db/schema/pool-config.ts +6 -0
- package/src/db/schema/pool-instances.ts +11 -0
- package/src/server/__tests__/build-container.test.ts +402 -0
- package/src/server/__tests__/container.test.ts +207 -0
- package/src/server/__tests__/lifecycle.test.ts +106 -0
- package/src/server/__tests__/mount-routes.test.ts +169 -0
- package/src/server/boot-config.ts +84 -0
- package/src/server/container.ts +264 -0
- package/src/server/index.ts +92 -0
- package/src/server/lifecycle.ts +72 -0
- package/src/server/middleware/__tests__/admin-auth.test.ts +67 -0
- package/src/server/middleware/__tests__/tenant-proxy.test.ts +308 -0
- package/src/server/middleware/admin-auth.ts +51 -0
- package/src/server/middleware/tenant-proxy.ts +192 -0
- package/src/server/mount-routes.ts +113 -0
- package/src/server/routes/__tests__/admin.test.ts +320 -0
- package/src/server/routes/__tests__/crypto-webhook.test.ts +167 -0
- package/src/server/routes/__tests__/provision-webhook.test.ts +323 -0
- package/src/server/routes/__tests__/stripe-webhook.test.ts +73 -0
- package/src/server/routes/admin.ts +360 -0
- package/src/server/routes/crypto-webhook.ts +110 -0
- package/src/server/routes/provision-webhook.ts +212 -0
- package/src/server/routes/stripe-webhook.ts +36 -0
- package/src/server/services/hot-pool-claim.ts +130 -0
- package/src/server/services/hot-pool.ts +174 -0
- package/src/server/services/pool-repository.ts +107 -0
- package/src/server/test-container.ts +120 -0
- package/src/trpc/auth-helpers.ts +28 -0
- package/src/trpc/container-factories.ts +114 -0
- package/src/trpc/index.ts +9 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestContainer } from "../test-container.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// PlatformContainer interface
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
describe("PlatformContainer", () => {
|
|
7
|
+
it("allows null feature services", () => {
|
|
8
|
+
const c = createTestContainer();
|
|
9
|
+
expect(c.fleet).toBeNull();
|
|
10
|
+
expect(c.crypto).toBeNull();
|
|
11
|
+
expect(c.stripe).toBeNull();
|
|
12
|
+
expect(c.gateway).toBeNull();
|
|
13
|
+
expect(c.hotPool).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
it("requires core services to be present", () => {
|
|
16
|
+
const c = createTestContainer();
|
|
17
|
+
expect(c.db).toBeDefined();
|
|
18
|
+
expect(c.pool).toBeDefined();
|
|
19
|
+
expect(c.productConfig).toBeDefined();
|
|
20
|
+
expect(c.creditLedger).toBeDefined();
|
|
21
|
+
expect(c.orgMemberRepo).toBeDefined();
|
|
22
|
+
expect(c.orgService).toBeDefined();
|
|
23
|
+
expect(c.userRoleRepo).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// BootConfig shape
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
describe("BootConfig", () => {
|
|
30
|
+
it("requires slug and provisionSecret", () => {
|
|
31
|
+
const config = {
|
|
32
|
+
slug: "test-product",
|
|
33
|
+
databaseUrl: "postgres://localhost/test",
|
|
34
|
+
provisionSecret: "s3cret",
|
|
35
|
+
features: {
|
|
36
|
+
fleet: false,
|
|
37
|
+
crypto: false,
|
|
38
|
+
stripe: false,
|
|
39
|
+
gateway: false,
|
|
40
|
+
hotPool: false,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
expect(config.slug).toBe("test-product");
|
|
44
|
+
expect(config.provisionSecret).toBe("s3cret");
|
|
45
|
+
});
|
|
46
|
+
it("accepts optional fields", () => {
|
|
47
|
+
const config = {
|
|
48
|
+
slug: "full",
|
|
49
|
+
databaseUrl: "postgres://localhost/full",
|
|
50
|
+
provisionSecret: "secret",
|
|
51
|
+
host: "127.0.0.1",
|
|
52
|
+
port: 4000,
|
|
53
|
+
features: {
|
|
54
|
+
fleet: true,
|
|
55
|
+
crypto: true,
|
|
56
|
+
stripe: true,
|
|
57
|
+
gateway: true,
|
|
58
|
+
hotPool: true,
|
|
59
|
+
},
|
|
60
|
+
stripeSecretKey: "sk_test_xxx",
|
|
61
|
+
stripeWebhookSecret: "whsec_xxx",
|
|
62
|
+
cryptoServiceKey: "csk_xxx",
|
|
63
|
+
routes: [],
|
|
64
|
+
};
|
|
65
|
+
expect(config.host).toBe("127.0.0.1");
|
|
66
|
+
expect(config.port).toBe(4000);
|
|
67
|
+
expect(config.features.fleet).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it("FeatureFlags has all five toggles", () => {
|
|
70
|
+
const flags = {
|
|
71
|
+
fleet: true,
|
|
72
|
+
crypto: false,
|
|
73
|
+
stripe: true,
|
|
74
|
+
gateway: false,
|
|
75
|
+
hotPool: true,
|
|
76
|
+
};
|
|
77
|
+
expect(Object.keys(flags)).toHaveLength(5);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// createTestContainer
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
describe("createTestContainer", () => {
|
|
84
|
+
it("returns valid defaults", () => {
|
|
85
|
+
const c = createTestContainer();
|
|
86
|
+
expect(c.fleet).toBeNull();
|
|
87
|
+
expect(c.crypto).toBeNull();
|
|
88
|
+
expect(c.stripe).toBeNull();
|
|
89
|
+
expect(c.gateway).toBeNull();
|
|
90
|
+
expect(c.hotPool).toBeNull();
|
|
91
|
+
expect(c.productConfig.product).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
it("allows overrides for core services", () => {
|
|
94
|
+
const customConfig = {
|
|
95
|
+
product: { slug: "custom", name: "Custom" },
|
|
96
|
+
navItems: [],
|
|
97
|
+
domains: [],
|
|
98
|
+
features: null,
|
|
99
|
+
fleet: null,
|
|
100
|
+
billing: null,
|
|
101
|
+
};
|
|
102
|
+
const c = createTestContainer({ productConfig: customConfig });
|
|
103
|
+
expect(c.productConfig.product).toEqual({ slug: "custom", name: "Custom" });
|
|
104
|
+
});
|
|
105
|
+
it("stub ledger methods return sensible defaults", async () => {
|
|
106
|
+
const c = createTestContainer();
|
|
107
|
+
expect(await c.creditLedger.balance("t1")).toBe(0);
|
|
108
|
+
expect(await c.creditLedger.hasReferenceId("ref")).toBe(false);
|
|
109
|
+
expect(await c.creditLedger.history("t1")).toEqual([]);
|
|
110
|
+
expect(await c.creditLedger.tenantsWithBalance()).toEqual([]);
|
|
111
|
+
expect(await c.creditLedger.lifetimeSpendBatch([])).toEqual(new Map());
|
|
112
|
+
});
|
|
113
|
+
it("stub orgMemberRepo methods return sensible defaults", async () => {
|
|
114
|
+
const c = createTestContainer();
|
|
115
|
+
expect(await c.orgMemberRepo.findMember("org1", "u1")).toBeNull();
|
|
116
|
+
expect(await c.orgMemberRepo.listMembers("org1")).toEqual([]);
|
|
117
|
+
expect(await c.orgMemberRepo.countAdminsAndOwners("org1")).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
it("stub userRoleRepo methods return sensible defaults", async () => {
|
|
120
|
+
const c = createTestContainer();
|
|
121
|
+
expect(await c.userRoleRepo.getTenantIdByUserId("u1")).toBeNull();
|
|
122
|
+
expect(await c.userRoleRepo.isPlatformAdmin("u1")).toBe(false);
|
|
123
|
+
expect(await c.userRoleRepo.listRolesByUser("u1")).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
it("allows enabling feature services via overrides", () => {
|
|
126
|
+
const fleet = {
|
|
127
|
+
manager: {},
|
|
128
|
+
docker: {},
|
|
129
|
+
proxy: {},
|
|
130
|
+
profileStore: {},
|
|
131
|
+
serviceKeyRepo: {},
|
|
132
|
+
};
|
|
133
|
+
const crypto = {
|
|
134
|
+
chargeRepo: {},
|
|
135
|
+
webhookSeenRepo: {},
|
|
136
|
+
};
|
|
137
|
+
const stripe = {
|
|
138
|
+
stripe: {},
|
|
139
|
+
webhookSecret: "whsec_test",
|
|
140
|
+
customerRepo: {},
|
|
141
|
+
processor: {},
|
|
142
|
+
};
|
|
143
|
+
const gateway = {
|
|
144
|
+
serviceKeyRepo: {},
|
|
145
|
+
};
|
|
146
|
+
const hotPool = {
|
|
147
|
+
start: async () => ({ stop: () => { } }),
|
|
148
|
+
claim: async () => null,
|
|
149
|
+
getPoolSize: async () => 2,
|
|
150
|
+
setPoolSize: async () => { },
|
|
151
|
+
};
|
|
152
|
+
const c = createTestContainer({ fleet, crypto, stripe, gateway, hotPool });
|
|
153
|
+
expect(c.fleet).not.toBeNull();
|
|
154
|
+
expect(c.crypto).not.toBeNull();
|
|
155
|
+
expect(c.stripe).not.toBeNull();
|
|
156
|
+
expect(c.stripe?.webhookSecret).toBe("whsec_test");
|
|
157
|
+
expect(c.gateway).not.toBeNull();
|
|
158
|
+
expect(c.hotPool).not.toBeNull();
|
|
159
|
+
});
|
|
160
|
+
it("overrides merge without affecting other defaults", () => {
|
|
161
|
+
const c = createTestContainer({ gateway: { serviceKeyRepo: {} } });
|
|
162
|
+
// Overridden field
|
|
163
|
+
expect(c.gateway).not.toBeNull();
|
|
164
|
+
// Other feature services remain null
|
|
165
|
+
expect(c.fleet).toBeNull();
|
|
166
|
+
expect(c.crypto).toBeNull();
|
|
167
|
+
expect(c.stripe).toBeNull();
|
|
168
|
+
expect(c.hotPool).toBeNull();
|
|
169
|
+
// Core services still present
|
|
170
|
+
expect(c.creditLedger).toBeDefined();
|
|
171
|
+
expect(c.orgMemberRepo).toBeDefined();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { gracefulShutdown, startBackgroundServices } from "../lifecycle.js";
|
|
3
|
+
import { createTestContainer } from "../test-container.js";
|
|
4
|
+
describe("startBackgroundServices", () => {
|
|
5
|
+
it("returns a BackgroundHandles object", async () => {
|
|
6
|
+
const container = createTestContainer();
|
|
7
|
+
const handles = await startBackgroundServices(container);
|
|
8
|
+
expect(handles).toHaveProperty("intervals");
|
|
9
|
+
expect(handles).toHaveProperty("unsubscribes");
|
|
10
|
+
expect(Array.isArray(handles.intervals)).toBe(true);
|
|
11
|
+
expect(Array.isArray(handles.unsubscribes)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("calls proxy.start when fleet is enabled", async () => {
|
|
14
|
+
const startFn = vi.fn().mockResolvedValue(undefined);
|
|
15
|
+
const container = createTestContainer({
|
|
16
|
+
fleet: {
|
|
17
|
+
manager: {},
|
|
18
|
+
docker: {},
|
|
19
|
+
proxy: {
|
|
20
|
+
start: startFn,
|
|
21
|
+
addRoute: async () => { },
|
|
22
|
+
removeRoute: () => { },
|
|
23
|
+
getRoutes: () => [],
|
|
24
|
+
},
|
|
25
|
+
profileStore: { list: async () => [] },
|
|
26
|
+
serviceKeyRepo: {},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
await startBackgroundServices(container);
|
|
30
|
+
expect(startFn).toHaveBeenCalledOnce();
|
|
31
|
+
});
|
|
32
|
+
it("does not throw when proxy.start fails", async () => {
|
|
33
|
+
const container = createTestContainer({
|
|
34
|
+
fleet: {
|
|
35
|
+
manager: {},
|
|
36
|
+
docker: {},
|
|
37
|
+
proxy: {
|
|
38
|
+
start: async () => {
|
|
39
|
+
throw new Error("proxy start failed");
|
|
40
|
+
},
|
|
41
|
+
addRoute: async () => { },
|
|
42
|
+
removeRoute: () => { },
|
|
43
|
+
getRoutes: () => [],
|
|
44
|
+
},
|
|
45
|
+
profileStore: { list: async () => [] },
|
|
46
|
+
serviceKeyRepo: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Should not throw
|
|
50
|
+
const handles = await startBackgroundServices(container);
|
|
51
|
+
expect(handles).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("gracefulShutdown", () => {
|
|
55
|
+
it("clears all intervals", async () => {
|
|
56
|
+
const container = createTestContainer();
|
|
57
|
+
const clearSpy = vi.spyOn(global, "clearInterval");
|
|
58
|
+
const interval1 = setInterval(() => { }, 10000);
|
|
59
|
+
const interval2 = setInterval(() => { }, 10000);
|
|
60
|
+
const handles = {
|
|
61
|
+
intervals: [interval1, interval2],
|
|
62
|
+
unsubscribes: [],
|
|
63
|
+
};
|
|
64
|
+
await gracefulShutdown(container, handles);
|
|
65
|
+
expect(clearSpy).toHaveBeenCalledWith(interval1);
|
|
66
|
+
expect(clearSpy).toHaveBeenCalledWith(interval2);
|
|
67
|
+
clearSpy.mockRestore();
|
|
68
|
+
});
|
|
69
|
+
it("calls all unsubscribe functions", async () => {
|
|
70
|
+
const container = createTestContainer();
|
|
71
|
+
const unsub1 = vi.fn();
|
|
72
|
+
const unsub2 = vi.fn();
|
|
73
|
+
const handles = {
|
|
74
|
+
intervals: [],
|
|
75
|
+
unsubscribes: [unsub1, unsub2],
|
|
76
|
+
};
|
|
77
|
+
await gracefulShutdown(container, handles);
|
|
78
|
+
expect(unsub1).toHaveBeenCalledOnce();
|
|
79
|
+
expect(unsub2).toHaveBeenCalledOnce();
|
|
80
|
+
});
|
|
81
|
+
it("calls pool.end()", async () => {
|
|
82
|
+
const endFn = vi.fn().mockResolvedValue(undefined);
|
|
83
|
+
const container = createTestContainer({
|
|
84
|
+
pool: { end: endFn },
|
|
85
|
+
});
|
|
86
|
+
const handles = { intervals: [], unsubscribes: [] };
|
|
87
|
+
await gracefulShutdown(container, handles);
|
|
88
|
+
expect(endFn).toHaveBeenCalledOnce();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { mountRoutes } from "../mount-routes.js";
|
|
4
|
+
import { createTestContainer } from "../test-container.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function defaultMountConfig() {
|
|
9
|
+
return {
|
|
10
|
+
provisionSecret: "test-secret",
|
|
11
|
+
cryptoServiceKey: "test-crypto-key",
|
|
12
|
+
platformDomain: "example.com",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function makeApp(container) {
|
|
16
|
+
const app = new Hono();
|
|
17
|
+
mountRoutes(app, container, defaultMountConfig());
|
|
18
|
+
return app;
|
|
19
|
+
}
|
|
20
|
+
async function req(app, method, path, opts) {
|
|
21
|
+
const request = new Request(`http://localhost${path}`, { method, ...opts });
|
|
22
|
+
return app.request(request);
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Minimal stubs for feature sub-containers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function stubCrypto() {
|
|
28
|
+
return {
|
|
29
|
+
chargeRepo: {},
|
|
30
|
+
webhookSeenRepo: {},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function stubStripe() {
|
|
34
|
+
return {
|
|
35
|
+
stripe: {},
|
|
36
|
+
webhookSecret: "whsec_test",
|
|
37
|
+
customerRepo: {},
|
|
38
|
+
processor: {
|
|
39
|
+
handleWebhook: async () => ({ ok: true }),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function stubFleet() {
|
|
44
|
+
return {
|
|
45
|
+
manager: {},
|
|
46
|
+
docker: {},
|
|
47
|
+
proxy: {
|
|
48
|
+
start: async () => { },
|
|
49
|
+
addRoute: async () => { },
|
|
50
|
+
removeRoute: () => { },
|
|
51
|
+
getRoutes: () => [],
|
|
52
|
+
},
|
|
53
|
+
profileStore: { list: async () => [] },
|
|
54
|
+
serviceKeyRepo: {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Tests
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
describe("mountRoutes", () => {
|
|
61
|
+
// 1. Health endpoint always available
|
|
62
|
+
it("mounts /health endpoint", async () => {
|
|
63
|
+
const app = makeApp(createTestContainer());
|
|
64
|
+
const res = await req(app, "GET", "/health");
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
const body = await res.json();
|
|
67
|
+
expect(body).toEqual({ ok: true });
|
|
68
|
+
});
|
|
69
|
+
// 2. Crypto webhook mounted when crypto enabled
|
|
70
|
+
it("mounts crypto webhook when crypto enabled", async () => {
|
|
71
|
+
const container = createTestContainer({ crypto: stubCrypto() });
|
|
72
|
+
const app = makeApp(container);
|
|
73
|
+
const res = await req(app, "POST", "/api/webhooks/crypto", {
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({}),
|
|
76
|
+
});
|
|
77
|
+
// Should return 401 (no auth), NOT 404 (not found)
|
|
78
|
+
expect(res.status).toBe(401);
|
|
79
|
+
});
|
|
80
|
+
// 3. Crypto webhook NOT mounted when crypto disabled
|
|
81
|
+
it("does not mount crypto webhook when crypto disabled", async () => {
|
|
82
|
+
const container = createTestContainer({ crypto: null });
|
|
83
|
+
const app = makeApp(container);
|
|
84
|
+
const res = await req(app, "POST", "/api/webhooks/crypto", {
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({}),
|
|
87
|
+
});
|
|
88
|
+
expect(res.status).toBe(404);
|
|
89
|
+
});
|
|
90
|
+
// 4. Stripe webhook mounted when stripe enabled
|
|
91
|
+
it("mounts stripe webhook when stripe enabled", async () => {
|
|
92
|
+
const container = createTestContainer({ stripe: stubStripe() });
|
|
93
|
+
const app = makeApp(container);
|
|
94
|
+
const res = await req(app, "POST", "/api/webhooks/stripe", {
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({}),
|
|
97
|
+
});
|
|
98
|
+
// Should return 400 (missing stripe-signature), NOT 404
|
|
99
|
+
expect(res.status).toBe(400);
|
|
100
|
+
});
|
|
101
|
+
// 5. Stripe webhook NOT mounted when stripe disabled
|
|
102
|
+
it("does not mount stripe webhook when stripe disabled", async () => {
|
|
103
|
+
const container = createTestContainer({ stripe: null });
|
|
104
|
+
const app = makeApp(container);
|
|
105
|
+
const res = await req(app, "POST", "/api/webhooks/stripe", {
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
body: JSON.stringify({}),
|
|
108
|
+
});
|
|
109
|
+
expect(res.status).toBe(404);
|
|
110
|
+
});
|
|
111
|
+
// 6. Provision webhook mounted when fleet enabled
|
|
112
|
+
it("mounts provision webhook when fleet enabled", async () => {
|
|
113
|
+
const container = createTestContainer({ fleet: stubFleet() });
|
|
114
|
+
const app = makeApp(container);
|
|
115
|
+
const res = await req(app, "POST", "/api/provision/create", {
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify({}),
|
|
118
|
+
});
|
|
119
|
+
// Should return 401 (no auth), NOT 404
|
|
120
|
+
expect(res.status).toBe(401);
|
|
121
|
+
});
|
|
122
|
+
// 7. Provision webhook NOT mounted when fleet disabled
|
|
123
|
+
it("does not mount provision webhook when fleet disabled", async () => {
|
|
124
|
+
const container = createTestContainer({ fleet: null });
|
|
125
|
+
const app = makeApp(container);
|
|
126
|
+
const res = await req(app, "POST", "/api/provision/create", {
|
|
127
|
+
headers: { "Content-Type": "application/json" },
|
|
128
|
+
body: JSON.stringify({}),
|
|
129
|
+
});
|
|
130
|
+
expect(res.status).toBe(404);
|
|
131
|
+
});
|
|
132
|
+
// 8. Product-specific route plugins
|
|
133
|
+
it("mounts product-specific route plugins", async () => {
|
|
134
|
+
const container = createTestContainer();
|
|
135
|
+
const app = new Hono();
|
|
136
|
+
mountRoutes(app, container, defaultMountConfig(), [
|
|
137
|
+
{
|
|
138
|
+
path: "/api/custom",
|
|
139
|
+
handler: () => {
|
|
140
|
+
const sub = new Hono();
|
|
141
|
+
sub.get("/ping", (c) => c.json({ pong: true }));
|
|
142
|
+
return sub;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
const res = await req(app, "GET", "/api/custom/ping");
|
|
147
|
+
expect(res.status).toBe(200);
|
|
148
|
+
const body = await res.json();
|
|
149
|
+
expect(body).toEqual({ pong: true });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import type { Hono } from "hono";
|
|
8
|
+
import type { PlatformContainer } from "./container.js";
|
|
9
|
+
export interface FeatureFlags {
|
|
10
|
+
fleet: boolean;
|
|
11
|
+
crypto: boolean;
|
|
12
|
+
stripe: boolean;
|
|
13
|
+
gateway: boolean;
|
|
14
|
+
hotPool: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface RoutePlugin {
|
|
17
|
+
path: string;
|
|
18
|
+
handler: (container: PlatformContainer) => Hono;
|
|
19
|
+
}
|
|
20
|
+
export interface BootConfig {
|
|
21
|
+
/** Short product identifier (e.g. "paperclip", "wopr", "holyship"). */
|
|
22
|
+
slug: string;
|
|
23
|
+
/** PostgreSQL connection string. */
|
|
24
|
+
databaseUrl: string;
|
|
25
|
+
/** Bind host (default "0.0.0.0"). */
|
|
26
|
+
host?: string;
|
|
27
|
+
/** Bind port (default 3001). */
|
|
28
|
+
port?: number;
|
|
29
|
+
/** Which optional feature slices to wire up. */
|
|
30
|
+
features: FeatureFlags;
|
|
31
|
+
/** Additional Hono sub-apps mounted after core routes. */
|
|
32
|
+
routes?: RoutePlugin[];
|
|
33
|
+
/** Required when features.stripe is true. */
|
|
34
|
+
stripeSecretKey?: string;
|
|
35
|
+
/** Required when features.stripe is true. */
|
|
36
|
+
stripeWebhookSecret?: string;
|
|
37
|
+
/** Service key for the crypto chain server webhook endpoint. */
|
|
38
|
+
cryptoServiceKey?: string;
|
|
39
|
+
/** Shared secret used to authenticate provision requests. */
|
|
40
|
+
provisionSecret: string;
|
|
41
|
+
}
|
|
42
|
+
export interface BootResult {
|
|
43
|
+
/** The fully-wired Hono application. */
|
|
44
|
+
app: Hono;
|
|
45
|
+
/** The assembled DI container — useful for tests and ad-hoc access. */
|
|
46
|
+
container: PlatformContainer;
|
|
47
|
+
/** Start listening. Uses BootConfig.port unless overridden. */
|
|
48
|
+
start: (port?: number) => Promise<void>;
|
|
49
|
+
/** Graceful shutdown: drain connections, close pool. */
|
|
50
|
+
stop: () => Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
import type Docker from "dockerode";
|
|
9
|
+
import type { Pool } from "pg";
|
|
10
|
+
import type Stripe from "stripe";
|
|
11
|
+
import type { IUserRoleRepository } from "../auth/user-role-repository.js";
|
|
12
|
+
import type { ICryptoChargeRepository } from "../billing/crypto/charge-store.js";
|
|
13
|
+
import type { IWebhookSeenRepository } from "../billing/webhook-seen-repository.js";
|
|
14
|
+
import type { ILedger } from "../credits/ledger.js";
|
|
15
|
+
import type { ITenantCustomerRepository } from "../credits/tenant-customer-repository.js";
|
|
16
|
+
import type { DrizzleDb } from "../db/index.js";
|
|
17
|
+
import type { FleetManager } from "../fleet/fleet-manager.js";
|
|
18
|
+
import type { IProfileStore } from "../fleet/profile-store.js";
|
|
19
|
+
import type { IServiceKeyRepository } from "../gateway/service-key-repository.js";
|
|
20
|
+
import type { ProductConfig } from "../product-config/repository-types.js";
|
|
21
|
+
import type { ProxyManagerInterface } from "../proxy/types.js";
|
|
22
|
+
import type { IOrgMemberRepository } from "../tenancy/org-member-repository.js";
|
|
23
|
+
import type { OrgService } from "../tenancy/org-service.js";
|
|
24
|
+
import type { BootConfig } from "./boot-config.js";
|
|
25
|
+
export interface FleetServices {
|
|
26
|
+
manager: FleetManager;
|
|
27
|
+
docker: Docker;
|
|
28
|
+
proxy: ProxyManagerInterface;
|
|
29
|
+
profileStore: IProfileStore;
|
|
30
|
+
serviceKeyRepo: IServiceKeyRepository;
|
|
31
|
+
}
|
|
32
|
+
export interface CryptoServices {
|
|
33
|
+
chargeRepo: ICryptoChargeRepository;
|
|
34
|
+
webhookSeenRepo: IWebhookSeenRepository;
|
|
35
|
+
}
|
|
36
|
+
export interface StripeServices {
|
|
37
|
+
stripe: Stripe;
|
|
38
|
+
webhookSecret: string;
|
|
39
|
+
customerRepo: ITenantCustomerRepository;
|
|
40
|
+
processor: {
|
|
41
|
+
handleWebhook(payload: Buffer, signature: string): Promise<unknown>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface GatewayServices {
|
|
45
|
+
serviceKeyRepo: IServiceKeyRepository;
|
|
46
|
+
}
|
|
47
|
+
export interface HotPoolServices {
|
|
48
|
+
/** Start the pool manager (replenish loop + cleanup). */
|
|
49
|
+
start: () => Promise<{
|
|
50
|
+
stop: () => void;
|
|
51
|
+
}>;
|
|
52
|
+
/** Claim a warm instance from the pool. Returns null if empty. */
|
|
53
|
+
claim: (name: string, tenantId: string, adminUser: {
|
|
54
|
+
id: string;
|
|
55
|
+
email: string;
|
|
56
|
+
name: string;
|
|
57
|
+
}) => Promise<{
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
subdomain: string;
|
|
61
|
+
} | null>;
|
|
62
|
+
/** Get current pool size from DB. */
|
|
63
|
+
getPoolSize: () => Promise<number>;
|
|
64
|
+
/** Set pool size in DB. */
|
|
65
|
+
setPoolSize: (size: number) => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface PlatformContainer {
|
|
68
|
+
db: DrizzleDb;
|
|
69
|
+
pool: Pool;
|
|
70
|
+
productConfig: ProductConfig;
|
|
71
|
+
creditLedger: ILedger;
|
|
72
|
+
orgMemberRepo: IOrgMemberRepository;
|
|
73
|
+
orgService: OrgService;
|
|
74
|
+
userRoleRepo: IUserRoleRepository;
|
|
75
|
+
/** Null when the product does not use fleet management. */
|
|
76
|
+
fleet: FleetServices | null;
|
|
77
|
+
/** Null when the product does not accept crypto payments. */
|
|
78
|
+
crypto: CryptoServices | null;
|
|
79
|
+
/** Null when the product does not use Stripe billing. */
|
|
80
|
+
stripe: StripeServices | null;
|
|
81
|
+
/** Null when the product does not expose a metered inference gateway. */
|
|
82
|
+
gateway: GatewayServices | null;
|
|
83
|
+
/** Null when the product does not use a hot-pool of pre-provisioned instances. */
|
|
84
|
+
hotPool: HotPoolServices | null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build a fully-wired PlatformContainer from a declarative BootConfig.
|
|
88
|
+
*
|
|
89
|
+
* Construction order mirrors the proven boot sequence from product index.ts
|
|
90
|
+
* files: DB pool -> Drizzle -> migrations -> productConfig -> credit ledger
|
|
91
|
+
* -> org repos -> org service -> user role repo -> feature services.
|
|
92
|
+
*
|
|
93
|
+
* Feature sub-containers (fleet, crypto, stripe, gateway) are only
|
|
94
|
+
* constructed when their corresponding feature flag is enabled in
|
|
95
|
+
* `bootConfig.features`. Disabled features yield `null`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildContainer(bootConfig: BootConfig): Promise<PlatformContainer>;
|