Googstr/public/drive.js
Jason Tudisco fcfaec675a Add NIP-46 signer, optional NIP-07 extension, passkey protection, backups
- NIP-46 remote signer in the page (bunker:// and nostrconnect:// pairing)
- Chrome extension (NIP-07) with per-site permission prompts, unlocked by
  the site via externally_connectable; downloadable from the page
- Passkey (WebAuthn PRF) encryption of the Drive key file
- NIP-49 backups: download or save to Drive; reveal nsec
- Fix: oldest key file wins when two devices sign up at once
- Fix: never overwrite an existing kind-0 because a relay was slow
- Fix: EOSE tracking; relays reconnect with backoff; verify event signatures
- One-click re-sign-in (consent only the first time)
- Vendored, pinned nostr-tools; CSP; no third-party scripts but Google's

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:33:02 -06:00

77 lines
2.8 KiB
JavaScript

// Google Drive helpers. Files written to the appDataFolder space are invisible
// to the user in the regular Drive UI and only readable by this app's Google
// Cloud project.
//
// Docs: https://developers.google.com/drive/api/guides/appdata
const DRIVE = "https://www.googleapis.com/drive/v3";
const UPLOAD = "https://www.googleapis.com/upload/drive/v3";
async function driveFetch(token, url, init = {}) {
const res = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${token}`,
...(init.headers || {}),
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Drive API ${res.status}: ${text.slice(0, 300)}`);
}
return res;
}
// Find a file by exact name inside the appDataFolder. Returns the file id, or
// null if not found. If several exist (two devices signed up at the same
// moment), the OLDEST wins, so every device converges on the same key.
export async function findAppDataFile(token, name) {
const url =
`${DRIVE}/files?spaces=appDataFolder` +
`&q=${encodeURIComponent(`name = '${name.replace(/'/g, "\\'")}'`)}` +
`&fields=files(id,createdTime)`;
const res = await driveFetch(token, url);
const { files } = await res.json();
if (!files || files.length === 0) return null;
files.sort((a, b) => (a.createdTime || "").localeCompare(b.createdTime || ""));
return files[0].id;
}
export async function downloadFile(token, fileId) {
const res = await driveFetch(token, `${DRIVE}/files/${fileId}?alt=media`);
return res.text();
}
// Multipart upload of a small text blob. If fileId is given, replaces that
// file's content; otherwise creates a new file under `parents`
// (["appDataFolder"] for the hidden key file, omit for My Drive).
export async function uploadFile(token, { name, content, mimeType = "application/json", parents, fileId = null }) {
const boundary = "goostr-" + Math.random().toString(36).slice(2);
const metadata = fileId ? { name } : { name, mimeType, ...(parents ? { parents } : {}) };
const body =
`--${boundary}\r\n` +
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
`${JSON.stringify(metadata)}\r\n` +
`--${boundary}\r\n` +
`Content-Type: ${mimeType}\r\n\r\n` +
`${content}\r\n` +
`--${boundary}--`;
const url = fileId
? `${UPLOAD}/files/${fileId}?uploadType=multipart`
: `${UPLOAD}/files?uploadType=multipart&fields=id,webViewLink`;
const res = await driveFetch(token, url, {
method: fileId ? "PATCH" : "POST",
headers: { "Content-Type": `multipart/related; boundary=${boundary}` },
body,
});
return res.json();
}
export async function fetchUserInfo(token) {
const res = await driveFetch(token, "https://www.googleapis.com/oauth2/v3/userinfo");
return res.json(); // { sub, name, picture, email, ... }
}