@enderrealmmc/cf-static-guard 0.1.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.
@@ -0,0 +1,257 @@
1
+ import { confirm, input, password, select } from '@inquirer/prompts';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { defaultProfile, getProfile, upsertProfile, } from '../lib/store.js';
4
+ import { box, error, step, success, warn } from '../lib/log.js';
5
+ import { createKvNamespace, putSecret, whoami } from '../lib/cf.js';
6
+ import { pushAuthConfig, pushUiConfig } from '../lib/kv.js';
7
+ export function registerInit(program) {
8
+ program
9
+ .command('init')
10
+ .description('Create/update a global profile and push first-time secrets (writes only to ~/.config/csg and Cloudflare)')
11
+ .option('-n, --name <worker>', 'Cloudflare worker name')
12
+ .option('-m, --mode <mode>', 'normal | strict', 'normal')
13
+ .option('--github-client-id <id>', 'GitHub OAuth client id')
14
+ .option('--github-client-secret <secret>', 'GitHub OAuth client secret')
15
+ .option('--session-secret <secret>', 'JWT session secret (auto-generated if omitted)')
16
+ .option('--admin-password <pwd>', 'Admin panel password (auto-generated if omitted)')
17
+ .option('--kv-id <id>', 'Existing KV namespace id')
18
+ .option('--create-kv', 'Create a new KV namespace named RULES')
19
+ .option('--spa', 'Enable SPA index fallback')
20
+ .option('--redirect-base <url>', 'OAuth redirect base origin')
21
+ .option('--custom-domain <host>', 'Attach custom domain to worker (e.g. docs.example.com)')
22
+ .option('--push', 'Push auth/ui config to KV after setup')
23
+ .option('--non-interactive', 'Fail instead of prompting when required values are missing')
24
+ .action(async (opts) => {
25
+ const interactive = !opts.nonInteractive;
26
+ const auth = await whoami();
27
+ if (!auth.ok) {
28
+ warn('Wrangler may not be logged in. Run `csg auth` if deploy fails.');
29
+ }
30
+ let name = opts.name;
31
+ if (!name && interactive) {
32
+ name = await input({
33
+ message: 'Worker name (subdomain on workers.dev)',
34
+ default: 'cf-static-guard',
35
+ validate: (v) => /^[a-z0-9-]+$/i.test(v) || 'Use letters, numbers, dashes',
36
+ });
37
+ }
38
+ if (!name) {
39
+ error('--name is required in non-interactive mode');
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ let mode = opts.mode === 'strict' ? 'strict' : 'normal';
44
+ if (interactive && !opts.nonInteractive) {
45
+ mode = await select({
46
+ message: 'Default access mode',
47
+ choices: [
48
+ { name: 'normal — OAuth success is enough (blocklist still applies)', value: 'normal' },
49
+ { name: 'strict — evaluate configured rules per provider', value: 'strict' },
50
+ ],
51
+ default: mode,
52
+ });
53
+ }
54
+ const existing = getProfile(name);
55
+ const profile = existing
56
+ ? { ...existing, updatedAt: new Date().toISOString() }
57
+ : defaultProfile(name, mode);
58
+ profile.mode = mode;
59
+ if (opts.spa)
60
+ profile.spaFallback = true;
61
+ if (opts.redirectBase)
62
+ profile.oauthRedirectBase = opts.redirectBase;
63
+ if (opts.customDomain)
64
+ profile.customDomain = opts.customDomain;
65
+ // GitHub
66
+ let clientId = opts.githubClientId || profile.providers.github?.clientId || process.env.CSG_GITHUB_CLIENT_ID;
67
+ let clientSecret = opts.githubClientSecret || process.env.CSG_GITHUB_CLIENT_SECRET;
68
+ if (interactive && !opts.nonInteractive) {
69
+ const enableGh = await confirm({
70
+ message: 'Enable GitHub OAuth?',
71
+ default: true,
72
+ });
73
+ profile.providers.github = {
74
+ enabled: enableGh,
75
+ mode: undefined,
76
+ rules: profile.providers.github?.rules || {},
77
+ };
78
+ if (enableGh) {
79
+ if (!clientId) {
80
+ clientId = await input({
81
+ message: 'GitHub OAuth Client ID',
82
+ validate: (v) => v.trim().length > 0 || 'required',
83
+ });
84
+ }
85
+ if (!clientSecret) {
86
+ clientSecret = await password({
87
+ message: 'GitHub OAuth Client Secret',
88
+ validate: (v) => v.trim().length > 0 || 'required',
89
+ });
90
+ }
91
+ if (mode === 'strict') {
92
+ profile.providers.github.rules = await promptStrictRules(profile.providers.github.rules || {});
93
+ }
94
+ }
95
+ }
96
+ else if (profile.providers.github?.enabled !== false) {
97
+ profile.providers.github = {
98
+ ...profile.providers.github,
99
+ enabled: true,
100
+ };
101
+ }
102
+ if (clientId)
103
+ profile.providers.github = { ...profile.providers.github, enabled: profile.providers.github?.enabled !== false, clientId };
104
+ // KV
105
+ let kvId = opts.kvId || profile.kvNamespaceId;
106
+ if (!kvId && interactive) {
107
+ const doKv = await confirm({
108
+ message: 'Create KV namespace RULES now? (needed for admin/config rules)',
109
+ default: true,
110
+ });
111
+ if (doKv) {
112
+ step('Creating KV namespace…');
113
+ try {
114
+ kvId = await createKvNamespace(name);
115
+ success(`KV created: ${kvId}`);
116
+ }
117
+ catch (e) {
118
+ error(e instanceof Error ? e.message : String(e));
119
+ const manual = await input({ message: 'Paste KV namespace id (or empty to skip)' });
120
+ kvId = manual.trim() || undefined;
121
+ }
122
+ }
123
+ }
124
+ else if (opts.createKv && !kvId) {
125
+ step('Creating KV namespace…');
126
+ kvId = await createKvNamespace(name);
127
+ success(`KV created: ${kvId}`);
128
+ }
129
+ if (kvId)
130
+ profile.kvNamespaceId = kvId;
131
+ // Secrets
132
+ let sessionSecret = opts.sessionSecret || process.env.CSG_SESSION_SECRET;
133
+ let adminPassword = opts.adminPassword || process.env.CSG_ADMIN_PASSWORD;
134
+ if (interactive && !opts.nonInteractive) {
135
+ if (!sessionSecret) {
136
+ const gen = await confirm({
137
+ message: 'Generate SESSION_SECRET automatically?',
138
+ default: true,
139
+ });
140
+ sessionSecret = gen
141
+ ? randomBytes(32).toString('base64url')
142
+ : await password({ message: 'SESSION_SECRET' });
143
+ }
144
+ if (!adminPassword) {
145
+ const gen = await confirm({
146
+ message: 'Generate ADMIN_PASSWORD automatically?',
147
+ default: true,
148
+ });
149
+ adminPassword = gen
150
+ ? randomBytes(18).toString('base64url')
151
+ : await password({ message: 'ADMIN_PASSWORD (for /admin)' });
152
+ }
153
+ }
154
+ else {
155
+ sessionSecret = sessionSecret || randomBytes(32).toString('base64url');
156
+ adminPassword = adminPassword || randomBytes(18).toString('base64url');
157
+ }
158
+ profile.updatedAt = new Date().toISOString();
159
+ upsertProfile(profile, true);
160
+ success(`Profile saved globally: ${profile.name}`);
161
+ // Push secrets to CF (requires existing worker after first deploy, or wrangler will still accept)
162
+ const secrets = [
163
+ ['SESSION_SECRET', sessionSecret],
164
+ ['ADMIN_PASSWORD', adminPassword],
165
+ ['GITHUB_CLIENT_ID', clientId],
166
+ ['GITHUB_CLIENT_SECRET', clientSecret],
167
+ ];
168
+ const doSecrets = opts.nonInteractive ||
169
+ (await confirm({
170
+ message: `Write secrets to Cloudflare worker "${profile.name}" now?`,
171
+ default: true,
172
+ }));
173
+ if (doSecrets) {
174
+ for (const [key, value] of secrets) {
175
+ if (!value)
176
+ continue;
177
+ step(`secret put ${key}…`);
178
+ try {
179
+ await putSecret(profile.name, key, value);
180
+ success(`${key} set`);
181
+ }
182
+ catch (e) {
183
+ error(`${key}: ${e instanceof Error ? e.message : e}`);
184
+ warn('If the worker does not exist yet, deploy once then re-run secrets.');
185
+ }
186
+ }
187
+ }
188
+ const doPush = opts.push ||
189
+ (profile.kvNamespaceId &&
190
+ (opts.nonInteractive ||
191
+ (await confirm({
192
+ message: 'Push initial auth/ui config to KV?',
193
+ default: true,
194
+ }))));
195
+ if (doPush && profile.kvNamespaceId) {
196
+ try {
197
+ await pushAuthConfig(profile);
198
+ await pushUiConfig(profile);
199
+ success('KV auth:config + ui:config written');
200
+ }
201
+ catch (e) {
202
+ error(e instanceof Error ? e.message : String(e));
203
+ }
204
+ }
205
+ else if (!profile.kvNamespaceId) {
206
+ warn('No KV id — rules/config commands and admin save will not work until you add one.');
207
+ }
208
+ box([
209
+ 'Next:',
210
+ ` csg deploy --site <your-dist>`,
211
+ ` csg open admin # password = ADMIN_PASSWORD`,
212
+ '',
213
+ 'Nothing was written into your site project directory.',
214
+ ]);
215
+ });
216
+ }
217
+ async function promptStrictRules(initial) {
218
+ const rules = { ...initial };
219
+ const allow = await input({
220
+ message: 'Allowlist (comma-separated logins, empty to skip)',
221
+ default: (rules.allowlist || []).join(','),
222
+ });
223
+ rules.allowlist = allow
224
+ .split(',')
225
+ .map((s) => s.trim())
226
+ .filter(Boolean);
227
+ const orgs = await input({
228
+ message: 'Required GitHub orgs (comma-separated, empty to skip)',
229
+ default: (rules.requiredOrgs || []).join(','),
230
+ });
231
+ rules.requiredOrgs = orgs
232
+ .split(',')
233
+ .map((s) => s.trim())
234
+ .filter(Boolean);
235
+ const teams = await input({
236
+ message: 'Required teams org/team (comma-separated, empty to skip)',
237
+ default: (rules.requiredTeams || []).map((t) => `${t.org}/${t.team}`).join(','),
238
+ });
239
+ rules.requiredTeams = teams
240
+ .split(',')
241
+ .map((s) => s.trim())
242
+ .filter(Boolean)
243
+ .map((line) => {
244
+ const [org, team] = line.split('/');
245
+ return { org: org || '', team: team || '' };
246
+ })
247
+ .filter((t) => t.org && t.team);
248
+ const age = await input({
249
+ message: 'Min account age in days (empty to skip)',
250
+ default: rules.minAccountAgeDays != null ? String(rules.minAccountAgeDays) : '',
251
+ });
252
+ if (age.trim())
253
+ rules.minAccountAgeDays = Number(age.trim());
254
+ else
255
+ delete rules.minAccountAgeDays;
256
+ return rules;
257
+ }
@@ -0,0 +1,39 @@
1
+ import { getProfile, loadStore, upsertProfile } from '../lib/store.js';
2
+ import { error, step, success, warn } from '../lib/log.js';
3
+ import { createKvNamespace } from '../lib/cf.js';
4
+ import { pushAuthConfig, pushUiConfig } from '../lib/kv.js';
5
+ export function registerKv(program) {
6
+ const kv = program.command('kv').description('KV namespace helpers for RULES');
7
+ kv
8
+ .command('create')
9
+ .description('Create KV namespace and store id on the profile')
10
+ .option('-p, --profile <name>')
11
+ .option('--title <title>', 'Namespace title')
12
+ .option('--push', 'Push default auth/ui config after create')
13
+ .action(async (opts) => {
14
+ const store = loadStore();
15
+ const profile = getProfile(opts.profile);
16
+ if (!profile) {
17
+ error('No profile. Run `csg init` first.');
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ step('Creating KV namespace RULES…');
22
+ const id = await createKvNamespace(opts.title || profile.name);
23
+ const next = {
24
+ ...profile,
25
+ kvNamespaceId: id,
26
+ updatedAt: new Date().toISOString(),
27
+ };
28
+ upsertProfile(next, store.defaultProfile === profile.name);
29
+ success(`KV id saved: ${id}`);
30
+ if (opts.push) {
31
+ await pushAuthConfig(next);
32
+ await pushUiConfig(next);
33
+ success('Pushed default auth/ui config');
34
+ }
35
+ else {
36
+ warn('Run `csg config push` later to write auth:config / ui:config');
37
+ }
38
+ });
39
+ }
@@ -0,0 +1,54 @@
1
+ import { execa } from 'execa';
2
+ import { getProfile } from '../lib/store.js';
3
+ import { error, info } from '../lib/log.js';
4
+ function openUrl(url) {
5
+ if (process.platform === 'win32') {
6
+ void execa('cmd', ['/c', 'start', '', url]);
7
+ return;
8
+ }
9
+ if (process.platform === 'darwin') {
10
+ void execa('open', [url]);
11
+ return;
12
+ }
13
+ void execa('xdg-open', [url]);
14
+ }
15
+ export function registerOpen(program) {
16
+ program
17
+ .command('open <target>')
18
+ .description('Open site | admin | login | health in browser')
19
+ .option('-p, --profile <name>')
20
+ .option('--base <url>', 'Full origin, e.g. https://my-docs.xxx.workers.dev')
21
+ .action(async (target, opts) => {
22
+ const profile = getProfile(opts.profile);
23
+ let base = opts.base || process.env.CSG_BASE_URL;
24
+ if (!base) {
25
+ if (!profile) {
26
+ error('No profile and no --base. Pass --base https://<worker>.<subdomain>.workers.dev');
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ // users often have custom domains; default guess is name.workers.dev under unknown subdomain
31
+ error(`Pass --base https://your-worker-domain (profile ${profile.name} has no recorded public origin).`);
32
+ info('Example: csg open admin --base https://my-docs.acme.workers.dev');
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ base = base.replace(/\/$/, '');
37
+ const map = {
38
+ site: '/',
39
+ home: '/',
40
+ admin: '/admin',
41
+ login: '/auth/login',
42
+ health: '/health',
43
+ };
44
+ const path = map[target];
45
+ if (!path) {
46
+ error(`Unknown target ${target}. Use: site | admin | login | health`);
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ const url = base + path;
51
+ info(`Opening ${url}`);
52
+ openUrl(url);
53
+ });
54
+ }
@@ -0,0 +1,69 @@
1
+ import { listProfileNames, loadStore, setDefaultProfile, upsertProfile, } from '../lib/store.js';
2
+ import { error, info, success } from '../lib/log.js';
3
+ export function registerProfile(program) {
4
+ const profile = program
5
+ .command('profile')
6
+ .description('Manage global deploy profiles (not stored in your site repo)');
7
+ profile
8
+ .command('list')
9
+ .description('List profiles')
10
+ .action(() => {
11
+ const store = loadStore();
12
+ const names = listProfileNames();
13
+ if (!names.length) {
14
+ info('No profiles. Run `csg init`.');
15
+ return;
16
+ }
17
+ for (const name of names) {
18
+ const p = store.profiles[name];
19
+ const mark = name === store.defaultProfile ? '*' : ' ';
20
+ console.log(`${mark} ${name} worker=${p.name} mode=${p.mode} kv=${p.kvNamespaceId || '-'}`);
21
+ }
22
+ });
23
+ profile
24
+ .command('use <name>')
25
+ .description('Set default profile')
26
+ .action((name) => {
27
+ if (!setDefaultProfile(name)) {
28
+ error(`Profile not found: ${name}`);
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ success(`Default profile → ${name}`);
33
+ });
34
+ profile
35
+ .command('show [name]')
36
+ .description('Show one profile JSON')
37
+ .action((name) => {
38
+ const store = loadStore();
39
+ const key = name || store.defaultProfile;
40
+ const p = store.profiles[key];
41
+ if (!p) {
42
+ error(`Profile not found: ${key}`);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ console.log(JSON.stringify(p, null, 2));
47
+ });
48
+ profile
49
+ .command('set-kv <id>')
50
+ .description('Set KV namespace id on current/default profile')
51
+ .option('-p, --profile <name>', 'profile name')
52
+ .action((id, opts) => {
53
+ const store = loadStore();
54
+ const key = opts.profile || store.defaultProfile;
55
+ const p = store.profiles[key];
56
+ if (!p) {
57
+ error(`Profile not found: ${key}`);
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+ const next = {
62
+ ...p,
63
+ kvNamespaceId: id,
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+ upsertProfile(next, key === store.defaultProfile);
67
+ success(`KV namespace id saved on profile ${key}`);
68
+ });
69
+ }
@@ -0,0 +1,72 @@
1
+ import { password } from '@inquirer/prompts';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { getProfile } from '../lib/store.js';
4
+ import { error, step, success } from '../lib/log.js';
5
+ import { putSecret } from '../lib/cf.js';
6
+ const KEYS = [
7
+ 'GITHUB_CLIENT_ID',
8
+ 'GITHUB_CLIENT_SECRET',
9
+ 'SESSION_SECRET',
10
+ 'ADMIN_PASSWORD',
11
+ ];
12
+ export function registerSecrets(program) {
13
+ const secrets = program
14
+ .command('secrets')
15
+ .description('Set Cloudflare Worker secrets (not stored in your project)');
16
+ secrets
17
+ .command('set [name]')
18
+ .description('Set one secret, or run without name for interactive multi-set')
19
+ .option('-p, --profile <name>', 'worker profile')
20
+ .option('--value <v>', 'Secret value (prefer prompt/env for sensitive data)')
21
+ .option('--generate', 'Generate a random value (SESSION_SECRET / ADMIN_PASSWORD)')
22
+ .action(async (name, opts) => {
23
+ const profile = getProfile(opts.profile);
24
+ if (!profile) {
25
+ error('No profile. Run `csg init` first.');
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ const targets = name ? [name] : [...KEYS];
30
+ if (name && !KEYS.includes(name)) {
31
+ error(`Unknown secret. Use: ${KEYS.join(', ')}`);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ for (const key of targets) {
36
+ let value = opts.value;
37
+ if (!value && opts.generate && (key === 'SESSION_SECRET' || key === 'ADMIN_PASSWORD')) {
38
+ value = randomBytes(24).toString('base64url');
39
+ }
40
+ if (!value) {
41
+ value = await password({
42
+ message: `${key} value for worker ${profile.name}`,
43
+ mask: '*',
44
+ });
45
+ }
46
+ if (!value) {
47
+ error(`${key}: empty value`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ step(`secret put ${key}…`);
52
+ await putSecret(profile.name, key, value);
53
+ success(`${key} updated on ${profile.name}`);
54
+ }
55
+ });
56
+ secrets
57
+ .command('rotate-session')
58
+ .description('Generate and set a new SESSION_SECRET (invalidates all logins)')
59
+ .option('-p, --profile <name>')
60
+ .action(async (opts) => {
61
+ const profile = getProfile(opts.profile);
62
+ if (!profile) {
63
+ error('No profile. Run `csg init` first.');
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ const value = randomBytes(32).toString('base64url');
68
+ step('secret put SESSION_SECRET…');
69
+ await putSecret(profile.name, 'SESSION_SECRET', value);
70
+ success('SESSION_SECRET rotated — users must log in again');
71
+ });
72
+ }
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import { Command } from 'commander';
2
+ import { cliVersion } from './lib/paths.js';
3
+ import { registerAuth } from './commands/auth.js';
4
+ import { registerInit } from './commands/init.js';
5
+ import { registerDeploy } from './commands/deploy.js';
6
+ import { registerConfig } from './commands/config.js';
7
+ import { registerSecrets } from './commands/secrets.js';
8
+ import { registerOpen } from './commands/open.js';
9
+ import { registerDoctor } from './commands/doctor.js';
10
+ import { registerProfile } from './commands/profile.js';
11
+ import { registerKv } from './commands/kv.js';
12
+ export async function run(argv) {
13
+ const program = new Command();
14
+ program
15
+ .name('csg')
16
+ .description('Cloudflare static-site guard CLI — deploy any static site behind OAuth without cloning the monorepo.')
17
+ .version(cliVersion());
18
+ registerAuth(program);
19
+ registerInit(program);
20
+ registerDeploy(program);
21
+ registerConfig(program);
22
+ registerSecrets(program);
23
+ registerOpen(program);
24
+ registerDoctor(program);
25
+ registerProfile(program);
26
+ registerKv(program);
27
+ program.addHelpText('after', `
28
+ Examples:
29
+ $ csg auth
30
+ $ csg init --name my-docs --mode normal
31
+ $ csg deploy --site ./dist
32
+ $ csg config get
33
+ $ csg config set-mode strict
34
+ $ csg open admin
35
+
36
+ Config lives in your global csg folder (~/.config/csg or %APPDATA%/csg).
37
+ Secrets and rules live on Cloudflare. This CLI does not write into your site repo.
38
+ `);
39
+ await program.parseAsync(argv);
40
+ }
package/dist/lib/cf.js ADDED
@@ -0,0 +1,22 @@
1
+ import { runWrangler } from './wrangler.js';
2
+ export async function putSecret(workerName, name, value) {
3
+ const res = await runWrangler(['secret', 'put', name, '--name', workerName], { input: value });
4
+ if (res.exitCode !== 0) {
5
+ throw new Error(res.stderr || res.stdout || `secret put ${name} failed`);
6
+ }
7
+ }
8
+ export async function createKvNamespace(_title) {
9
+ const res = await runWrangler(['kv', 'namespace', 'create', 'RULES', '--preview=false']);
10
+ // wrangler prints: { binding = "RULES", id = "xxxx" }
11
+ const text = `${res.stdout}\n${res.stderr}`;
12
+ const match = text.match(/id\s*=\s*"([a-f0-9]{32})"/i) || text.match(/id['":\s]+([a-f0-9]{32})/i);
13
+ if (res.exitCode !== 0 || !match) {
14
+ throw new Error(`Failed to create KV namespace.\n${text}\nCreate it in the dashboard and set kvNamespaceId on the profile.`);
15
+ }
16
+ return match[1];
17
+ }
18
+ export async function whoami() {
19
+ const res = await runWrangler(['whoami']);
20
+ const text = `${res.stdout}\n${res.stderr}`.trim();
21
+ return { ok: res.exitCode === 0, text };
22
+ }
@@ -0,0 +1,75 @@
1
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { workerBundlePath } from './paths.js';
5
+ export function buildWranglerToml(profile) {
6
+ const publicPaths = (profile.publicPaths?.length
7
+ ? profile.publicPaths
8
+ : ['/health'])
9
+ .map((p) => (p.startsWith('/') ? p : `/${p}`))
10
+ .join(',');
11
+ const kvBlock = profile.kvNamespaceId
12
+ ? `
13
+ [[kv_namespaces]]
14
+ binding = "RULES"
15
+ id = "${profile.kvNamespaceId}"
16
+ `
17
+ : `
18
+ # KV not configured. Run: csg kv create
19
+ # [[kv_namespaces]]
20
+ # binding = "RULES"
21
+ # id = "<id>"
22
+ `;
23
+ const redirect = profile.oauthRedirectBase
24
+ ? `\nOAUTH_REDIRECT_BASE = "${profile.oauthRedirectBase}"`
25
+ : '';
26
+ const customDomain = profile.customDomain
27
+ ? `
28
+ [[routes]]
29
+ pattern = "${profile.customDomain}"
30
+ custom_domain = true
31
+ `
32
+ : '';
33
+ return `name = "${profile.name}"
34
+ main = "worker.js"
35
+ compatibility_date = "2025-03-01"
36
+ compatibility_flags = ["nodejs_compat"]
37
+ ${customDomain}
38
+ [assets]
39
+ directory = "./assets/site"
40
+ binding = "ASSETS"
41
+ run_worker_first = true
42
+
43
+ [vars]
44
+ GUARD_MODE = "${profile.mode}"
45
+ PUBLIC_PATHS = "${publicPaths}"
46
+ SPA_FALLBACK = "${profile.spaFallback ? 'true' : 'false'}"
47
+ SESSION_TTL_SECONDS = "${String(profile.sessionTtlSeconds || 604800)}"
48
+ ${redirect}
49
+ ${kvBlock}`;
50
+ }
51
+ export function prepareDeployDir(profile, siteDir) {
52
+ if (!existsSync(workerBundlePath())) {
53
+ throw new Error('Worker bundle missing. Run `npm run build` in the monorepo, or reinstall the CLI package.');
54
+ }
55
+ if (!existsSync(siteDir)) {
56
+ throw new Error(`Site directory not found: ${siteDir}`);
57
+ }
58
+ const root = mkdtempSync(join(tmpdir(), 'csg-deploy-'));
59
+ const assets = join(root, 'assets', 'site');
60
+ mkdirSync(assets, { recursive: true });
61
+ cpSync(workerBundlePath(), join(root, 'worker.js'));
62
+ cpSync(siteDir, assets, { recursive: true });
63
+ writeFileSync(join(root, 'wrangler.toml'), buildWranglerToml(profile), 'utf8');
64
+ return {
65
+ root,
66
+ cleanup: () => {
67
+ try {
68
+ rmSync(root, { recursive: true, force: true });
69
+ }
70
+ catch {
71
+ // ignore
72
+ }
73
+ },
74
+ };
75
+ }