@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.
@@ -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
- const hexPubKey = `0x${Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("")}`;
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
- * Returns the service (for tRPC router wiring) and the resolved config
25
- * (for CORS, email, fleet, auth initialization).
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
- * Returns the service (for tRPC router wiring) and the resolved config
9
- * (for CORS, email, fleet, auth initialization).
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
- const config = await service.getBySlug(slug);
16
+ let config = await service.getBySlug(slug);
17
+ let seeded = false;
25
18
  if (!config) {
26
- throw new Error(`Product "${slug}" not found in database. Run the seed script: DATABASE_URL=... npx tsx scripts/seed-products.ts`);
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.60.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
- const hexPubKey = `0x${Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`;
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
- * Returns the service (for tRPC router wiring) and the resolved config
30
- * (for CORS, email, fleet, auth initialization).
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
- const config = await service.getBySlug(slug);
41
+ let config = await service.getBySlug(slug);
42
+ let seeded = false;
43
+
48
44
  if (!config) {
49
- throw new Error(
50
- `Product "${slug}" not found in database. Run the seed script: DATABASE_URL=... npx tsx scripts/seed-products.ts`,
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
+ };