@wopr-network/platform-core 1.60.1 → 1.61.1
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/billing/crypto/address-gen.js +8 -1
- package/dist/product-config/boot.d.ts +4 -11
- package/dist/product-config/boot.js +28 -14
- package/dist/product-config/index.d.ts +2 -0
- package/dist/product-config/index.js +1 -0
- package/dist/product-config/presets.d.ts +36 -0
- package/dist/product-config/presets.js +134 -0
- package/package.json +2 -1
- package/src/billing/crypto/address-gen.ts +9 -1
- package/src/product-config/boot.ts +37 -15
- package/src/product-config/index.ts +2 -0
- package/src/product-config/presets.ts +169 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - p2pkh: DOGE (D...), TRON (T...) — params: { version }
|
|
11
11
|
* - evm: ETH, ERC-20 (0x...) — params: {}
|
|
12
12
|
*/
|
|
13
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
13
14
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
14
15
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
15
16
|
import { bech32 } from "@scure/base";
|
|
@@ -53,7 +54,13 @@ function encodeP2pkh(pubkey, versionByte) {
|
|
|
53
54
|
return base58encode(full);
|
|
54
55
|
}
|
|
55
56
|
function encodeEvm(pubkey) {
|
|
56
|
-
|
|
57
|
+
// HDKey.publicKey is SEC1 compressed (33 bytes, 02/03 prefix).
|
|
58
|
+
// Ethereum addresses = keccak256(uncompressed_pubkey[1:]).slice(-20).
|
|
59
|
+
// viem's publicKeyToAddress expects uncompressed (65 bytes, 04 prefix).
|
|
60
|
+
// Decompress via secp256k1 point recovery before hashing.
|
|
61
|
+
const hexKey = Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
const uncompressed = secp256k1.Point.fromHex(hexKey).toBytes(false);
|
|
63
|
+
const hexPubKey = `0x${Array.from(uncompressed, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
57
64
|
return publicKeyToAddress(hexPubKey);
|
|
58
65
|
}
|
|
59
66
|
// ---------- public API ----------
|
|
@@ -8,6 +8,8 @@ export interface PlatformBootResult {
|
|
|
8
8
|
config: ProductConfig;
|
|
9
9
|
/** CORS origins derived from product domains + optional dev origins. */
|
|
10
10
|
corsOrigins: string[];
|
|
11
|
+
/** Whether the product was auto-seeded from built-in presets. */
|
|
12
|
+
seeded: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface PlatformBootOptions {
|
|
13
15
|
/** Product slug (e.g. "paperclip", "wopr", "holyship", "nemoclaw"). */
|
|
@@ -21,16 +23,7 @@ export interface PlatformBootOptions {
|
|
|
21
23
|
* Bootstrap product configuration from DB.
|
|
22
24
|
*
|
|
23
25
|
* Call once at startup, after DB + migrations, before route registration.
|
|
24
|
-
*
|
|
25
|
-
* (
|
|
26
|
-
*
|
|
27
|
-
* This replaces: BRAND_NAME, PLATFORM_DOMAIN, UI_ORIGIN, FROM_EMAIL,
|
|
28
|
-
* SUPPORT_EMAIL, COOKIE_DOMAIN, APP_BASE_URL, and all other product-
|
|
29
|
-
* specific env vars that platform-core modules previously read from
|
|
30
|
-
* process.env.
|
|
31
|
-
*
|
|
32
|
-
* Product backends still own their specific wiring (crypto watchers,
|
|
33
|
-
* fleet updaters, notification pipelines). platformBoot handles the
|
|
34
|
-
* config-driven parts that are identical across products.
|
|
26
|
+
* If the product doesn't exist in the DB yet, auto-seeds from built-in
|
|
27
|
+
* presets (zero manual steps on first deploy).
|
|
35
28
|
*/
|
|
36
29
|
export declare function platformBoot(opts: PlatformBootOptions): Promise<PlatformBootResult>;
|
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
import { DrizzleProductConfigRepository } from "./drizzle-product-config-repository.js";
|
|
2
|
+
import { PRODUCT_PRESETS } from "./presets.js";
|
|
2
3
|
import { deriveCorsOrigins } from "./repository-types.js";
|
|
3
4
|
import { ProductConfigService } from "./service.js";
|
|
4
5
|
/**
|
|
5
6
|
* Bootstrap product configuration from DB.
|
|
6
7
|
*
|
|
7
8
|
* Call once at startup, after DB + migrations, before route registration.
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
* This replaces: BRAND_NAME, PLATFORM_DOMAIN, UI_ORIGIN, FROM_EMAIL,
|
|
12
|
-
* SUPPORT_EMAIL, COOKIE_DOMAIN, APP_BASE_URL, and all other product-
|
|
13
|
-
* specific env vars that platform-core modules previously read from
|
|
14
|
-
* process.env.
|
|
15
|
-
*
|
|
16
|
-
* Product backends still own their specific wiring (crypto watchers,
|
|
17
|
-
* fleet updaters, notification pipelines). platformBoot handles the
|
|
18
|
-
* config-driven parts that are identical across products.
|
|
9
|
+
* If the product doesn't exist in the DB yet, auto-seeds from built-in
|
|
10
|
+
* presets (zero manual steps on first deploy).
|
|
19
11
|
*/
|
|
20
12
|
export async function platformBoot(opts) {
|
|
21
13
|
const { slug, db, devOrigins = [] } = opts;
|
|
22
14
|
const repo = new DrizzleProductConfigRepository(db);
|
|
23
15
|
const service = new ProductConfigService(repo);
|
|
24
|
-
|
|
16
|
+
let config = await service.getBySlug(slug);
|
|
17
|
+
let seeded = false;
|
|
25
18
|
if (!config) {
|
|
26
|
-
|
|
19
|
+
const preset = PRODUCT_PRESETS[slug];
|
|
20
|
+
if (!preset) {
|
|
21
|
+
throw new Error(`Product "${slug}" not found in database and no built-in preset exists. Known presets: ${Object.keys(PRODUCT_PRESETS).join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
// Auto-seed from preset
|
|
24
|
+
const { navItems, fleet, ...productData } = preset;
|
|
25
|
+
const product = await repo.upsertProduct(slug, productData);
|
|
26
|
+
await repo.replaceNavItems(product.id, navItems.map((item) => ({
|
|
27
|
+
label: item.label,
|
|
28
|
+
href: item.href,
|
|
29
|
+
sortOrder: item.sortOrder,
|
|
30
|
+
requiresRole: item.requiresRole,
|
|
31
|
+
enabled: true,
|
|
32
|
+
})));
|
|
33
|
+
await repo.upsertFleetConfig(product.id, fleet);
|
|
34
|
+
await repo.upsertFeatures(product.id, {});
|
|
35
|
+
// Re-fetch to get the complete config
|
|
36
|
+
config = await service.getBySlug(slug);
|
|
37
|
+
if (!config) {
|
|
38
|
+
throw new Error(`Failed to seed product "${slug}" — database write succeeded but read returned null`);
|
|
39
|
+
}
|
|
40
|
+
seeded = true;
|
|
27
41
|
}
|
|
28
42
|
const corsOrigins = [...deriveCorsOrigins(config.product, config.domains), ...devOrigins];
|
|
29
|
-
return { service, config, corsOrigins };
|
|
43
|
+
return { service, config, corsOrigins, seeded };
|
|
30
44
|
}
|
|
@@ -3,6 +3,8 @@ import type { PlatformBootOptions, PlatformBootResult } from "./boot.js";
|
|
|
3
3
|
import type { IProductConfigRepository } from "./repository-types.js";
|
|
4
4
|
import { ProductConfigService } from "./service.js";
|
|
5
5
|
export type { PlatformBootOptions, PlatformBootResult } from "./boot.js";
|
|
6
|
+
export type { ProductPreset } from "./presets.js";
|
|
7
|
+
export { PRODUCT_PRESETS } from "./presets.js";
|
|
6
8
|
/**
|
|
7
9
|
* Bootstrap product config from DB and register the service globally.
|
|
8
10
|
* Wraps the raw boot to ensure getProductConfigService() works after calling this.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { platformBoot as rawPlatformBoot } from "./boot.js";
|
|
2
2
|
import { DrizzleProductConfigRepository } from "./drizzle-product-config-repository.js";
|
|
3
3
|
import { ProductConfigService } from "./service.js";
|
|
4
|
+
export { PRODUCT_PRESETS } from "./presets.js";
|
|
4
5
|
/**
|
|
5
6
|
* Bootstrap product config from DB and register the service globally.
|
|
6
7
|
* Wraps the raw boot to ensure getProductConfigService() works after calling this.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in product presets.
|
|
3
|
+
* Used by platformBoot() for auto-seeding and by scripts/seed-products.ts.
|
|
4
|
+
*/
|
|
5
|
+
export interface NavItemPreset {
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
sortOrder: number;
|
|
9
|
+
requiresRole?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FleetPreset {
|
|
12
|
+
containerImage: string;
|
|
13
|
+
lifecycle: "managed" | "ephemeral";
|
|
14
|
+
billingModel: "monthly" | "per_use" | "none";
|
|
15
|
+
maxInstances: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ProductPreset {
|
|
18
|
+
brandName: string;
|
|
19
|
+
productName: string;
|
|
20
|
+
tagline: string;
|
|
21
|
+
domain: string;
|
|
22
|
+
appDomain: string;
|
|
23
|
+
cookieDomain: string;
|
|
24
|
+
companyLegal: string;
|
|
25
|
+
priceLabel: string;
|
|
26
|
+
defaultImage: string;
|
|
27
|
+
emailSupport: string;
|
|
28
|
+
emailPrivacy: string;
|
|
29
|
+
emailLegal: string;
|
|
30
|
+
fromEmail: string;
|
|
31
|
+
homePath: string;
|
|
32
|
+
storagePrefix: string;
|
|
33
|
+
navItems: NavItemPreset[];
|
|
34
|
+
fleet: FleetPreset;
|
|
35
|
+
}
|
|
36
|
+
export declare const PRODUCT_PRESETS: Record<string, ProductPreset>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in product presets.
|
|
3
|
+
* Used by platformBoot() for auto-seeding and by scripts/seed-products.ts.
|
|
4
|
+
*/
|
|
5
|
+
export const PRODUCT_PRESETS = {
|
|
6
|
+
wopr: {
|
|
7
|
+
brandName: "WOPR",
|
|
8
|
+
productName: "WOPR Bot",
|
|
9
|
+
tagline: "A $5/month supercomputer that manages your business.",
|
|
10
|
+
domain: "wopr.bot",
|
|
11
|
+
appDomain: "app.wopr.bot",
|
|
12
|
+
cookieDomain: ".wopr.bot",
|
|
13
|
+
companyLegal: "WOPR Network Inc.",
|
|
14
|
+
priceLabel: "$5/month",
|
|
15
|
+
defaultImage: "ghcr.io/wopr-network/wopr:latest",
|
|
16
|
+
emailSupport: "support@wopr.bot",
|
|
17
|
+
emailPrivacy: "privacy@wopr.bot",
|
|
18
|
+
emailLegal: "legal@wopr.bot",
|
|
19
|
+
fromEmail: "noreply@wopr.bot",
|
|
20
|
+
homePath: "/marketplace",
|
|
21
|
+
storagePrefix: "wopr",
|
|
22
|
+
navItems: [
|
|
23
|
+
{ label: "Dashboard", href: "/dashboard", sortOrder: 0 },
|
|
24
|
+
{ label: "Chat", href: "/chat", sortOrder: 1 },
|
|
25
|
+
{ label: "Marketplace", href: "/marketplace", sortOrder: 2 },
|
|
26
|
+
{ label: "Channels", href: "/channels", sortOrder: 3 },
|
|
27
|
+
{ label: "Plugins", href: "/plugins", sortOrder: 4 },
|
|
28
|
+
{ label: "Instances", href: "/instances", sortOrder: 5 },
|
|
29
|
+
{ label: "Changesets", href: "/changesets", sortOrder: 6 },
|
|
30
|
+
{ label: "Network", href: "/dashboard/network", sortOrder: 7 },
|
|
31
|
+
{ label: "Fleet Health", href: "/fleet/health", sortOrder: 8 },
|
|
32
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 9 },
|
|
33
|
+
{ label: "Billing", href: "/billing/plans", sortOrder: 10 },
|
|
34
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 11 },
|
|
35
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 12, requiresRole: "platform_admin" },
|
|
36
|
+
],
|
|
37
|
+
fleet: {
|
|
38
|
+
containerImage: "ghcr.io/wopr-network/wopr:latest",
|
|
39
|
+
lifecycle: "managed",
|
|
40
|
+
billingModel: "monthly",
|
|
41
|
+
maxInstances: 5,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
paperclip: {
|
|
45
|
+
brandName: "Paperclip",
|
|
46
|
+
productName: "Paperclip",
|
|
47
|
+
tagline: "AI agents that run your business.",
|
|
48
|
+
domain: "runpaperclip.com",
|
|
49
|
+
appDomain: "app.runpaperclip.com",
|
|
50
|
+
cookieDomain: ".runpaperclip.com",
|
|
51
|
+
companyLegal: "Paperclip AI Inc.",
|
|
52
|
+
priceLabel: "$5/month",
|
|
53
|
+
defaultImage: "ghcr.io/wopr-network/paperclip:managed",
|
|
54
|
+
emailSupport: "support@runpaperclip.com",
|
|
55
|
+
emailPrivacy: "privacy@runpaperclip.com",
|
|
56
|
+
emailLegal: "legal@runpaperclip.com",
|
|
57
|
+
fromEmail: "noreply@runpaperclip.com",
|
|
58
|
+
homePath: "/instances",
|
|
59
|
+
storagePrefix: "paperclip",
|
|
60
|
+
navItems: [
|
|
61
|
+
{ label: "Instances", href: "/instances", sortOrder: 0 },
|
|
62
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 1 },
|
|
63
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 2 },
|
|
64
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 3, requiresRole: "platform_admin" },
|
|
65
|
+
],
|
|
66
|
+
fleet: {
|
|
67
|
+
containerImage: "ghcr.io/wopr-network/paperclip:managed",
|
|
68
|
+
lifecycle: "managed",
|
|
69
|
+
billingModel: "monthly",
|
|
70
|
+
maxInstances: 5,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
holyship: {
|
|
74
|
+
brandName: "Holy Ship",
|
|
75
|
+
productName: "Holy Ship",
|
|
76
|
+
tagline: "Ship it.",
|
|
77
|
+
domain: "holyship.wtf",
|
|
78
|
+
appDomain: "app.holyship.wtf",
|
|
79
|
+
cookieDomain: ".holyship.wtf",
|
|
80
|
+
companyLegal: "WOPR Network Inc.",
|
|
81
|
+
priceLabel: "",
|
|
82
|
+
defaultImage: "ghcr.io/wopr-network/holyship:latest",
|
|
83
|
+
emailSupport: "support@holyship.wtf",
|
|
84
|
+
emailPrivacy: "privacy@holyship.wtf",
|
|
85
|
+
emailLegal: "legal@holyship.wtf",
|
|
86
|
+
fromEmail: "noreply@holyship.wtf",
|
|
87
|
+
homePath: "/dashboard",
|
|
88
|
+
storagePrefix: "holyship",
|
|
89
|
+
navItems: [
|
|
90
|
+
{ label: "Dashboard", href: "/dashboard", sortOrder: 0 },
|
|
91
|
+
{ label: "Ship", href: "/ship", sortOrder: 1 },
|
|
92
|
+
{ label: "Approvals", href: "/approvals", sortOrder: 2 },
|
|
93
|
+
{ label: "Connect", href: "/connect", sortOrder: 3 },
|
|
94
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 4 },
|
|
95
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 5 },
|
|
96
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 6, requiresRole: "platform_admin" },
|
|
97
|
+
],
|
|
98
|
+
fleet: {
|
|
99
|
+
containerImage: "ghcr.io/wopr-network/holyship:latest",
|
|
100
|
+
lifecycle: "ephemeral",
|
|
101
|
+
billingModel: "none",
|
|
102
|
+
maxInstances: 50,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
nemoclaw: {
|
|
106
|
+
brandName: "NemoPod",
|
|
107
|
+
productName: "NemoPod",
|
|
108
|
+
tagline: "NVIDIA NeMo, one click away",
|
|
109
|
+
domain: "nemopod.com",
|
|
110
|
+
appDomain: "app.nemopod.com",
|
|
111
|
+
cookieDomain: ".nemopod.com",
|
|
112
|
+
companyLegal: "WOPR Network Inc.",
|
|
113
|
+
priceLabel: "$5 free credits",
|
|
114
|
+
defaultImage: "ghcr.io/wopr-network/nemoclaw:latest",
|
|
115
|
+
emailSupport: "support@nemopod.com",
|
|
116
|
+
emailPrivacy: "privacy@nemopod.com",
|
|
117
|
+
emailLegal: "legal@nemopod.com",
|
|
118
|
+
fromEmail: "noreply@nemopod.com",
|
|
119
|
+
homePath: "/instances",
|
|
120
|
+
storagePrefix: "nemopod",
|
|
121
|
+
navItems: [
|
|
122
|
+
{ label: "NemoClaws", href: "/instances", sortOrder: 0 },
|
|
123
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 1 },
|
|
124
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 2 },
|
|
125
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 3, requiresRole: "platform_admin" },
|
|
126
|
+
],
|
|
127
|
+
fleet: {
|
|
128
|
+
containerImage: "ghcr.io/wopr-network/nemoclaw:latest",
|
|
129
|
+
lifecycle: "managed",
|
|
130
|
+
billingModel: "monthly",
|
|
131
|
+
maxInstances: 5,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"dependencies": {
|
|
132
132
|
"@aws-sdk/client-ses": "^3.1014.0",
|
|
133
133
|
"@hono/node-server": "^1.19.11",
|
|
134
|
+
"@noble/curves": "^2.0.1",
|
|
134
135
|
"@noble/hashes": "^2.0.1",
|
|
135
136
|
"@scure/base": "^2.0.0",
|
|
136
137
|
"@scure/bip32": "^2.0.1",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - p2pkh: DOGE (D...), TRON (T...) — params: { version }
|
|
11
11
|
* - evm: ETH, ERC-20 (0x...) — params: {}
|
|
12
12
|
*/
|
|
13
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
13
14
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
14
15
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
15
16
|
import { bech32 } from "@scure/base";
|
|
@@ -64,7 +65,14 @@ function encodeP2pkh(pubkey: Uint8Array, versionByte: number): string {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function encodeEvm(pubkey: Uint8Array): string {
|
|
67
|
-
|
|
68
|
+
// HDKey.publicKey is SEC1 compressed (33 bytes, 02/03 prefix).
|
|
69
|
+
// Ethereum addresses = keccak256(uncompressed_pubkey[1:]).slice(-20).
|
|
70
|
+
// viem's publicKeyToAddress expects uncompressed (65 bytes, 04 prefix).
|
|
71
|
+
// Decompress via secp256k1 point recovery before hashing.
|
|
72
|
+
const hexKey = Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
73
|
+
const uncompressed = secp256k1.Point.fromHex(hexKey).toBytes(false);
|
|
74
|
+
const hexPubKey =
|
|
75
|
+
`0x${Array.from(uncompressed, (b: number) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`;
|
|
68
76
|
return publicKeyToAddress(hexPubKey);
|
|
69
77
|
}
|
|
70
78
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DrizzleDb } from "../db/index.js";
|
|
2
2
|
import { DrizzleProductConfigRepository } from "./drizzle-product-config-repository.js";
|
|
3
|
+
import { PRODUCT_PRESETS } from "./presets.js";
|
|
3
4
|
import type { ProductConfig } from "./repository-types.js";
|
|
4
5
|
import { deriveCorsOrigins } from "./repository-types.js";
|
|
5
6
|
import { ProductConfigService } from "./service.js";
|
|
@@ -11,6 +12,8 @@ export interface PlatformBootResult {
|
|
|
11
12
|
config: ProductConfig;
|
|
12
13
|
/** CORS origins derived from product domains + optional dev origins. */
|
|
13
14
|
corsOrigins: string[];
|
|
15
|
+
/** Whether the product was auto-seeded from built-in presets. */
|
|
16
|
+
seeded: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface PlatformBootOptions {
|
|
@@ -26,17 +29,8 @@ export interface PlatformBootOptions {
|
|
|
26
29
|
* Bootstrap product configuration from DB.
|
|
27
30
|
*
|
|
28
31
|
* Call once at startup, after DB + migrations, before route registration.
|
|
29
|
-
*
|
|
30
|
-
* (
|
|
31
|
-
*
|
|
32
|
-
* This replaces: BRAND_NAME, PLATFORM_DOMAIN, UI_ORIGIN, FROM_EMAIL,
|
|
33
|
-
* SUPPORT_EMAIL, COOKIE_DOMAIN, APP_BASE_URL, and all other product-
|
|
34
|
-
* specific env vars that platform-core modules previously read from
|
|
35
|
-
* process.env.
|
|
36
|
-
*
|
|
37
|
-
* Product backends still own their specific wiring (crypto watchers,
|
|
38
|
-
* fleet updaters, notification pipelines). platformBoot handles the
|
|
39
|
-
* config-driven parts that are identical across products.
|
|
32
|
+
* If the product doesn't exist in the DB yet, auto-seeds from built-in
|
|
33
|
+
* presets (zero manual steps on first deploy).
|
|
40
34
|
*/
|
|
41
35
|
export async function platformBoot(opts: PlatformBootOptions): Promise<PlatformBootResult> {
|
|
42
36
|
const { slug, db, devOrigins = [] } = opts;
|
|
@@ -44,14 +38,42 @@ export async function platformBoot(opts: PlatformBootOptions): Promise<PlatformB
|
|
|
44
38
|
const repo = new DrizzleProductConfigRepository(db);
|
|
45
39
|
const service = new ProductConfigService(repo);
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
let config = await service.getBySlug(slug);
|
|
42
|
+
let seeded = false;
|
|
43
|
+
|
|
48
44
|
if (!config) {
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
const preset = PRODUCT_PRESETS[slug];
|
|
46
|
+
if (!preset) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Product "${slug}" not found in database and no built-in preset exists. Known presets: ${Object.keys(PRODUCT_PRESETS).join(", ")}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Auto-seed from preset
|
|
53
|
+
const { navItems, fleet, ...productData } = preset;
|
|
54
|
+
const product = await repo.upsertProduct(slug, productData);
|
|
55
|
+
await repo.replaceNavItems(
|
|
56
|
+
product.id,
|
|
57
|
+
navItems.map((item) => ({
|
|
58
|
+
label: item.label,
|
|
59
|
+
href: item.href,
|
|
60
|
+
sortOrder: item.sortOrder,
|
|
61
|
+
requiresRole: item.requiresRole,
|
|
62
|
+
enabled: true,
|
|
63
|
+
})),
|
|
51
64
|
);
|
|
65
|
+
await repo.upsertFleetConfig(product.id, fleet);
|
|
66
|
+
await repo.upsertFeatures(product.id, {});
|
|
67
|
+
|
|
68
|
+
// Re-fetch to get the complete config
|
|
69
|
+
config = await service.getBySlug(slug);
|
|
70
|
+
if (!config) {
|
|
71
|
+
throw new Error(`Failed to seed product "${slug}" — database write succeeded but read returned null`);
|
|
72
|
+
}
|
|
73
|
+
seeded = true;
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
const corsOrigins = [...deriveCorsOrigins(config.product, config.domains), ...devOrigins];
|
|
55
77
|
|
|
56
|
-
return { service, config, corsOrigins };
|
|
78
|
+
return { service, config, corsOrigins, seeded };
|
|
57
79
|
}
|
|
@@ -6,6 +6,8 @@ import type { IProductConfigRepository } from "./repository-types.js";
|
|
|
6
6
|
import { ProductConfigService } from "./service.js";
|
|
7
7
|
|
|
8
8
|
export type { PlatformBootOptions, PlatformBootResult } from "./boot.js";
|
|
9
|
+
export type { ProductPreset } from "./presets.js";
|
|
10
|
+
export { PRODUCT_PRESETS } from "./presets.js";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Bootstrap product config from DB and register the service globally.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in product presets.
|
|
3
|
+
* Used by platformBoot() for auto-seeding and by scripts/seed-products.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface NavItemPreset {
|
|
7
|
+
label: string;
|
|
8
|
+
href: string;
|
|
9
|
+
sortOrder: number;
|
|
10
|
+
requiresRole?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FleetPreset {
|
|
14
|
+
containerImage: string;
|
|
15
|
+
lifecycle: "managed" | "ephemeral";
|
|
16
|
+
billingModel: "monthly" | "per_use" | "none";
|
|
17
|
+
maxInstances: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProductPreset {
|
|
21
|
+
brandName: string;
|
|
22
|
+
productName: string;
|
|
23
|
+
tagline: string;
|
|
24
|
+
domain: string;
|
|
25
|
+
appDomain: string;
|
|
26
|
+
cookieDomain: string;
|
|
27
|
+
companyLegal: string;
|
|
28
|
+
priceLabel: string;
|
|
29
|
+
defaultImage: string;
|
|
30
|
+
emailSupport: string;
|
|
31
|
+
emailPrivacy: string;
|
|
32
|
+
emailLegal: string;
|
|
33
|
+
fromEmail: string;
|
|
34
|
+
homePath: string;
|
|
35
|
+
storagePrefix: string;
|
|
36
|
+
navItems: NavItemPreset[];
|
|
37
|
+
fleet: FleetPreset;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const PRODUCT_PRESETS: Record<string, ProductPreset> = {
|
|
41
|
+
wopr: {
|
|
42
|
+
brandName: "WOPR",
|
|
43
|
+
productName: "WOPR Bot",
|
|
44
|
+
tagline: "A $5/month supercomputer that manages your business.",
|
|
45
|
+
domain: "wopr.bot",
|
|
46
|
+
appDomain: "app.wopr.bot",
|
|
47
|
+
cookieDomain: ".wopr.bot",
|
|
48
|
+
companyLegal: "WOPR Network Inc.",
|
|
49
|
+
priceLabel: "$5/month",
|
|
50
|
+
defaultImage: "ghcr.io/wopr-network/wopr:latest",
|
|
51
|
+
emailSupport: "support@wopr.bot",
|
|
52
|
+
emailPrivacy: "privacy@wopr.bot",
|
|
53
|
+
emailLegal: "legal@wopr.bot",
|
|
54
|
+
fromEmail: "noreply@wopr.bot",
|
|
55
|
+
homePath: "/marketplace",
|
|
56
|
+
storagePrefix: "wopr",
|
|
57
|
+
navItems: [
|
|
58
|
+
{ label: "Dashboard", href: "/dashboard", sortOrder: 0 },
|
|
59
|
+
{ label: "Chat", href: "/chat", sortOrder: 1 },
|
|
60
|
+
{ label: "Marketplace", href: "/marketplace", sortOrder: 2 },
|
|
61
|
+
{ label: "Channels", href: "/channels", sortOrder: 3 },
|
|
62
|
+
{ label: "Plugins", href: "/plugins", sortOrder: 4 },
|
|
63
|
+
{ label: "Instances", href: "/instances", sortOrder: 5 },
|
|
64
|
+
{ label: "Changesets", href: "/changesets", sortOrder: 6 },
|
|
65
|
+
{ label: "Network", href: "/dashboard/network", sortOrder: 7 },
|
|
66
|
+
{ label: "Fleet Health", href: "/fleet/health", sortOrder: 8 },
|
|
67
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 9 },
|
|
68
|
+
{ label: "Billing", href: "/billing/plans", sortOrder: 10 },
|
|
69
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 11 },
|
|
70
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 12, requiresRole: "platform_admin" },
|
|
71
|
+
],
|
|
72
|
+
fleet: {
|
|
73
|
+
containerImage: "ghcr.io/wopr-network/wopr:latest",
|
|
74
|
+
lifecycle: "managed",
|
|
75
|
+
billingModel: "monthly",
|
|
76
|
+
maxInstances: 5,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
paperclip: {
|
|
80
|
+
brandName: "Paperclip",
|
|
81
|
+
productName: "Paperclip",
|
|
82
|
+
tagline: "AI agents that run your business.",
|
|
83
|
+
domain: "runpaperclip.com",
|
|
84
|
+
appDomain: "app.runpaperclip.com",
|
|
85
|
+
cookieDomain: ".runpaperclip.com",
|
|
86
|
+
companyLegal: "Paperclip AI Inc.",
|
|
87
|
+
priceLabel: "$5/month",
|
|
88
|
+
defaultImage: "ghcr.io/wopr-network/paperclip:managed",
|
|
89
|
+
emailSupport: "support@runpaperclip.com",
|
|
90
|
+
emailPrivacy: "privacy@runpaperclip.com",
|
|
91
|
+
emailLegal: "legal@runpaperclip.com",
|
|
92
|
+
fromEmail: "noreply@runpaperclip.com",
|
|
93
|
+
homePath: "/instances",
|
|
94
|
+
storagePrefix: "paperclip",
|
|
95
|
+
navItems: [
|
|
96
|
+
{ label: "Instances", href: "/instances", sortOrder: 0 },
|
|
97
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 1 },
|
|
98
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 2 },
|
|
99
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 3, requiresRole: "platform_admin" },
|
|
100
|
+
],
|
|
101
|
+
fleet: {
|
|
102
|
+
containerImage: "ghcr.io/wopr-network/paperclip:managed",
|
|
103
|
+
lifecycle: "managed",
|
|
104
|
+
billingModel: "monthly",
|
|
105
|
+
maxInstances: 5,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
holyship: {
|
|
109
|
+
brandName: "Holy Ship",
|
|
110
|
+
productName: "Holy Ship",
|
|
111
|
+
tagline: "Ship it.",
|
|
112
|
+
domain: "holyship.wtf",
|
|
113
|
+
appDomain: "app.holyship.wtf",
|
|
114
|
+
cookieDomain: ".holyship.wtf",
|
|
115
|
+
companyLegal: "WOPR Network Inc.",
|
|
116
|
+
priceLabel: "",
|
|
117
|
+
defaultImage: "ghcr.io/wopr-network/holyship:latest",
|
|
118
|
+
emailSupport: "support@holyship.wtf",
|
|
119
|
+
emailPrivacy: "privacy@holyship.wtf",
|
|
120
|
+
emailLegal: "legal@holyship.wtf",
|
|
121
|
+
fromEmail: "noreply@holyship.wtf",
|
|
122
|
+
homePath: "/dashboard",
|
|
123
|
+
storagePrefix: "holyship",
|
|
124
|
+
navItems: [
|
|
125
|
+
{ label: "Dashboard", href: "/dashboard", sortOrder: 0 },
|
|
126
|
+
{ label: "Ship", href: "/ship", sortOrder: 1 },
|
|
127
|
+
{ label: "Approvals", href: "/approvals", sortOrder: 2 },
|
|
128
|
+
{ label: "Connect", href: "/connect", sortOrder: 3 },
|
|
129
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 4 },
|
|
130
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 5 },
|
|
131
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 6, requiresRole: "platform_admin" },
|
|
132
|
+
],
|
|
133
|
+
fleet: {
|
|
134
|
+
containerImage: "ghcr.io/wopr-network/holyship:latest",
|
|
135
|
+
lifecycle: "ephemeral",
|
|
136
|
+
billingModel: "none",
|
|
137
|
+
maxInstances: 50,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
nemoclaw: {
|
|
141
|
+
brandName: "NemoPod",
|
|
142
|
+
productName: "NemoPod",
|
|
143
|
+
tagline: "NVIDIA NeMo, one click away",
|
|
144
|
+
domain: "nemopod.com",
|
|
145
|
+
appDomain: "app.nemopod.com",
|
|
146
|
+
cookieDomain: ".nemopod.com",
|
|
147
|
+
companyLegal: "WOPR Network Inc.",
|
|
148
|
+
priceLabel: "$5 free credits",
|
|
149
|
+
defaultImage: "ghcr.io/wopr-network/nemoclaw:latest",
|
|
150
|
+
emailSupport: "support@nemopod.com",
|
|
151
|
+
emailPrivacy: "privacy@nemopod.com",
|
|
152
|
+
emailLegal: "legal@nemopod.com",
|
|
153
|
+
fromEmail: "noreply@nemopod.com",
|
|
154
|
+
homePath: "/instances",
|
|
155
|
+
storagePrefix: "nemopod",
|
|
156
|
+
navItems: [
|
|
157
|
+
{ label: "NemoClaws", href: "/instances", sortOrder: 0 },
|
|
158
|
+
{ label: "Credits", href: "/billing/credits", sortOrder: 1 },
|
|
159
|
+
{ label: "Settings", href: "/settings/profile", sortOrder: 2 },
|
|
160
|
+
{ label: "Admin", href: "/admin/tenants", sortOrder: 3, requiresRole: "platform_admin" },
|
|
161
|
+
],
|
|
162
|
+
fleet: {
|
|
163
|
+
containerImage: "ghcr.io/wopr-network/nemoclaw:latest",
|
|
164
|
+
lifecycle: "managed",
|
|
165
|
+
billingModel: "monthly",
|
|
166
|
+
maxInstances: 5,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|