@wopr-network/platform-core 1.68.0 → 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.
- package/dist/backup/types.d.ts +1 -1
- 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 +170 -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 +81 -0
- package/dist/server/container.js +134 -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 +46 -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 +111 -0
- package/dist/server/routes/admin.js +273 -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/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/package.json +5 -1
- package/src/server/__tests__/build-container.test.ts +402 -0
- package/src/server/__tests__/container.test.ts +204 -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 +237 -0
- package/src/server/index.ts +92 -0
- package/src/server/lifecycle.ts +62 -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 +334 -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/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,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,81 @@
|
|
|
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
|
+
/** Will be typed properly when extracted from nemoclaw. */
|
|
49
|
+
poolManager: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface PlatformContainer {
|
|
52
|
+
db: DrizzleDb;
|
|
53
|
+
pool: Pool;
|
|
54
|
+
productConfig: ProductConfig;
|
|
55
|
+
creditLedger: ILedger;
|
|
56
|
+
orgMemberRepo: IOrgMemberRepository;
|
|
57
|
+
orgService: OrgService;
|
|
58
|
+
userRoleRepo: IUserRoleRepository;
|
|
59
|
+
/** Null when the product does not use fleet management. */
|
|
60
|
+
fleet: FleetServices | null;
|
|
61
|
+
/** Null when the product does not accept crypto payments. */
|
|
62
|
+
crypto: CryptoServices | null;
|
|
63
|
+
/** Null when the product does not use Stripe billing. */
|
|
64
|
+
stripe: StripeServices | null;
|
|
65
|
+
/** Null when the product does not expose a metered inference gateway. */
|
|
66
|
+
gateway: GatewayServices | null;
|
|
67
|
+
/** Null when the product does not use a hot-pool of pre-provisioned instances. */
|
|
68
|
+
hotPool: HotPoolServices | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a fully-wired PlatformContainer from a declarative BootConfig.
|
|
72
|
+
*
|
|
73
|
+
* Construction order mirrors the proven boot sequence from product index.ts
|
|
74
|
+
* files: DB pool -> Drizzle -> migrations -> productConfig -> credit ledger
|
|
75
|
+
* -> org repos -> org service -> user role repo -> feature services.
|
|
76
|
+
*
|
|
77
|
+
* Feature sub-containers (fleet, crypto, stripe, gateway) are only
|
|
78
|
+
* constructed when their corresponding feature flag is enabled in
|
|
79
|
+
* `bootConfig.features`. Disabled features yield `null`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildContainer(bootConfig: BootConfig): Promise<PlatformContainer>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlatformContainer — the central DI container for platform-core.
|
|
3
|
+
*
|
|
4
|
+
* Products compose a container at boot time, enabling only the feature
|
|
5
|
+
* slices they need. Nullable sub-containers (fleet, crypto, stripe,
|
|
6
|
+
* gateway, hotPool) let each product opt in without pulling unused deps.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// buildContainer — construct a PlatformContainer from a BootConfig
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Build a fully-wired PlatformContainer from a declarative BootConfig.
|
|
13
|
+
*
|
|
14
|
+
* Construction order mirrors the proven boot sequence from product index.ts
|
|
15
|
+
* files: DB pool -> Drizzle -> migrations -> productConfig -> credit ledger
|
|
16
|
+
* -> org repos -> org service -> user role repo -> feature services.
|
|
17
|
+
*
|
|
18
|
+
* Feature sub-containers (fleet, crypto, stripe, gateway) are only
|
|
19
|
+
* constructed when their corresponding feature flag is enabled in
|
|
20
|
+
* `bootConfig.features`. Disabled features yield `null`.
|
|
21
|
+
*/
|
|
22
|
+
export async function buildContainer(bootConfig) {
|
|
23
|
+
if (!bootConfig.databaseUrl) {
|
|
24
|
+
throw new Error("buildContainer: databaseUrl is required");
|
|
25
|
+
}
|
|
26
|
+
// 1. Database pool
|
|
27
|
+
const { Pool: PgPool } = await import("pg");
|
|
28
|
+
const pool = new PgPool({ connectionString: bootConfig.databaseUrl });
|
|
29
|
+
// 2. Drizzle ORM instance
|
|
30
|
+
const { createDb } = await import("../db/index.js");
|
|
31
|
+
const db = createDb(pool);
|
|
32
|
+
// 3. Run Drizzle migrations
|
|
33
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
34
|
+
const path = await import("node:path");
|
|
35
|
+
const migrationsFolder = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../drizzle");
|
|
36
|
+
await migrate(db, { migrationsFolder });
|
|
37
|
+
// 4. Bootstrap product config from DB (auto-seeds from presets if needed)
|
|
38
|
+
const { platformBoot } = await import("../product-config/boot.js");
|
|
39
|
+
const { config: productConfig } = await platformBoot({ slug: bootConfig.slug, db });
|
|
40
|
+
// 5. Credit ledger
|
|
41
|
+
const { DrizzleLedger } = await import("../credits/ledger.js");
|
|
42
|
+
const creditLedger = new DrizzleLedger(db);
|
|
43
|
+
await creditLedger.seedSystemAccounts();
|
|
44
|
+
// 6. Org repositories + OrgService
|
|
45
|
+
const { DrizzleOrgMemberRepository } = await import("../tenancy/org-member-repository.js");
|
|
46
|
+
const { DrizzleOrgRepository } = await import("../tenancy/drizzle-org-repository.js");
|
|
47
|
+
const { OrgService: OrgServiceClass } = await import("../tenancy/org-service.js");
|
|
48
|
+
const { BetterAuthUserRepository } = await import("../db/auth-user-repository.js");
|
|
49
|
+
const orgMemberRepo = new DrizzleOrgMemberRepository(db);
|
|
50
|
+
const orgRepo = new DrizzleOrgRepository(db);
|
|
51
|
+
const authUserRepo = new BetterAuthUserRepository(pool);
|
|
52
|
+
const orgService = new OrgServiceClass(orgRepo, orgMemberRepo, db, {
|
|
53
|
+
userRepo: authUserRepo,
|
|
54
|
+
});
|
|
55
|
+
// 7. User role repository
|
|
56
|
+
const { DrizzleUserRoleRepository } = await import("../auth/user-role-repository.js");
|
|
57
|
+
const userRoleRepo = new DrizzleUserRoleRepository(db);
|
|
58
|
+
// 8. Fleet services (when enabled)
|
|
59
|
+
let fleet = null;
|
|
60
|
+
if (bootConfig.features.fleet) {
|
|
61
|
+
const { FleetManager: FleetManagerClass } = await import("../fleet/fleet-manager.js");
|
|
62
|
+
const { ProfileStore } = await import("../fleet/profile-store.js");
|
|
63
|
+
const { ProxyManager } = await import("../proxy/manager.js");
|
|
64
|
+
const { DrizzleServiceKeyRepository } = await import("../gateway/service-key-repository.js");
|
|
65
|
+
const DockerModule = await import("dockerode");
|
|
66
|
+
const DockerClass = DockerModule.default ?? DockerModule;
|
|
67
|
+
const docker = new DockerClass();
|
|
68
|
+
const fleetDataDir = productConfig.fleet?.fleetDataDir ?? "/data/fleet";
|
|
69
|
+
const profileStore = new ProfileStore(fleetDataDir);
|
|
70
|
+
const proxy = new ProxyManager();
|
|
71
|
+
const serviceKeyRepo = new DrizzleServiceKeyRepository(db);
|
|
72
|
+
const manager = new FleetManagerClass(docker, profileStore, undefined, // platformDiscovery
|
|
73
|
+
undefined, // networkPolicy
|
|
74
|
+
proxy);
|
|
75
|
+
fleet = { manager, docker, proxy, profileStore, serviceKeyRepo };
|
|
76
|
+
}
|
|
77
|
+
// 9. Crypto services (when enabled)
|
|
78
|
+
let crypto = null;
|
|
79
|
+
if (bootConfig.features.crypto) {
|
|
80
|
+
const { DrizzleCryptoChargeRepository } = await import("../billing/crypto/charge-store.js");
|
|
81
|
+
const { DrizzleWebhookSeenRepository } = await import("../billing/drizzle-webhook-seen-repository.js");
|
|
82
|
+
const chargeRepo = new DrizzleCryptoChargeRepository(db);
|
|
83
|
+
const webhookSeenRepo = new DrizzleWebhookSeenRepository(db);
|
|
84
|
+
crypto = { chargeRepo, webhookSeenRepo };
|
|
85
|
+
}
|
|
86
|
+
// 10. Stripe services (when enabled)
|
|
87
|
+
let stripe = null;
|
|
88
|
+
if (bootConfig.features.stripe && bootConfig.stripeSecretKey) {
|
|
89
|
+
const StripeModule = await import("stripe");
|
|
90
|
+
const StripeClass = StripeModule.default;
|
|
91
|
+
const stripeClient = new StripeClass(bootConfig.stripeSecretKey);
|
|
92
|
+
const { DrizzleTenantCustomerRepository } = await import("../billing/stripe/tenant-store.js");
|
|
93
|
+
const { loadCreditPriceMap } = await import("../billing/stripe/credit-prices.js");
|
|
94
|
+
const { StripePaymentProcessor } = await import("../billing/stripe/stripe-payment-processor.js");
|
|
95
|
+
const customerRepo = new DrizzleTenantCustomerRepository(db);
|
|
96
|
+
const priceMap = loadCreditPriceMap();
|
|
97
|
+
const processor = new StripePaymentProcessor({
|
|
98
|
+
stripe: stripeClient,
|
|
99
|
+
tenantRepo: customerRepo,
|
|
100
|
+
webhookSecret: bootConfig.stripeWebhookSecret ?? "",
|
|
101
|
+
priceMap,
|
|
102
|
+
creditLedger,
|
|
103
|
+
});
|
|
104
|
+
stripe = {
|
|
105
|
+
stripe: stripeClient,
|
|
106
|
+
webhookSecret: bootConfig.stripeWebhookSecret ?? "",
|
|
107
|
+
customerRepo,
|
|
108
|
+
processor,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// 11. Gateway services (when enabled)
|
|
112
|
+
let gateway = null;
|
|
113
|
+
if (bootConfig.features.gateway) {
|
|
114
|
+
const { DrizzleServiceKeyRepository } = await import("../gateway/service-key-repository.js");
|
|
115
|
+
const serviceKeyRepo = new DrizzleServiceKeyRepository(db);
|
|
116
|
+
gateway = { serviceKeyRepo };
|
|
117
|
+
}
|
|
118
|
+
// hotPool: not yet implemented — needs nemoclaw extraction
|
|
119
|
+
const hotPool = null;
|
|
120
|
+
return {
|
|
121
|
+
db,
|
|
122
|
+
pool,
|
|
123
|
+
productConfig,
|
|
124
|
+
creditLedger,
|
|
125
|
+
orgMemberRepo,
|
|
126
|
+
orgService,
|
|
127
|
+
userRoleRepo,
|
|
128
|
+
fleet,
|
|
129
|
+
crypto,
|
|
130
|
+
stripe,
|
|
131
|
+
gateway,
|
|
132
|
+
hotPool,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platform-core server entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports types, the test helper, and provides bootPlatformServer() —
|
|
5
|
+
* the single-call boot function products use to go from a declarative
|
|
6
|
+
* BootConfig to a running Hono server with DI container.
|
|
7
|
+
*/
|
|
8
|
+
import type { BootConfig, BootResult } from "./boot-config.js";
|
|
9
|
+
export type { BootConfig, BootResult, FeatureFlags, RoutePlugin } from "./boot-config.js";
|
|
10
|
+
export type { CryptoServices, FleetServices, GatewayServices, HotPoolServices, PlatformContainer, StripeServices, } from "./container.js";
|
|
11
|
+
export { buildContainer } from "./container.js";
|
|
12
|
+
export { type BackgroundHandles, gracefulShutdown, startBackgroundServices } from "./lifecycle.js";
|
|
13
|
+
export { type MountConfig, mountRoutes } from "./mount-routes.js";
|
|
14
|
+
export { createTestContainer } from "./test-container.js";
|
|
15
|
+
/**
|
|
16
|
+
* Boot a fully-wired platform server from a declarative config.
|
|
17
|
+
*
|
|
18
|
+
* 1. Builds the DI container (DB, migrations, product config, feature slices)
|
|
19
|
+
* 2. Creates a Hono app and mounts shared routes
|
|
20
|
+
* 3. Returns start/stop lifecycle hooks
|
|
21
|
+
*
|
|
22
|
+
* Products call this from their index.ts:
|
|
23
|
+
* ```ts
|
|
24
|
+
* const { app, container, start, stop } = await bootPlatformServer({
|
|
25
|
+
* slug: "paperclip",
|
|
26
|
+
* databaseUrl: process.env.DATABASE_URL!,
|
|
27
|
+
* provisionSecret: process.env.PROVISION_SECRET!,
|
|
28
|
+
* features: { fleet: true, crypto: true, stripe: true, gateway: true, hotPool: false },
|
|
29
|
+
* });
|
|
30
|
+
* await start();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function bootPlatformServer(config: BootConfig): Promise<BootResult>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platform-core server entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports types, the test helper, and provides bootPlatformServer() —
|
|
5
|
+
* the single-call boot function products use to go from a declarative
|
|
6
|
+
* BootConfig to a running Hono server with DI container.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { buildContainer } from "./container.js";
|
|
10
|
+
import { gracefulShutdown, startBackgroundServices } from "./lifecycle.js";
|
|
11
|
+
import { mountRoutes } from "./mount-routes.js";
|
|
12
|
+
export { buildContainer } from "./container.js";
|
|
13
|
+
export { gracefulShutdown, startBackgroundServices } from "./lifecycle.js";
|
|
14
|
+
export { mountRoutes } from "./mount-routes.js";
|
|
15
|
+
export { createTestContainer } from "./test-container.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// bootPlatformServer
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Boot a fully-wired platform server from a declarative config.
|
|
21
|
+
*
|
|
22
|
+
* 1. Builds the DI container (DB, migrations, product config, feature slices)
|
|
23
|
+
* 2. Creates a Hono app and mounts shared routes
|
|
24
|
+
* 3. Returns start/stop lifecycle hooks
|
|
25
|
+
*
|
|
26
|
+
* Products call this from their index.ts:
|
|
27
|
+
* ```ts
|
|
28
|
+
* const { app, container, start, stop } = await bootPlatformServer({
|
|
29
|
+
* slug: "paperclip",
|
|
30
|
+
* databaseUrl: process.env.DATABASE_URL!,
|
|
31
|
+
* provisionSecret: process.env.PROVISION_SECRET!,
|
|
32
|
+
* features: { fleet: true, crypto: true, stripe: true, gateway: true, hotPool: false },
|
|
33
|
+
* });
|
|
34
|
+
* await start();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function bootPlatformServer(config) {
|
|
38
|
+
const container = await buildContainer(config);
|
|
39
|
+
const app = new Hono();
|
|
40
|
+
mountRoutes(app, container, {
|
|
41
|
+
provisionSecret: config.provisionSecret,
|
|
42
|
+
cryptoServiceKey: config.cryptoServiceKey,
|
|
43
|
+
platformDomain: container.productConfig.product?.domain ?? "localhost",
|
|
44
|
+
}, config.routes);
|
|
45
|
+
let handles = null;
|
|
46
|
+
let server = null;
|
|
47
|
+
return {
|
|
48
|
+
app,
|
|
49
|
+
container,
|
|
50
|
+
start: async (port) => {
|
|
51
|
+
const { serve } = await import("@hono/node-server");
|
|
52
|
+
const listenPort = port ?? config.port ?? 3001;
|
|
53
|
+
const hostname = config.host ?? "0.0.0.0";
|
|
54
|
+
server = serve({ fetch: app.fetch, hostname, port: listenPort }, async () => {
|
|
55
|
+
handles = await startBackgroundServices(container);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
stop: async () => {
|
|
59
|
+
if (server)
|
|
60
|
+
server.close();
|
|
61
|
+
if (handles) {
|
|
62
|
+
await gracefulShutdown(container, handles);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle management — background services and graceful shutdown.
|
|
3
|
+
*
|
|
4
|
+
* Products currently handle background tasks in their serve() callbacks.
|
|
5
|
+
* This module provides a standard interface for starting and stopping
|
|
6
|
+
* those tasks so bootPlatformServer can manage them uniformly.
|
|
7
|
+
*/
|
|
8
|
+
import type { PlatformContainer } from "./container.js";
|
|
9
|
+
export interface BackgroundHandles {
|
|
10
|
+
intervals: ReturnType<typeof setInterval>[];
|
|
11
|
+
unsubscribes: (() => void)[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start background services that run after the server is listening.
|
|
15
|
+
*
|
|
16
|
+
* Currently a thin scaffold — the hooks exist so products can migrate their
|
|
17
|
+
* background tasks (fleet updater, notification worker, caddy hydration,
|
|
18
|
+
* health monitor) incrementally without changing the boot contract.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startBackgroundServices(container: PlatformContainer): Promise<BackgroundHandles>;
|
|
21
|
+
/**
|
|
22
|
+
* Graceful shutdown: clear intervals, call unsubscribe hooks, close the
|
|
23
|
+
* database connection pool.
|
|
24
|
+
*/
|
|
25
|
+
export declare function gracefulShutdown(container: PlatformContainer, handles: BackgroundHandles): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle management — background services and graceful shutdown.
|
|
3
|
+
*
|
|
4
|
+
* Products currently handle background tasks in their serve() callbacks.
|
|
5
|
+
* This module provides a standard interface for starting and stopping
|
|
6
|
+
* those tasks so bootPlatformServer can manage them uniformly.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// startBackgroundServices
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Start background services that run after the server is listening.
|
|
13
|
+
*
|
|
14
|
+
* Currently a thin scaffold — the hooks exist so products can migrate their
|
|
15
|
+
* background tasks (fleet updater, notification worker, caddy hydration,
|
|
16
|
+
* health monitor) incrementally without changing the boot contract.
|
|
17
|
+
*/
|
|
18
|
+
export async function startBackgroundServices(container) {
|
|
19
|
+
const handles = { intervals: [], unsubscribes: [] };
|
|
20
|
+
// Caddy proxy hydration (if fleet + proxy are enabled)
|
|
21
|
+
if (container.fleet?.proxy) {
|
|
22
|
+
try {
|
|
23
|
+
await container.fleet.proxy.start?.();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Non-fatal — proxy sync will retry on next health tick
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return handles;
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// gracefulShutdown
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Graceful shutdown: clear intervals, call unsubscribe hooks, close the
|
|
36
|
+
* database connection pool.
|
|
37
|
+
*/
|
|
38
|
+
export async function gracefulShutdown(container, handles) {
|
|
39
|
+
for (const interval of handles.intervals) {
|
|
40
|
+
clearInterval(interval);
|
|
41
|
+
}
|
|
42
|
+
for (const unsub of handles.unsubscribes) {
|
|
43
|
+
unsub();
|
|
44
|
+
}
|
|
45
|
+
await container.pool.end();
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { createAdminAuthMiddleware } from "../admin-auth.js";
|
|
4
|
+
function createApp(adminApiKey) {
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
app.use("/admin/*", createAdminAuthMiddleware({ adminApiKey }));
|
|
7
|
+
app.get("/admin/status", (c) => c.json({ ok: true }));
|
|
8
|
+
return app;
|
|
9
|
+
}
|
|
10
|
+
describe("createAdminAuthMiddleware", () => {
|
|
11
|
+
const API_KEY = "test-admin-key-abc123";
|
|
12
|
+
it("returns 401 without Authorization header", async () => {
|
|
13
|
+
const app = createApp(API_KEY);
|
|
14
|
+
const res = await app.request("/admin/status");
|
|
15
|
+
expect(res.status).toBe(401);
|
|
16
|
+
const body = await res.json();
|
|
17
|
+
expect(body.error).toContain("Unauthorized");
|
|
18
|
+
});
|
|
19
|
+
it("returns 401 with wrong API key", async () => {
|
|
20
|
+
const app = createApp(API_KEY);
|
|
21
|
+
const res = await app.request("/admin/status", {
|
|
22
|
+
headers: { authorization: "Bearer wrong-key" },
|
|
23
|
+
});
|
|
24
|
+
expect(res.status).toBe(401);
|
|
25
|
+
const body = await res.json();
|
|
26
|
+
expect(body.error).toContain("invalid admin credentials");
|
|
27
|
+
});
|
|
28
|
+
it("passes through with correct API key (timing-safe)", async () => {
|
|
29
|
+
const app = createApp(API_KEY);
|
|
30
|
+
const res = await app.request("/admin/status", {
|
|
31
|
+
headers: { authorization: `Bearer ${API_KEY}` },
|
|
32
|
+
});
|
|
33
|
+
expect(res.status).toBe(200);
|
|
34
|
+
const body = await res.json();
|
|
35
|
+
expect(body.ok).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it("rejects keys of different length (timing-safe length check)", async () => {
|
|
38
|
+
const app = createApp(API_KEY);
|
|
39
|
+
// Key with same prefix but different length
|
|
40
|
+
const res = await app.request("/admin/status", {
|
|
41
|
+
headers: { authorization: `Bearer ${API_KEY}extra` },
|
|
42
|
+
});
|
|
43
|
+
expect(res.status).toBe(401);
|
|
44
|
+
});
|
|
45
|
+
it("returns 503 when admin key is not configured (fail-closed)", async () => {
|
|
46
|
+
const app = createApp("");
|
|
47
|
+
const res = await app.request("/admin/status", {
|
|
48
|
+
headers: { authorization: "Bearer anything" },
|
|
49
|
+
});
|
|
50
|
+
expect(res.status).toBe(503);
|
|
51
|
+
});
|
|
52
|
+
it("rejects non-Bearer auth schemes", async () => {
|
|
53
|
+
const app = createApp(API_KEY);
|
|
54
|
+
const res = await app.request("/admin/status", {
|
|
55
|
+
headers: { authorization: `Basic ${API_KEY}` },
|
|
56
|
+
});
|
|
57
|
+
expect(res.status).toBe(401);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|