@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,137 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createTestContainer } from "../../test-container.js";
3
+ import { createCryptoWebhookRoutes } from "../crypto-webhook.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Mock the key-server-webhook handler so we never hit real billing logic
6
+ // ---------------------------------------------------------------------------
7
+ vi.mock("../../../billing/crypto/key-server-webhook.js", () => ({
8
+ handleKeyServerWebhook: vi.fn().mockResolvedValue({
9
+ handled: true,
10
+ creditedCents: 500,
11
+ }),
12
+ }));
13
+ // ---------------------------------------------------------------------------
14
+ // Helpers
15
+ // ---------------------------------------------------------------------------
16
+ const SECRET = "test-provision-secret";
17
+ const CRYPTO_KEY = "test-crypto-service-key";
18
+ function makeConfig(overrides) {
19
+ return {
20
+ provisionSecret: SECRET,
21
+ cryptoServiceKey: CRYPTO_KEY,
22
+ ...overrides,
23
+ };
24
+ }
25
+ function makeCrypto() {
26
+ return {
27
+ chargeRepo: {},
28
+ webhookSeenRepo: {},
29
+ };
30
+ }
31
+ const validPayload = {
32
+ chargeId: "ch_123",
33
+ chain: "ETH",
34
+ address: "0xabc",
35
+ status: "confirmed",
36
+ };
37
+ function buildApp(opts) {
38
+ const container = createTestContainer({
39
+ crypto: opts?.crypto !== undefined ? opts.crypto : makeCrypto(),
40
+ });
41
+ const config = makeConfig(opts?.config);
42
+ return createCryptoWebhookRoutes(container, config);
43
+ }
44
+ async function post(app, body, headers) {
45
+ const init = {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ ...headers,
50
+ },
51
+ };
52
+ if (body !== undefined) {
53
+ init.body = typeof body === "string" ? body : JSON.stringify(body);
54
+ }
55
+ return app.request("/", init);
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Tests
59
+ // ---------------------------------------------------------------------------
60
+ describe("createCryptoWebhookRoutes", () => {
61
+ // 1. No auth header
62
+ it("returns 401 without auth header", async () => {
63
+ const app = buildApp();
64
+ const res = await post(app, validPayload);
65
+ expect(res.status).toBe(401);
66
+ const json = await res.json();
67
+ expect(json.error).toBe("Unauthorized");
68
+ });
69
+ // 2. Wrong Bearer token
70
+ it("returns 401 with wrong Bearer token", async () => {
71
+ const app = buildApp();
72
+ const res = await post(app, validPayload, {
73
+ Authorization: "Bearer wrong-token",
74
+ });
75
+ expect(res.status).toBe(401);
76
+ const json = await res.json();
77
+ expect(json.error).toBe("Unauthorized");
78
+ });
79
+ // 3. Invalid JSON
80
+ it("returns 400 on invalid JSON", async () => {
81
+ const app = buildApp();
82
+ const res = await app.request("/", {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ Authorization: `Bearer ${SECRET}`,
87
+ },
88
+ body: "not-valid-json{{{",
89
+ });
90
+ expect(res.status).toBe(400);
91
+ const json = await res.json();
92
+ expect(json.error).toBe("Invalid JSON");
93
+ });
94
+ // 4. Zod validation failure (missing required fields)
95
+ it("returns 400 on payload that fails Zod validation", async () => {
96
+ const app = buildApp();
97
+ const res = await post(app, { chargeId: "ch_123" }, // missing chain, address, status
98
+ { Authorization: `Bearer ${SECRET}` });
99
+ expect(res.status).toBe(400);
100
+ const json = await res.json();
101
+ expect(json.error).toBe("Invalid payload");
102
+ expect(json.issues).toBeDefined();
103
+ expect(json.issues.length).toBeGreaterThan(0);
104
+ });
105
+ // 5. Container crypto is null -> 501
106
+ it("returns 501 when container.crypto is null", async () => {
107
+ const app = buildApp({ crypto: null });
108
+ const res = await post(app, validPayload, {
109
+ Authorization: `Bearer ${SECRET}`,
110
+ });
111
+ expect(res.status).toBe(501);
112
+ const json = await res.json();
113
+ expect(json.error).toBe("Crypto payments not configured");
114
+ });
115
+ // 6. Valid Bearer matching provision secret
116
+ it("accepts valid Bearer token matching provision secret", async () => {
117
+ const app = buildApp();
118
+ const res = await post(app, validPayload, {
119
+ Authorization: `Bearer ${SECRET}`,
120
+ });
121
+ expect(res.status).toBe(200);
122
+ const json = await res.json();
123
+ expect(json.handled).toBe(true);
124
+ expect(json.creditedCents).toBe(500);
125
+ });
126
+ // 7. Valid Bearer matching crypto service key
127
+ it("accepts valid Bearer token matching crypto service key", async () => {
128
+ const app = buildApp();
129
+ const res = await post(app, validPayload, {
130
+ Authorization: `Bearer ${CRYPTO_KEY}`,
131
+ });
132
+ expect(res.status).toBe(200);
133
+ const json = await res.json();
134
+ expect(json.handled).toBe(true);
135
+ expect(json.creditedCents).toBe(500);
136
+ });
137
+ });
@@ -0,0 +1,212 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createTestContainer } from "../../test-container.js";
3
+ import { createProvisionWebhookRoutes } from "../provision-webhook.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+ const SECRET = "test-provision-secret-1234";
8
+ function makeConfig(overrides) {
9
+ return {
10
+ provisionSecret: SECRET,
11
+ instanceImage: "ghcr.io/test/app:latest",
12
+ containerPort: 3000,
13
+ maxInstancesPerTenant: 5,
14
+ gatewayUrl: "http://gateway:4000",
15
+ containerPrefix: "test",
16
+ ...overrides,
17
+ };
18
+ }
19
+ function makeFleet() {
20
+ return {
21
+ manager: {
22
+ create: vi.fn().mockResolvedValue({
23
+ id: "inst-001",
24
+ containerId: "docker-abc",
25
+ containerName: "test-myapp",
26
+ url: "http://test-myapp:3000",
27
+ profile: { id: "inst-001", name: "myapp", tenantId: "tenant-1" },
28
+ }),
29
+ remove: vi.fn().mockResolvedValue(undefined),
30
+ status: vi.fn().mockResolvedValue({
31
+ id: "inst-001",
32
+ name: "myapp",
33
+ state: "running",
34
+ }),
35
+ },
36
+ docker: {},
37
+ proxy: {
38
+ addRoute: vi.fn().mockResolvedValue(undefined),
39
+ removeRoute: vi.fn(),
40
+ updateHealth: vi.fn(),
41
+ getRoutes: vi.fn().mockReturnValue([]),
42
+ start: vi.fn().mockResolvedValue(undefined),
43
+ stop: vi.fn().mockResolvedValue(undefined),
44
+ reload: vi.fn().mockResolvedValue(undefined),
45
+ },
46
+ profileStore: {
47
+ init: vi.fn().mockResolvedValue(undefined),
48
+ save: vi.fn().mockResolvedValue(undefined),
49
+ get: vi.fn().mockResolvedValue(null),
50
+ list: vi.fn().mockResolvedValue([]),
51
+ delete: vi.fn().mockResolvedValue(true),
52
+ },
53
+ serviceKeyRepo: {
54
+ generate: vi.fn().mockResolvedValue("key-abc"),
55
+ resolve: vi.fn().mockResolvedValue(null),
56
+ revokeByInstance: vi.fn().mockResolvedValue(undefined),
57
+ revokeByTenant: vi.fn().mockResolvedValue(undefined),
58
+ },
59
+ };
60
+ }
61
+ function buildApp(opts) {
62
+ const container = createTestContainer({
63
+ fleet: opts?.fleet !== undefined ? opts.fleet : makeFleet(),
64
+ });
65
+ const config = makeConfig(opts?.config);
66
+ return createProvisionWebhookRoutes(container, config);
67
+ }
68
+ async function request(app, method, path, body, headers) {
69
+ const init = {
70
+ method,
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ ...headers,
74
+ },
75
+ };
76
+ if (body !== undefined) {
77
+ init.body = JSON.stringify(body);
78
+ }
79
+ return app.request(path, init);
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Tests
83
+ // ---------------------------------------------------------------------------
84
+ describe("createProvisionWebhookRoutes", () => {
85
+ // ---- Auth tests (apply to all endpoints) ----
86
+ it("returns 401 without authorization header", async () => {
87
+ const app = buildApp();
88
+ const res = await request(app, "POST", "/create", { tenantId: "t1", subdomain: "test" });
89
+ expect(res.status).toBe(401);
90
+ const json = await res.json();
91
+ expect(json.error).toBe("Unauthorized");
92
+ });
93
+ it("returns 401 with wrong secret", async () => {
94
+ const app = buildApp();
95
+ const res = await request(app, "POST", "/create", { tenantId: "t1", subdomain: "test" }, {
96
+ Authorization: "Bearer wrong-secret",
97
+ });
98
+ expect(res.status).toBe(401);
99
+ const json = await res.json();
100
+ expect(json.error).toBe("Unauthorized");
101
+ });
102
+ // ---- Fleet not configured ----
103
+ it("returns 501 when fleet not configured (container.fleet is null)", async () => {
104
+ const app = buildApp({ fleet: null });
105
+ const res = await request(app, "POST", "/create", { tenantId: "t1", subdomain: "test" }, {
106
+ Authorization: `Bearer ${SECRET}`,
107
+ });
108
+ expect(res.status).toBe(501);
109
+ const json = await res.json();
110
+ expect(json.error).toBe("Fleet management not configured");
111
+ });
112
+ it("returns 501 on destroy when fleet not configured", async () => {
113
+ const app = buildApp({ fleet: null });
114
+ const res = await request(app, "POST", "/destroy", { instanceId: "inst-001" }, {
115
+ Authorization: `Bearer ${SECRET}`,
116
+ });
117
+ expect(res.status).toBe(501);
118
+ });
119
+ it("returns 501 on budget when fleet not configured", async () => {
120
+ const app = buildApp({ fleet: null });
121
+ const res = await request(app, "PUT", "/budget", { instanceId: "inst-001", tenantEntityId: "te-1", budgetCents: 1000 }, { Authorization: `Bearer ${SECRET}` });
122
+ expect(res.status).toBe(501);
123
+ });
124
+ // ---- Create endpoint ----
125
+ it("handles create webhook with valid auth and payload", async () => {
126
+ const fleet = makeFleet();
127
+ const app = buildApp({ fleet });
128
+ const res = await request(app, "POST", "/create", { tenantId: "tenant-1", subdomain: "myapp" }, { Authorization: `Bearer ${SECRET}` });
129
+ expect(res.status).toBe(201);
130
+ const json = await res.json();
131
+ expect(json.ok).toBe(true);
132
+ expect(json.instanceId).toBe("inst-001");
133
+ expect(json.subdomain).toBe("myapp");
134
+ expect(json.containerUrl).toBe("http://test-myapp:3000");
135
+ // Verify fleet.create was called with generic env var names
136
+ expect(fleet.manager.create).toHaveBeenCalledTimes(1);
137
+ const createCall = fleet.manager.create.mock.calls[0][0];
138
+ expect(createCall.env.HOSTED_MODE).toBe("true");
139
+ expect(createCall.env.DEPLOYMENT_MODE).toBe("hosted_proxy");
140
+ expect(createCall.env.DEPLOYMENT_EXPOSURE).toBe("private");
141
+ expect(createCall.env.MIGRATION_AUTO_APPLY).toBe("true");
142
+ // Verify NO product-specific prefixes
143
+ expect(createCall.env.PAPERCLIP_HOSTED_MODE).toBeUndefined();
144
+ expect(createCall.env.PAPERCLIP_DEPLOYMENT_MODE).toBeUndefined();
145
+ // Verify proxy route was registered
146
+ expect(fleet.proxy.addRoute).toHaveBeenCalledTimes(1);
147
+ });
148
+ it("returns 422 on create when required fields are missing", async () => {
149
+ const app = buildApp();
150
+ const res = await request(app, "POST", "/create", { tenantId: "tenant-1" }, // missing subdomain
151
+ { Authorization: `Bearer ${SECRET}` });
152
+ expect(res.status).toBe(422);
153
+ const json = await res.json();
154
+ expect(json.error).toContain("Missing required fields");
155
+ });
156
+ // ---- Destroy endpoint ----
157
+ it("handles destroy webhook with valid auth and instanceId", async () => {
158
+ const fleet = makeFleet();
159
+ const app = buildApp({ fleet });
160
+ const res = await request(app, "POST", "/destroy", { instanceId: "inst-001" }, { Authorization: `Bearer ${SECRET}` });
161
+ expect(res.status).toBe(200);
162
+ const json = await res.json();
163
+ expect(json.ok).toBe(true);
164
+ expect(fleet.serviceKeyRepo.revokeByInstance).toHaveBeenCalledWith("inst-001");
165
+ expect(fleet.manager.remove).toHaveBeenCalledWith("inst-001");
166
+ expect(fleet.proxy.removeRoute).toHaveBeenCalledWith("inst-001");
167
+ });
168
+ it("returns 422 on destroy when instanceId is missing", async () => {
169
+ const app = buildApp();
170
+ const res = await request(app, "POST", "/destroy", {}, {
171
+ Authorization: `Bearer ${SECRET}`,
172
+ });
173
+ expect(res.status).toBe(422);
174
+ const json = await res.json();
175
+ expect(json.error).toContain("Missing required field");
176
+ });
177
+ // ---- Budget endpoint ----
178
+ it("handles budget webhook with valid auth and payload", async () => {
179
+ const fleet = makeFleet();
180
+ const app = buildApp({ fleet });
181
+ const res = await request(app, "PUT", "/budget", { instanceId: "inst-001", tenantEntityId: "te-1", budgetCents: 5000 }, { Authorization: `Bearer ${SECRET}` });
182
+ expect(res.status).toBe(200);
183
+ const json = await res.json();
184
+ expect(json.ok).toBe(true);
185
+ expect(json.budgetCents).toBe(5000);
186
+ });
187
+ it("returns 422 on budget when required fields are missing", async () => {
188
+ const app = buildApp();
189
+ const res = await request(app, "PUT", "/budget", { instanceId: "inst-001" }, // missing tenantEntityId, budgetCents
190
+ { Authorization: `Bearer ${SECRET}` });
191
+ expect(res.status).toBe(422);
192
+ const json = await res.json();
193
+ expect(json.error).toContain("Missing required fields");
194
+ });
195
+ // ---- Generic env var names ----
196
+ it("uses generic env var names with no PAPERCLIP_ prefix anywhere", async () => {
197
+ const fleet = makeFleet();
198
+ const app = buildApp({ fleet });
199
+ await request(app, "POST", "/create", { tenantId: "tenant-1", subdomain: "myapp" }, { Authorization: `Bearer ${SECRET}` });
200
+ const createCall = fleet.manager.create.mock.calls[0][0];
201
+ const envKeys = Object.keys(createCall.env);
202
+ for (const key of envKeys) {
203
+ expect(key).not.toMatch(/^PAPERCLIP_/);
204
+ }
205
+ // Verify expected generic names are present
206
+ expect(envKeys).toContain("HOSTED_MODE");
207
+ expect(envKeys).toContain("DEPLOYMENT_MODE");
208
+ expect(envKeys).toContain("DEPLOYMENT_EXPOSURE");
209
+ expect(envKeys).toContain("MIGRATION_AUTO_APPLY");
210
+ expect(envKeys).toContain("PROVISION_SECRET");
211
+ });
212
+ });
@@ -0,0 +1,65 @@
1
+ import { Hono } from "hono";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { createTestContainer } from "../../test-container.js";
4
+ import { createStripeWebhookRoutes } from "../stripe-webhook.js";
5
+ function makeApp(stripeOverride) {
6
+ const container = createTestContainer(stripeOverride !== undefined ? { stripe: stripeOverride } : {});
7
+ const app = new Hono();
8
+ app.route("/api/webhooks/stripe", createStripeWebhookRoutes(container));
9
+ return app;
10
+ }
11
+ describe("stripe webhook route", () => {
12
+ it("returns 501 when stripe not configured", async () => {
13
+ const app = makeApp(null);
14
+ const res = await app.request("/api/webhooks/stripe", { method: "POST" });
15
+ expect(res.status).toBe(501);
16
+ });
17
+ it("returns 400 when stripe-signature header missing", async () => {
18
+ const app = makeApp({
19
+ stripe: {},
20
+ webhookSecret: "whsec_test",
21
+ customerRepo: {},
22
+ processor: { handleWebhook: vi.fn() },
23
+ });
24
+ const res = await app.request("/api/webhooks/stripe", {
25
+ method: "POST",
26
+ body: "{}",
27
+ });
28
+ expect(res.status).toBe(400);
29
+ const body = await res.json();
30
+ expect(body.error).toBe("Missing stripe-signature header");
31
+ });
32
+ it("returns 200 on valid webhook", async () => {
33
+ const handleWebhook = vi.fn().mockResolvedValue({ handled: true, event_type: "checkout.session.completed" });
34
+ const app = makeApp({
35
+ stripe: {},
36
+ webhookSecret: "whsec_test",
37
+ customerRepo: {},
38
+ processor: { handleWebhook },
39
+ });
40
+ const res = await app.request("/api/webhooks/stripe", {
41
+ method: "POST",
42
+ headers: { "stripe-signature": "t=123,v1=abc" },
43
+ body: '{"type":"checkout.session.completed"}',
44
+ });
45
+ expect(res.status).toBe(200);
46
+ expect(handleWebhook).toHaveBeenCalledOnce();
47
+ });
48
+ it("returns 400 when processor throws (bad signature)", async () => {
49
+ const handleWebhook = vi.fn().mockRejectedValue(new Error("Signature verification failed"));
50
+ const app = makeApp({
51
+ stripe: {},
52
+ webhookSecret: "whsec_test",
53
+ customerRepo: {},
54
+ processor: { handleWebhook },
55
+ });
56
+ const res = await app.request("/api/webhooks/stripe", {
57
+ method: "POST",
58
+ headers: { "stripe-signature": "t=123,v1=bad" },
59
+ body: "invalid",
60
+ });
61
+ expect(res.status).toBe(400);
62
+ const body = await res.json();
63
+ expect(body.error).toBe("Webhook processing failed");
64
+ });
65
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Admin tRPC router factory — platform-wide settings for the operator.
3
+ *
4
+ * All endpoints require platform_admin role (via adminProcedure).
5
+ * Dependencies are injected via PlatformContainer rather than module-level
6
+ * singletons, enabling clean testing and per-product composition.
7
+ */
8
+ import type { PlatformContainer } from "../container.js";
9
+ type CachedModel = {
10
+ id: string;
11
+ name: string;
12
+ contextLength: number;
13
+ promptPrice: string;
14
+ completionPrice: string;
15
+ };
16
+ /**
17
+ * Synchronous model resolver for the gateway proxy.
18
+ * Returns the cached DB value, or null to fall back to env var.
19
+ * The cache is refreshed asynchronously every 5 seconds.
20
+ */
21
+ export declare function resolveGatewayModel(): string | null;
22
+ /** Seed the cache on startup so the first request doesn't miss. */
23
+ export declare function warmModelCache(container: PlatformContainer): Promise<void>;
24
+ export interface AdminRouterConfig {
25
+ openRouterApiKey?: string;
26
+ }
27
+ export declare function createAdminRouter(container: PlatformContainer, config?: AdminRouterConfig): import("@trpc/server").TRPCBuiltRouter<{
28
+ ctx: import("../../trpc/init.js").TRPCContext;
29
+ meta: object;
30
+ errorShape: import("@trpc/server").TRPCDefaultErrorShape;
31
+ transformer: false;
32
+ }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
33
+ /** Get the current gateway model setting. */
34
+ getGatewayModel: import("@trpc/server").TRPCQueryProcedure<{
35
+ input: void;
36
+ output: {
37
+ model: string;
38
+ updatedAt: string;
39
+ };
40
+ meta: object;
41
+ }>;
42
+ /** Set the gateway model. Takes effect within 5 seconds. */
43
+ setGatewayModel: import("@trpc/server").TRPCMutationProcedure<{
44
+ input: {
45
+ model: string;
46
+ };
47
+ output: {
48
+ ok: boolean;
49
+ model: string;
50
+ };
51
+ meta: object;
52
+ }>;
53
+ /** List available OpenRouter models for the gateway model dropdown. */
54
+ listAvailableModels: import("@trpc/server").TRPCQueryProcedure<{
55
+ input: void;
56
+ output: {
57
+ models: CachedModel[];
58
+ };
59
+ meta: object;
60
+ }>;
61
+ /** List ALL instances across all tenants with health status. */
62
+ listAllInstances: import("@trpc/server").TRPCQueryProcedure<{
63
+ input: void;
64
+ output: {
65
+ instances: never[];
66
+ error: string;
67
+ } | {
68
+ instances: {
69
+ id: string;
70
+ name: string;
71
+ tenantId: string;
72
+ image: string;
73
+ state: "paused" | "error" | "running" | "stopped" | "pulling" | "created" | "restarting" | "exited" | "dead";
74
+ health: string | null;
75
+ uptime: string | null;
76
+ containerId: string | null;
77
+ startedAt: string | null;
78
+ }[];
79
+ error?: undefined;
80
+ };
81
+ meta: object;
82
+ }>;
83
+ /** List all organizations with member counts and instance counts. */
84
+ listAllOrgs: import("@trpc/server").TRPCQueryProcedure<{
85
+ input: void;
86
+ output: {
87
+ orgs: {
88
+ id: string;
89
+ name: string;
90
+ slug: string | null;
91
+ createdAt: string;
92
+ memberCount: number;
93
+ instanceCount: number;
94
+ balanceCents: number;
95
+ }[];
96
+ };
97
+ meta: object;
98
+ }>;
99
+ /** Get platform billing summary: total credits, active service keys, payment method count. */
100
+ billingOverview: import("@trpc/server").TRPCQueryProcedure<{
101
+ input: void;
102
+ output: {
103
+ totalBalanceCents: number;
104
+ activeKeyCount: number;
105
+ paymentMethodCount: number;
106
+ orgCount: number;
107
+ };
108
+ meta: object;
109
+ }>;
110
+ getPoolConfig: import("@trpc/server").TRPCQueryProcedure<{
111
+ input: void;
112
+ output: {
113
+ enabled: boolean;
114
+ poolSize: number;
115
+ warmCount: number;
116
+ };
117
+ meta: object;
118
+ }>;
119
+ setPoolSize: import("@trpc/server").TRPCMutationProcedure<{
120
+ input: {
121
+ size: number;
122
+ };
123
+ output: {
124
+ poolSize: number;
125
+ };
126
+ meta: object;
127
+ }>;
128
+ }>>;
129
+ export {};