fuzzyworld

word-waltz / files / scripts / setup-push.js

setup-push.js

6.1 KB · history · blame

1#!/usr/bin/env node
2/**
3 * scripts/setup-push.js — one-time Web Push / VAPID key setup
4 *
5 * Generates a VAPID key pair, writes the public key to .env, and stores all
6 * three secrets in Firebase Secret Manager. Run once (or to rotate keys).
7 * After running, deploy the Cloud Function with: npm run deploy-functions
8 *
9 * Prerequisites:
10 * - firebase login
11 * - Project must be on the Blaze plan
12 */
13
14import { execSync } from 'child_process';
15import fs from 'fs';
16import path from 'path';
17import { fileURLToPath } from 'url';
18import readline from 'readline';
19
20const __dirname = path.dirname(fileURLToPath(import.meta.url));
21const ROOT = path.resolve(__dirname, '..');
22const ENV_FILE = path.join(ROOT, '.env');
23
24// ---------------------------------------------------------------------------
25// Helpers
26// ---------------------------------------------------------------------------
27
28function ask(question) {
29 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30 return new Promise((resolve) => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
31}
32
33/** Read .env as a key=value map (ignores comments and blanks). */
34function readEnv() {
35 if (!fs.existsSync(ENV_FILE)) return {};
36 const lines = fs.readFileSync(ENV_FILE, 'utf8').split('\n');
37 const map = {};
38 for (const line of lines) {
39 const trimmed = line.trim();
40 if (!trimmed || trimmed.startsWith('#')) continue;
41 const idx = trimmed.indexOf('=');
42 if (idx === -1) continue;
43 map[trimmed.slice(0, idx)] = trimmed.slice(idx + 1);
44 }
45 return map;
46}
47
48/** Write a single key=value into .env, updating it if the key already exists. */
49function writeEnvKey(key, value) {
50 let content = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
51 const regex = new RegExp(`^${key}=.*$`, 'm');
52 if (regex.test(content)) {
53 content = content.replace(regex, `${key}=${value}`);
54 } else {
55 content += (content.endsWith('\n') ? '' : '\n') + `${key}=${value}\n`;
56 }
57 fs.writeFileSync(ENV_FILE, content);
58 console.log(` ✓ Written ${key} to .env`);
59}
60
61/** Set a Firebase Functions secret non-interactively by piping the value. */
62function setFirebaseSecret(name, value) {
63 console.log(`\n$ firebase functions:secrets:set ${name} [piped]`);
64 execSync(`npx --package=firebase-tools firebase --project word-waltz functions:secrets:set ${name}`, {
65 input: value,
66 stdio: ['pipe', 'inherit', 'inherit'],
67 cwd: ROOT,
68 });
69}
70
71// ---------------------------------------------------------------------------
72// Main
73// ---------------------------------------------------------------------------
74
75async function main() {
76 console.log('=== Word Waltz — Web Push setup ===\n');
77
78 // ---- 1. Generate VAPID keys ----
79 console.log('Step 1: Generating VAPID key pair…');
80 // We need web-push available. Try the local functions/ installation first,
81 // fall back to npx.
82 let generateKeys;
83 try {
84 const wpPath = path.join(ROOT, 'functions', 'node_modules', 'web-push', 'src', 'vapid-helper.js');
85 if (fs.existsSync(wpPath)) {
86 const { generateVAPIDKeys } = await import(wpPath);
87 generateKeys = generateVAPIDKeys;
88 }
89 } catch {
90 // will fall back to npx
91 }
92
93 let publicKey, privateKey;
94 if (generateKeys) {
95 ({ publicKey, privateKey } = generateKeys());
96 } else {
97 // Use npx web-push and parse stdout
98 const output = execSync('npx --yes web-push generate-vapid-keys --json', {
99 cwd: ROOT,
100 encoding: 'utf8',
101 });
102 try {
103 const parsed = JSON.parse(output);
104 publicKey = parsed.publicKey;
105 privateKey = parsed.privateKey;
106 } catch {
107 // Older web-push versions print human-readable text
108 const pubMatch = output.match(/Public Key:\s*\n?\s*(\S+)/);
109 const privMatch = output.match(/Private Key:\s*\n?\s*(\S+)/);
110 if (!pubMatch || !privMatch) {
111 console.error('Could not parse web-push output:\n', output);
112 process.exit(1);
113 }
114 publicKey = pubMatch[1];
115 privateKey = privMatch[1];
116 }
117 }
118
119 console.log(` Public key : ${publicKey}`);
120 console.log(` Private key: ${privateKey.slice(0, 6)}… (kept secret)`);
121
122 // ---- 2. Write public key to .env ----
123 console.log('\nStep 2: Writing public key to .env…');
124 const existing = readEnv();
125 if (existing['VITE_VAPID_PUBLIC_KEY'] && existing['VITE_VAPID_PUBLIC_KEY'] !== publicKey) {
126 const overwrite = await ask(
127 ` .env already has a VAPID public key. Overwrite? [y/N] `,
128 );
129 if (!overwrite.toLowerCase().startsWith('y')) {
130 console.log(' Skipped — keeping existing key. Re-run if you want fresh keys.');
131 process.exit(0);
132 }
133 }
134 writeEnvKey('VITE_VAPID_PUBLIC_KEY', publicKey);
135
136 // Also update .env.example placeholder without the real value
137 const exampleFile = path.join(ROOT, '.env.example');
138 if (fs.existsSync(exampleFile)) {
139 let ex = fs.readFileSync(exampleFile, 'utf8');
140 ex = ex.replace(/^VITE_VAPID_PUBLIC_KEY=.*$/m, 'VITE_VAPID_PUBLIC_KEY=');
141 fs.writeFileSync(exampleFile, ex);
142 console.log(' ✓ Cleared VITE_VAPID_PUBLIC_KEY in .env.example');
143 }
144
145 // ---- 3. Set Firebase Functions secrets ----
146 console.log('\nStep 3: Setting Firebase Functions secrets…');
147 const subject = 'mailto:pavel@fuzzyworld.net';
148
149 setFirebaseSecret('VAPID_SUBJECT', subject);
150 setFirebaseSecret('VAPID_PUBLIC_KEY', publicKey);
151 setFirebaseSecret('VAPID_PRIVATE_KEY', privateKey);
152 console.log(' ✓ All three secrets stored in Firebase.');
153
154 console.log('\n✅ VAPID setup complete!');
155 console.log(' Next steps:');
156 console.log(' 1. Run: npm run deploy-functions');
157 console.log(' 2. Set this as a masked CI/CD variable in the word-waltz GitLab project:');
158 console.log(`\n VITE_VAPID_PUBLIC_KEY=${publicKey}\n`);
159}
160
161main().catch((err) => {
162 console.error('\n❌ Setup failed:', err.message ?? err);
163 process.exit(1);
164});