@wopr-network/platform-core 1.67.1 → 1.69.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 (81) hide show
  1. package/dist/auth/better-auth.js +7 -0
  2. package/dist/backup/types.d.ts +1 -1
  3. package/dist/email/client.js +16 -0
  4. package/dist/server/__tests__/build-container.test.d.ts +1 -0
  5. package/dist/server/__tests__/build-container.test.js +339 -0
  6. package/dist/server/__tests__/container.test.d.ts +1 -0
  7. package/dist/server/__tests__/container.test.js +170 -0
  8. package/dist/server/__tests__/lifecycle.test.d.ts +1 -0
  9. package/dist/server/__tests__/lifecycle.test.js +90 -0
  10. package/dist/server/__tests__/mount-routes.test.d.ts +1 -0
  11. package/dist/server/__tests__/mount-routes.test.js +151 -0
  12. package/dist/server/boot-config.d.ts +51 -0
  13. package/dist/server/boot-config.js +7 -0
  14. package/dist/server/container.d.ts +81 -0
  15. package/dist/server/container.js +134 -0
  16. package/dist/server/index.d.ts +33 -0
  17. package/dist/server/index.js +66 -0
  18. package/dist/server/lifecycle.d.ts +25 -0
  19. package/dist/server/lifecycle.js +46 -0
  20. package/dist/server/middleware/__tests__/admin-auth.test.d.ts +1 -0
  21. package/dist/server/middleware/__tests__/admin-auth.test.js +59 -0
  22. package/dist/server/middleware/__tests__/tenant-proxy.test.d.ts +1 -0
  23. package/dist/server/middleware/__tests__/tenant-proxy.test.js +268 -0
  24. package/dist/server/middleware/admin-auth.d.ts +18 -0
  25. package/dist/server/middleware/admin-auth.js +38 -0
  26. package/dist/server/middleware/tenant-proxy.d.ts +56 -0
  27. package/dist/server/middleware/tenant-proxy.js +162 -0
  28. package/dist/server/mount-routes.d.ts +30 -0
  29. package/dist/server/mount-routes.js +74 -0
  30. package/dist/server/routes/__tests__/admin.test.d.ts +1 -0
  31. package/dist/server/routes/__tests__/admin.test.js +267 -0
  32. package/dist/server/routes/__tests__/crypto-webhook.test.d.ts +1 -0
  33. package/dist/server/routes/__tests__/crypto-webhook.test.js +137 -0
  34. package/dist/server/routes/__tests__/provision-webhook.test.d.ts +1 -0
  35. package/dist/server/routes/__tests__/provision-webhook.test.js +212 -0
  36. package/dist/server/routes/__tests__/stripe-webhook.test.d.ts +1 -0
  37. package/dist/server/routes/__tests__/stripe-webhook.test.js +65 -0
  38. package/dist/server/routes/admin.d.ts +111 -0
  39. package/dist/server/routes/admin.js +273 -0
  40. package/dist/server/routes/crypto-webhook.d.ts +23 -0
  41. package/dist/server/routes/crypto-webhook.js +82 -0
  42. package/dist/server/routes/provision-webhook.d.ts +38 -0
  43. package/dist/server/routes/provision-webhook.js +160 -0
  44. package/dist/server/routes/stripe-webhook.d.ts +10 -0
  45. package/dist/server/routes/stripe-webhook.js +29 -0
  46. package/dist/server/test-container.d.ts +15 -0
  47. package/dist/server/test-container.js +103 -0
  48. package/dist/trpc/auth-helpers.d.ts +17 -0
  49. package/dist/trpc/auth-helpers.js +26 -0
  50. package/dist/trpc/container-factories.d.ts +300 -0
  51. package/dist/trpc/container-factories.js +80 -0
  52. package/dist/trpc/index.d.ts +2 -0
  53. package/dist/trpc/index.js +2 -0
  54. package/package.json +8 -3
  55. package/src/auth/better-auth.ts +8 -0
  56. package/src/email/client.ts +18 -0
  57. package/src/server/__tests__/build-container.test.ts +402 -0
  58. package/src/server/__tests__/container.test.ts +204 -0
  59. package/src/server/__tests__/lifecycle.test.ts +106 -0
  60. package/src/server/__tests__/mount-routes.test.ts +169 -0
  61. package/src/server/boot-config.ts +84 -0
  62. package/src/server/container.ts +237 -0
  63. package/src/server/index.ts +92 -0
  64. package/src/server/lifecycle.ts +62 -0
  65. package/src/server/middleware/__tests__/admin-auth.test.ts +67 -0
  66. package/src/server/middleware/__tests__/tenant-proxy.test.ts +308 -0
  67. package/src/server/middleware/admin-auth.ts +51 -0
  68. package/src/server/middleware/tenant-proxy.ts +192 -0
  69. package/src/server/mount-routes.ts +113 -0
  70. package/src/server/routes/__tests__/admin.test.ts +320 -0
  71. package/src/server/routes/__tests__/crypto-webhook.test.ts +167 -0
  72. package/src/server/routes/__tests__/provision-webhook.test.ts +323 -0
  73. package/src/server/routes/__tests__/stripe-webhook.test.ts +73 -0
  74. package/src/server/routes/admin.ts +334 -0
  75. package/src/server/routes/crypto-webhook.ts +110 -0
  76. package/src/server/routes/provision-webhook.ts +212 -0
  77. package/src/server/routes/stripe-webhook.ts +36 -0
  78. package/src/server/test-container.ts +120 -0
  79. package/src/trpc/auth-helpers.ts +28 -0
  80. package/src/trpc/container-factories.ts +114 -0
  81. package/src/trpc/index.ts +9 -0
