public
imalexnord
read
Slot
Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.
Languages
Repository composition by tracked source files.
TypeScript
79%
CSS
19%
SQL
2%
HTML
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { stdin as input, stdout as output } from 'node:process';
import { createInterface } from 'node:readline/promises';
const root = process.cwd();
const workerConfigPath = path.join(root, 'wrangler.jsonc');
const wranglerCli = path.join(root, 'node_modules', 'wrangler', 'bin', 'wrangler.js');
function fail(message) {
console.error(`\nStripe setup failed: ${message}`);
process.exit(1);
}
function ensureProjectRoot() {
if (!fs.existsSync(workerConfigPath)) {
fail('Run npm run setup:stripe from the Slot project root.');
}
if (!fs.existsSync(wranglerCli)) {
fail('Dependencies are missing. Run npm install, then try again.');
}
}
function wrangler(args, options = {}) {
const { allowFailure = false, capture = false } = options;
const result = spawnSync(process.execPath, [wranglerCli, ...args], {
cwd: root,
shell: false,
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
encoding: capture ? 'utf8' : undefined,
env: { ...process.env, NO_COLOR: process.env.NO_COLOR ?? '1' },
});
if (result.error) {
if (allowFailure) return result;
throw result.error;
}
if ((result.status ?? 1) !== 0 && !allowFailure) {
process.exit(result.status ?? 1);
}
return result;
}
async function promptForStripe() {
const rl = createInterface({ input, output });
try {
console.log('\nStripe configuration');
console.log('Use test-mode values while testing. Do not paste these secrets into chat.\n');
const stripeSecretKey = (await rl.question('Stripe Secret Key (sk_test_... or sk_live_...): ')).trim();
const stripeWebhookSecret = (await rl.question('Stripe Webhook Signing Secret (whsec_...): ')).trim();
if (!/^sk_(test|live)_/.test(stripeSecretKey)) {
throw new Error('Stripe Secret Key must begin with sk_test_ or sk_live_.');
}
if (!/^whsec_/.test(stripeWebhookSecret)) {
throw new Error('Stripe Webhook Signing Secret must begin with whsec_.');
}
return { stripeSecretKey, stripeWebhookSecret };
} finally {
rl.close();
}
}
function uploadStripeSecrets(answers) {
const secretFile = path.join(root, `.slot-stripe-secrets-${process.pid}.json`);
try {
fs.writeFileSync(secretFile, JSON.stringify({
STRIPE_SECRET_KEY: answers.stripeSecretKey,
STRIPE_WEBHOOK_SECRET: answers.stripeWebhookSecret,
}), { mode: 0o600 });
wrangler(['secret', 'bulk', secretFile]);
} finally {
fs.rmSync(secretFile, { force: true });
}
}
function listSecretNames() {
const result = wrangler(['secret', 'list', '--format', 'json'], { allowFailure: true, capture: true });
if ((result.status ?? 1) !== 0) return new Set();
try {
const parsed = JSON.parse(`${result.stdout ?? ''}`.trim() || '[]');
return new Set(Array.isArray(parsed) ? parsed.map((item) => item.name).filter(Boolean) : []);
} catch {
return new Set();
}
}
ensureProjectRoot();
console.log('\nSlot Stripe setup\n=================');
console.log('This only adds/updates Stripe Worker secrets. It does not touch D1, Google OAuth, or application code.\n');
if ((wrangler(['whoami'], { allowFailure: true, capture: true }).status ?? 1) !== 0) {
console.log('Opening Cloudflare login...');
wrangler(['login']);
}
const answers = await promptForStripe();
uploadStripeSecrets(answers);
const names = listSecretNames();
const missing = ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'].filter((name) => !names.has(name));
if (missing.length) {
fail(`Could not verify Worker secrets: ${missing.join(', ')}`);
}
console.log('\nDone. Stripe secrets are configured on the existing Worker.');
console.log('Next, set the per-minute rate and enable required payment in Slot Admin -> Settings -> Stripe payments.\n');