fix: add script for get refresh token

This commit is contained in:
2026-08-16 13:53:40 +07:00
parent 5dd168dc98
commit ffe9269bb5
4 changed files with 214 additions and 46 deletions
+6 -1
View File
@@ -18,10 +18,15 @@ For the sake of your Google account, we strongly recommend using this in an envi
## How to Deploy
### 1. Prepare a Google Drive API Refresh Token
You will need to use rclone to obtain your Google API credentials and a Google Drive API refresh token.
You need Google API credentials and a Google Drive API refresh token. Either method below requires you to first create your own OAuth client (Google requires this per-app; a shared client cannot be scripted around it) — see the steps in either option.
**Option A: rclone**
Follow the rclone documentation to configure the client.
https://rclone.org/drive/#making-your-own-client-id
**Option B: local script**
Run `pnpm get-refresh-token -- --client-id <ID> --client-secret <SECRET>` (see `scripts/get-google-refresh-token.mjs` for the Google Cloud Console setup steps — enabling the Drive API and creating a "Desktop app" OAuth client). It opens the consent screen, catches the redirect locally, and prints the values below directly.
> [!NOTE]
> When your Google API client is in "Testing" mode, the refresh token will expire after a certain period of time, so if you need to use it for a long period of time, be sure to switch the mode before authenticating with rclone.
>
+1
View File
@@ -8,6 +8,7 @@
"start": "pnpx wrangler dev",
"test": "vitest --run",
"cf-typegen": "pnpx wrangler types",
"get-refresh-token": "node scripts/get-google-refresh-token.mjs",
"lint": "biome check",
"format": "biome format --write"
},
+160
View File
@@ -0,0 +1,160 @@
#!/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 { randomBytes } from "node:crypto";
import http from "node:http";
import { exec } from "node:child_process";
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 <ID> --client-secret <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);
});
+47 -45
View File
@@ -3,67 +3,69 @@
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "iris",
"main": "src/index.ts",
"compatibility_date": "2025-09-27",
"vars": {
"ALLOW_MULTIPART": "false",
"ETAG_STYLE": "md5"
},
"observability": {
"enabled": true
},
"durable_objects": {
"bindings": [
{
"name": "MPU",
"class_name": "MultipartUploadDO"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MultipartUploadDO"]
}
],
"kv_namespaces": [
{
"binding": "AUTH_KV",
"id": "ba953a1ce2cd41a7ba1d348075d34171"
},
{
"binding": "FOLDER_CACHE",
"id": "6332b0674a8342e4b270ca3368d3c52a"
}
]
/**
"$schema": "node_modules/wrangler/config-schema.json",
"name": "iris",
"main": "src/index.ts",
"compatibility_date": "2025-09-27",
"vars": {
"ALLOW_MULTIPART": "true",
"ETAG_STYLE": "md5"
},
"observability": {
"enabled": true
},
"durable_objects": {
"bindings": [
{
"name": "MPU",
"class_name": "MultipartUploadDO"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"MultipartUploadDO"
]
}
],
"kv_namespaces": [
{
"binding": "AUTH_KV",
"id": "2113be46ce514e088572d0ef7459c1bf"
},
{
"binding": "FOLDER_CACHE",
"id": "6d2f9904a3ff4471b0f04025c1ae87cd"
}
]
/**
* Smart Placement
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
*/
// "placement": { "mode": "smart" }
/**
// "placement": { "mode": "smart" }
/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
/**
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*/
// "vars": { "MY_VARIABLE": "production_value" }
/**
// "vars": { "MY_VARIABLE": "production_value" }
/**
* Static Assets
* https://developers.cloudflare.com/workers/static-assets/binding/
*/
// "assets": { "directory": "./public/", "binding": "ASSETS" }
/**
// "assets": { "directory": "./public/", "binding": "ASSETS" }
/**
* Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
}
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
}