Fork Horse repository, listed by its owner. Publisher snapshot.

scripts/core-deployment.ts

Snapshot at 34e3175

Containing directory

5,528 bytes · typescriptView raw
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