Fork Horse repository, listed by its owner. Publisher snapshot.
scripts/core-deployment.ts
Snapshot at 34e3175
1import type { BaseConfigs, DeploymentConfig, ProdWranglerConfig } from "./deployment-config.ts";
2
3/** Optional public core; owner values belong only in deployment.local.jsonc. */
4export interface CoreDeployment {
5 enabled: boolean;
6 workerName: string;
7 hostname: string;
8 mcpPath: string;
9 project: string;
10 artifactsNamespace: string;
11 oauthKvNamespaceId: string | null;
12 pushQueue: string;
13 pushQueueId: string;
14 secretsFile?: string | null;
15 deadLetterQueue: string;
16 access: { issuer: string; audience: string; owners: string[] };
17}
18
19export function validateCore(config: DeploymentConfig): void {
20 const core = config.core;
21 if (core === undefined) return;
22 if (!core || typeof core.enabled !== "boolean")
23 throw new Error("core.enabled must be a boolean.");
24 if (!core.enabled) return;
25 const name = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
26 if (
27 ![core.workerName, core.pushQueue, core.deadLetterQueue].every(
28 (value) => typeof value === "string" && name.test(value),
29 )
30 )
31 throw new Error("core Worker and Queue names must use lowercase letters, numbers and hyphens.");
32 if (
33 Object.entries(config.workers).some(
34 ([key, worker]) =>
35 key !== "customGatekeeper" &&
36 (key !== "errorReporter" || config.errorReporting.enabled) &&
37 worker.name === core.workerName,
38 )
39 )
40 throw new Error("core.workerName must be unique.");
41 if (typeof core.pushQueueId !== "string" || !/^[a-f0-9]{32}$/i.test(core.pushQueueId))
42 throw new Error("core.pushQueueId must be the resolved 32-hex ID of core.pushQueue.");
43 if (core.secretsFile != null && (typeof core.secretsFile !== "string" ||
44 !core.secretsFile.trim() || core.secretsFile !== core.secretsFile.trim()))
45 throw new Error("core.secretsFile must be null or an ignored owner file path.");
46 if (core.pushQueue === core.deadLetterQueue)
47 throw new Error("core Queue and dead-letter Queue must differ.");
48 if (
49 typeof core.hostname !== "string" ||
50 !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(
51 core.hostname,
52 ) ||
53 core.hostname ===
54 new URL(config.publicBaseUrl ?? `https://${config.workers.router.route.customDomain}`)
55 .hostname
56 )
57 throw new Error(
58 "core.hostname must be a lowercase custom hostname distinct from the OS router.",
59 );
60 if (
61 typeof core.mcpPath !== "string" ||
62 !/^\/[a-z0-9-]+$/.test(core.mcpPath) ||
63 ["/app", "/projects", "/authorize", "/oauth"].includes(core.mcpPath)
64 )
65 throw new Error(
66 "core.mcpPath must be one path segment, separate from app, projects and OAuth.",
67 );
68 if (
69 typeof core.project !== "string" ||
70 core.project.length > 48 ||
71 !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(core.project)
72 )
73 throw new Error("core.project must be a lowercase project slug of at most 48 characters.");
74 if (
75 typeof core.artifactsNamespace !== "string" ||
76 !/^[a-z0-9][a-z0-9._-]*$/i.test(core.artifactsNamespace)
77 )
78 throw new Error("core.artifactsNamespace must be a valid namespace.");
79 if (
80 core.oauthKvNamespaceId !== null &&
81 (typeof core.oauthKvNamespaceId !== "string" ||
82 !/^[a-f0-9]{32}$/i.test(core.oauthKvNamespaceId))
83 )
84 throw new Error("core.oauthKvNamespaceId must be null or a 32-hex namespace ID.");
85 if (
86 !core.access ||
87 typeof core.access.issuer !== "string" ||
88 !/^https:\/\/[a-z0-9-]+\.cloudflareaccess\.com$/.test(core.access.issuer) ||
89 typeof core.access.audience !== "string" ||
90 !core.access.audience ||
91 core.access.audience.trim() !== core.access.audience ||
92 !Array.isArray(core.access.owners) ||
93 !core.access.owners.length ||
94 !core.access.owners.every(
95 (email) => typeof email === "string" && /^[^@\s]+@[^@\s]+$/.test(email),
96 )
97 )
98 throw new Error(
99 "core.access requires an Access team issuer, application audience and owner emails.",
100 );
101}
102
103export function coreConfig(
104 config: DeploymentConfig,
105 bases: BaseConfigs,
106): ProdWranglerConfig | undefined {
107 if (!config.core?.enabled) return undefined;
108 if (!bases.core) throw new Error("Missing core base Wrangler config.");
109 const core = config.core;
110 return {
111 ...structuredClone(bases.core),
112 account_id: config.accountId,
113 name: core.workerName,
114 workers_dev: false,
115 preview_urls: false,
116 routes: [{ pattern: core.hostname, custom_domain: true }],
117 vars: {
118 ARTIFACTS_NAMESPACE: core.artifactsNamespace,
119 CF_ACCOUNT_ID: config.accountId,
120 PUSH_QUEUE_ID: core.pushQueueId,
121 CORE_PUBLIC_ORIGIN: `https://${core.hostname}`,
122 MCP_PATH: core.mcpPath,
123 MCP_PROJECT: core.project,
124 CORE_ACCESS_ISSUER: core.access.issuer,
125 CORE_ACCESS_AUDIENCE: core.access.audience,
126 CORE_ACCESS_OWNERS: core.access.owners,
127 },
128 artifacts: [{ binding: "ARTIFACTS", namespace: core.artifactsNamespace }],
129 kv_namespaces: [
130 { binding: "OAUTH_KV", ...(core.oauthKvNamespaceId ? { id: core.oauthKvNamespaceId } : {}) },
131 ],
132 secrets: { required: ["OWNER_API_TOKEN_HASH", "CF_EVENTS_API_TOKEN"] },
133 assets: {
134 directory: "../forkhorse-dashboard/dist",
135 binding: "ASSETS",
136 run_worker_first: true,
137 html_handling: "none",
138 not_found_handling: "none",
139 },
140 queues: {
141 consumers: [
142 {
143 queue: core.pushQueue,
144 dead_letter_queue: core.deadLetterQueue,
145 max_retries: 5,
146 max_batch_size: 10,
147 },
148 ],
149 },
150 };
151}
152