@@ -0,0 +1,323 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { FleetServices } from "../../container.js";
3
+ import { createTestContainer } from "../../test-container.js";
4
+ import { createProvisionWebhookRoutes, type ProvisionWebhookConfig } from "../provision-webhook.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const SECRET = "test-provision-secret-1234";
11
+
12
+ function makeConfig(overrides?: Partial<ProvisionWebhookConfig>): ProvisionWebhookConfig {
13
+ return {
14
+ provisionSecret: SECRET,
15
+ instanceImage: "ghcr.io/test/app:latest",
16
+ containerPort: 3000,
17
+ maxInstancesPerTenant: 5,
18
+ gatewayUrl: "http://gateway:4000",
19
+ containerPrefix: "test",
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ function makeFleet(): FleetServices {
25
+ return {
26
+ manager: {
27
+ create: vi.fn().mockResolvedValue({
28
+ id: "inst-001",
29
+ containerId: "docker-abc",
30
+ containerName: "test-myapp",
31
+ url: "http://test-myapp:3000",
32
+ profile: { id: "inst-001", name: "myapp", tenantId: "tenant-1" },
33
+ }),
34
+ remove: vi.fn().mockResolvedValue(undefined),
35
+ status: vi.fn().mockResolvedValue({
36
+ id: "inst-001",
37
+ name: "myapp",
38
+ state: "running",
39
+ }),
40
+ } as never,
41
+ docker: {} as never,
42
+ proxy: {
43
+ addRoute: vi.fn().mockResolvedValue(undefined),
44
+ removeRoute: vi.fn(),
45
+ updateHealth: vi.fn(),
46
+ getRoutes: vi.fn().mockReturnValue([]),
47
+ start: vi.fn().mockResolvedValue(undefined),
48
+ stop: vi.fn().mockResolvedValue(undefined),
49
+ reload: vi.fn().mockResolvedValue(undefined),
50
+ },
51
+ profileStore: {
52
+ init: vi.fn().mockResolvedValue(undefined),
53
+ save: vi.fn().mockResolvedValue(undefined),
54
+ get: vi.fn().mockResolvedValue(null),
55
+ list: vi.fn().mockResolvedValue([]),
56
+ delete: vi.fn().mockResolvedValue(true),
57
+ },
58
+ serviceKeyRepo: {
59
+ generate: vi.fn().mockResolvedValue("key-abc"),
60
+ resolve: vi.fn().mockResolvedValue(null),
61
+ revokeByInstance: vi.fn().mockResolvedValue(undefined),
62
+ revokeByTenant: vi.fn().mockResolvedValue(undefined),
63
+ } as never,
64
+ };
65
+ }
66
+
67
+ function buildApp(opts?: { fleet?: FleetServices | null; config?: Partial<ProvisionWebhookConfig> }) {
68
+ const container = createTestContainer({
69
+ fleet: opts?.fleet !== undefined ? opts.fleet : makeFleet(),
70
+ });
71
+ const config = makeConfig(opts?.config);
72
+ return createProvisionWebhookRoutes(container, config);
73
+ }
74
+
75
+ async function request(
76
+ app: ReturnType<typeof buildApp>,
77
+ method: string,
78
+ path: string,
79
+ body?: unknown,
80
+ headers?: Record<string, string>,
81
+ ) {
82
+ const init: RequestInit = {
83
+ method,
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ ...headers,
87
+ },
88
+ };
89
+ if (body !== undefined) {
90
+ init.body = JSON.stringify(body);
91
+ }
92
+ return app.request(path, init);
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Tests
97
+ // ---------------------------------------------------------------------------
98
+
99
+ describe("createProvisionWebhookRoutes", () => {
100
+ // ---- Auth tests (apply to all endpoints) ----
101
+
102
+ it("returns 401 without authorization header", async () => {
103
+ const app = buildApp();
104
+ const res = await request(app, "POST", "/create", { tenantId: "t1", subdomain: "test" });
105
+
106
+ expect(res.status).toBe(401);
107
+ const json = await res.json();
108
+ expect(json.error).toBe("Unauthorized");
109
+ });
110
+
111
+ it("returns 401 with wrong secret", async () => {
112
+ const app = buildApp();
113
+ const res = await request(
114
+ app,
115
+ "POST",
116
+ "/create",
117
+ { tenantId: "t1", subdomain: "test" },
118
+ {
119
+ Authorization: "Bearer wrong-secret",
120
+ },
121
+ );
122
+
123
+ expect(res.status).toBe(401);
124
+ const json = await res.json();
125
+ expect(json.error).toBe("Unauthorized");
126
+ });
127
+
128
+ // ---- Fleet not configured ----
129
+
130
+ it("returns 501 when fleet not configured (container.fleet is null)", async () => {
131
+ const app = buildApp({ fleet: null });
132
+ const res = await request(
133
+ app,
134
+ "POST",
135
+ "/create",
136
+ { tenantId: "t1", subdomain: "test" },
137
+ {
138
+ Authorization: `Bearer ${SECRET}`,
139
+ },
140
+ );
141
+
142
+ expect(res.status).toBe(501);
143
+ const json = await res.json();
144
+ expect(json.error).toBe("Fleet management not configured");
145
+ });
146
+
147
+ it("returns 501 on destroy when fleet not configured", async () => {
148
+ const app = buildApp({ fleet: null });
149
+ const res = await request(
150
+ app,
151
+ "POST",
152
+ "/destroy",
153
+ { instanceId: "inst-001" },
154
+ {
155
+ Authorization: `Bearer ${SECRET}`,
156
+ },
157
+ );
158
+
159
+ expect(res.status).toBe(501);
160
+ });
161
+
162
+ it("returns 501 on budget when fleet not configured", async () => {
163
+ const app = buildApp({ fleet: null });
164
+ const res = await request(
165
+ app,
166
+ "PUT",
167
+ "/budget",
168
+ { instanceId: "inst-001", tenantEntityId: "te-1", budgetCents: 1000 },
169
+ { Authorization: `Bearer ${SECRET}` },
170
+ );
171
+
172
+ expect(res.status).toBe(501);
173
+ });
174
+
175
+ // ---- Create endpoint ----
176
+
177
+ it("handles create webhook with valid auth and payload", async () => {
178
+ const fleet = makeFleet();
179
+ const app = buildApp({ fleet });
180
+ const res = await request(
181
+ app,
182
+ "POST",
183
+ "/create",
184
+ { tenantId: "tenant-1", subdomain: "myapp" },
185
+ { Authorization: `Bearer ${SECRET}` },
186
+ );
187
+
188
+ expect(res.status).toBe(201);
189
+ const json = await res.json();
190
+ expect(json.ok).toBe(true);
191
+ expect(json.instanceId).toBe("inst-001");
192
+ expect(json.subdomain).toBe("myapp");
193
+ expect(json.containerUrl).toBe("http://test-myapp:3000");
194
+
195
+ // Verify fleet.create was called with generic env var names
196
+ expect(fleet.manager.create).toHaveBeenCalledTimes(1);
197
+ const createCall = (fleet.manager.create as ReturnType<typeof vi.fn>).mock.calls[0][0];
198
+ expect(createCall.env.HOSTED_MODE).toBe("true");
199
+ expect(createCall.env.DEPLOYMENT_MODE).toBe("hosted_proxy");
200
+ expect(createCall.env.DEPLOYMENT_EXPOSURE).toBe("private");
201
+ expect(createCall.env.MIGRATION_AUTO_APPLY).toBe("true");
202
+ // Verify NO product-specific prefixes
203
+ expect(createCall.env.PAPERCLIP_HOSTED_MODE).toBeUndefined();
204
+ expect(createCall.env.PAPERCLIP_DEPLOYMENT_MODE).toBeUndefined();
205
+
206
+ // Verify proxy route was registered
207
+ expect(fleet.proxy.addRoute).toHaveBeenCalledTimes(1);
208
+ });
209
+
210
+ it("returns 422 on create when required fields are missing", async () => {
211
+ const app = buildApp();
212
+ const res = await request(
213
+ app,
214
+ "POST",
215
+ "/create",
216
+ { tenantId: "tenant-1" }, // missing subdomain
217
+ { Authorization: `Bearer ${SECRET}` },
218
+ );
219
+
220
+ expect(res.status).toBe(422);
221
+ const json = await res.json();
222
+ expect(json.error).toContain("Missing required fields");
223
+ });
224
+
225
+ // ---- Destroy endpoint ----
226
+
227
+ it("handles destroy webhook with valid auth and instanceId", async () => {
228
+ const fleet = makeFleet();
229
+ const app = buildApp({ fleet });
230
+ const res = await request(
231
+ app,
232
+ "POST",
233
+ "/destroy",
234
+ { instanceId: "inst-001" },
235
+ { Authorization: `Bearer ${SECRET}` },
236
+ );
237
+
238
+ expect(res.status).toBe(200);
239
+ const json = await res.json();
240
+ expect(json.ok).toBe(true);
241
+
242
+ expect(fleet.serviceKeyRepo.revokeByInstance).toHaveBeenCalledWith("inst-001");
243
+ expect(fleet.manager.remove).toHaveBeenCalledWith("inst-001");
244
+ expect(fleet.proxy.removeRoute).toHaveBeenCalledWith("inst-001");
245
+ });
246
+
247
+ it("returns 422 on destroy when instanceId is missing", async () => {
248
+ const app = buildApp();
249
+ const res = await request(
250
+ app,
251
+ "POST",
252
+ "/destroy",
253
+ {},
254
+ {
255
+ Authorization: `Bearer ${SECRET}`,
256
+ },
257
+ );
258
+
259
+ expect(res.status).toBe(422);
260
+ const json = await res.json();
261
+ expect(json.error).toContain("Missing required field");
262
+ });
263
+
264
+ // ---- Budget endpoint ----
265
+
266
+ it("handles budget webhook with valid auth and payload", async () => {
267
+ const fleet = makeFleet();
268
+ const app = buildApp({ fleet });
269
+ const res = await request(
270
+ app,
271
+ "PUT",
272
+ "/budget",
273
+ { instanceId: "inst-001", tenantEntityId: "te-1", budgetCents: 5000 },
274
+ { Authorization: `Bearer ${SECRET}` },
275
+ );
276
+
277
+ expect(res.status).toBe(200);
278
+ const json = await res.json();
279
+ expect(json.ok).toBe(true);
280
+ expect(json.budgetCents).toBe(5000);
281
+ });
282
+
283
+ it("returns 422 on budget when required fields are missing", async () => {
284
+ const app = buildApp();
285
+ const res = await request(
286
+ app,
287
+ "PUT",
288
+ "/budget",
289
+ { instanceId: "inst-001" }, // missing tenantEntityId, budgetCents
290
+ { Authorization: `Bearer ${SECRET}` },
291
+ );
292
+
293
+ expect(res.status).toBe(422);
294
+ const json = await res.json();
295
+ expect(json.error).toContain("Missing required fields");
296
+ });
297
+
298
+ // ---- Generic env var names ----
299
+
300
+ it("uses generic env var names with no PAPERCLIP_ prefix anywhere", async () => {
301
+ const fleet = makeFleet();
302
+ const app = buildApp({ fleet });
303
+ await request(
304
+ app,
305
+ "POST",
306
+ "/create",
307
+ { tenantId: "tenant-1", subdomain: "myapp" },
308
+ { Authorization: `Bearer ${SECRET}` },
309
+ );
310
+
311
+ const createCall = (fleet.manager.create as ReturnType<typeof vi.fn>).mock.calls[0][0];
312
+ const envKeys = Object.keys(createCall.env);
313
+ for (const key of envKeys) {
314
+ expect(key).not.toMatch(/^PAPERCLIP_/);
315
+ }
316
+ // Verify expected generic names are present
317
+ expect(envKeys).toContain("HOSTED_MODE");
318
+ expect(envKeys).toContain("DEPLOYMENT_MODE");
319
+ expect(envKeys).toContain("DEPLOYMENT_EXPOSURE");
320
+ expect(envKeys).toContain("MIGRATION_AUTO_APPLY");
321
+ expect(envKeys).toContain("PROVISION_SECRET");
322
+ });
323
+ });
@@ -0,0 +1,73 @@
1
+ import { Hono } from "hono";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { createTestContainer } from "../../test-container.js";
5
+ import { createStripeWebhookRoutes } from "../stripe-webhook.js";
6
+
7
+ function makeApp(stripeOverride?: unknown) {
8
+ const container = createTestContainer(
9
+ stripeOverride !== undefined ? { stripe: stripeOverride as ReturnType<typeof createTestContainer>["stripe"] } : {},
10
+ );
11
+ const app = new Hono();
12
+ app.route("/api/webhooks/stripe", createStripeWebhookRoutes(container));
13
+ return app;
14
+ }
15
+
16
+ describe("stripe webhook route", () => {
17
+ it("returns 501 when stripe not configured", async () => {
18
+ const app = makeApp(null);
19
+ const res = await app.request("/api/webhooks/stripe", { method: "POST" });
20
+ expect(res.status).toBe(501);
21
+ });
22
+
23
+ it("returns 400 when stripe-signature header missing", async () => {
24
+ const app = makeApp({
25
+ stripe: {},
26
+ webhookSecret: "whsec_test",
27
+ customerRepo: {},
28
+ processor: { handleWebhook: vi.fn() },
29
+ });
30
+ const res = await app.request("/api/webhooks/stripe", {
31
+ method: "POST",
32
+ body: "{}",
33
+ });
34
+ expect(res.status).toBe(400);
35
+ const body = await res.json();
36
+ expect(body.error).toBe("Missing stripe-signature header");
37
+ });
38
+
39
+ it("returns 200 on valid webhook", async () => {
40
+ const handleWebhook = vi.fn().mockResolvedValue({ handled: true, event_type: "checkout.session.completed" });
41
+ const app = makeApp({
42
+ stripe: {},
43
+ webhookSecret: "whsec_test",
44
+ customerRepo: {},
45
+ processor: { handleWebhook },
46
+ });
47
+ const res = await app.request("/api/webhooks/stripe", {
48
+ method: "POST",
49
+ headers: { "stripe-signature": "t=123,v1=abc" },
50
+ body: '{"type":"checkout.session.completed"}',
51
+ });
52
+ expect(res.status).toBe(200);
53
+ expect(handleWebhook).toHaveBeenCalledOnce();
54
+ });
55
+
56
+ it("returns 400 when processor throws (bad signature)", async () => {
57
+ const handleWebhook = vi.fn().mockRejectedValue(new Error("Signature verification failed"));
58
+ const app = makeApp({
59
+ stripe: {},
60
+ webhookSecret: "whsec_test",
61
+ customerRepo: {},
62
+ processor: { handleWebhook },
63
+ });
64
+ const res = await app.request("/api/webhooks/stripe", {
65
+ method: "POST",
66
+ headers: { "stripe-signature": "t=123,v1=bad" },
67
+ body: "invalid",
68
+ });
69
+ expect(res.status).toBe(400);
70
+ const body = await res.json();
71
+ expect(body.error).toBe("Webhook processing failed");
72
+ });
73
+ });