#!/usr/bin/env node // Local alternative to `rclone config` for obtaining a Google Drive OAuth // refresh token. Runs the standard "installed app" / loopback OAuth flow // (RFC 8252): opens the consent screen, catches the redirect on a local // HTTP server, and exchanges the code for tokens. // // You still need your own OAuth client (Google requires this per-app, it // cannot be generated by a script): // 1. https://console.cloud.google.com/ -> create/select a project. // 2. APIs & Services > Library -> enable "Google Drive API". // 3. APIs & Services > OAuth consent screen -> External, Testing is fine; // add your own Google account under "Test users". // 4. APIs & Services > Credentials -> Create credentials > OAuth client ID // -> Application type: "Desktop app". Copy the Client ID and Client secret. // // Usage: // node scripts/get-google-refresh-token.mjs --client-id ID --client-secret SECRET // (or set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET env vars instead of flags) // // Optional: --port 8976 (must not be in use; Desktop-app clients accept any // loopback port automatically, no need to register it in Google Cloud Console). import { exec } from "node:child_process"; import { randomBytes } from "node:crypto"; import http from "node:http"; import { URL } from "node:url"; const SCOPE = "https://www.googleapis.com/auth/drive"; const AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"; const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"; function parseArgs() { const args = process.argv.slice(2); const opts = { port: 8976 }; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === "--client-id") opts.clientId = args[++i]; else if (a === "--client-secret") opts.clientSecret = args[++i]; else if (a === "--port") opts.port = Number(args[++i]); else if (a === "--help" || a === "-h") opts.help = true; } opts.clientId ??= process.env.GOOGLE_CLIENT_ID; opts.clientSecret ??= process.env.GOOGLE_CLIENT_SECRET; return opts; } function printHelp() { console.log(` Usage: node scripts/get-google-refresh-token.mjs --client-id --client-secret [--port 8976] Before running this script, create a Desktop-app OAuth client in the Google Cloud Console (see the comment block at the top of this file for the exact steps) and enable the Google Drive API for that project. Env vars GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are accepted instead of flags. `); } function openBrowser(url) { const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`; exec(cmd, () => {}); } async function main() { const opts = parseArgs(); if (opts.help || !opts.clientId || !opts.clientSecret) { printHelp(); process.exit(opts.help ? 0 : 1); } const redirectUri = `http://127.0.0.1:${opts.port}/`; const state = randomBytes(16).toString("hex"); const authUrl = new URL(AUTH_ENDPOINT); authUrl.searchParams.set("client_id", opts.clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", SCOPE); authUrl.searchParams.set("access_type", "offline"); authUrl.searchParams.set("prompt", "consent"); // force refresh_token even on repeat auth authUrl.searchParams.set("state", state); const code = await new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const reqUrl = new URL(req.url, redirectUri); if (reqUrl.pathname !== "/") { res.writeHead(404).end(); return; } const returnedState = reqUrl.searchParams.get("state"); const err = reqUrl.searchParams.get("error"); const authCode = reqUrl.searchParams.get("code"); if (err) { res.writeHead(400, { "Content-Type": "text/plain" }).end(`Authorization failed: ${err}`); server.close(); reject(new Error(`Google returned error: ${err}`)); return; } if (returnedState !== state || !authCode) { res.writeHead(400, { "Content-Type": "text/plain" }).end("Invalid state or missing code."); server.close(); reject(new Error("State mismatch or missing authorization code.")); return; } res.writeHead(200, { "Content-Type": "text/plain" }).end("Authorization complete. You can close this tab and return to the terminal."); server.close(); resolve(authCode); }); server.listen(opts.port, "127.0.0.1", () => { console.log(`\nOpen this URL in your browser if it doesn't open automatically:\n\n${authUrl.toString()}\n`); openBrowser(authUrl.toString()); console.log("Waiting for authorization..."); }); server.on("error", reject); }); const tokenRes = await fetch(TOKEN_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ code, client_id: opts.clientId, client_secret: opts.clientSecret, redirect_uri: redirectUri, grant_type: "authorization_code", }), }); const tokenData = await tokenRes.json(); if (!tokenRes.ok) { console.error("Token exchange failed:", tokenData); process.exit(1); } if (!tokenData.refresh_token) { console.error("No refresh_token was returned. This usually means the account already has an active grant.\n" + "Revoke it at https://myaccount.google.com/permissions and re-run this script."); process.exit(1); } console.log("\nSuccess. Set these as your Worker secrets:\n"); console.log(`GOOGLE_CLIENT_ID=${opts.clientId}`); console.log(`GOOGLE_CLIENT_SECRET=${opts.clientSecret}`); console.log(`GOOGLE_REFRESH_TOKEN=${tokenData.refresh_token}`); console.log("\nExample (from the repo root):\n"); console.log(` echo -n "${opts.clientId}" | npx wrangler secret put GOOGLE_CLIENT_ID`); console.log(` echo -n "${opts.clientSecret}" | npx wrangler secret put GOOGLE_CLIENT_SECRET`); console.log(` echo -n "${tokenData.refresh_token}" | npx wrangler secret put GOOGLE_REFRESH_TOKEN`); } main().catch((err) => { console.error(err.message ?? err); process.exit(1); });