- 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>
77 lines
2.8 KiB
JavaScript
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, ... }
|
|
}
|