@bobfrankston/rmfmail 1.2.188 → 1.2.190

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1024,61 +1024,80 @@ async function refreshAccessToken(refreshToken: string): Promise<{ access_token:
1024
1024
  // ── Token provider (browser OAuth, same as desktop) ──
1025
1025
 
1026
1026
  function createNativeTokenProvider(email: string): () => Promise<string> {
1027
- return async () => {
1028
- // Check cached token first
1029
- const cached = await getCachedToken(email);
1030
- if (cached?.access_token) {
1031
- const expiresAt = cached.expires_at || 0;
1032
- const bufferMs = 5 * 60 * 1000; // 5 min buffer
1033
- if (Date.now() < expiresAt - bufferMs) {
1034
- return cached.access_token;
1035
- }
1036
- // Try refresh
1037
- if (cached.refresh_token) {
1038
- try {
1039
- console.log(`[oauth] Refreshing token for ${email}`);
1040
- const refreshed = await refreshAccessToken(cached.refresh_token);
1041
- const token = {
1042
- access_token: refreshed.access_token,
1043
- refresh_token: cached.refresh_token,
1044
- expires_at: Date.now() + refreshed.expires_in * 1000,
1045
- };
1046
- await setCachedToken(email, token);
1047
- return token.access_token;
1048
- } catch (e: any) {
1049
- console.warn(`[oauth] Refresh failed: ${e.message}, starting new flow`);
1050
- }
1027
+ return () => {
1028
+ // C158: single-flight per email. Startup fires several concurrent
1029
+ // token requests (GDrive reconcile, contacts sync, syncAll) — with
1030
+ // no cached token each one launched its OWN browser-consent intent
1031
+ // (three "Starting OAuth flow" in one boot, 2026-07-22 logit trail).
1032
+ // Concurrent callers now share one resolution. Keyed on a window
1033
+ // global, not a module local, because the double-init bug that
1034
+ // exposed this also loads the module twice — two module instances
1035
+ // must still share the guard.
1036
+ const w = window as any;
1037
+ const inflight: Map<string, Promise<string>> = w.__rmfOAuthInflight ||= new Map();
1038
+ const key = canonEmail(email);
1039
+ const existing = inflight.get(key);
1040
+ if (existing) return existing;
1041
+ const p = fetchTokenForEmail(email).finally(() => inflight.delete(key));
1042
+ inflight.set(key, p);
1043
+ return p;
1044
+ };
1045
+ }
1046
+
1047
+ async function fetchTokenForEmail(email: string): Promise<string> {
1048
+ // Check cached token first
1049
+ const cached = await getCachedToken(email);
1050
+ if (cached?.access_token) {
1051
+ const expiresAt = cached.expires_at || 0;
1052
+ const bufferMs = 5 * 60 * 1000; // 5 min buffer
1053
+ if (Date.now() < expiresAt - bufferMs) {
1054
+ return cached.access_token;
1055
+ }
1056
+ // Try refresh
1057
+ if (cached.refresh_token) {
1058
+ try {
1059
+ console.log(`[oauth] Refreshing token for ${email}`);
1060
+ const refreshed = await refreshAccessToken(cached.refresh_token);
1061
+ const token = {
1062
+ access_token: refreshed.access_token,
1063
+ refresh_token: cached.refresh_token,
1064
+ expires_at: Date.now() + refreshed.expires_in * 1000,
1065
+ };
1066
+ await setCachedToken(email, token);
1067
+ return token.access_token;
1068
+ } catch (e: any) {
1069
+ console.warn(`[oauth] Refresh failed: ${e.message}, starting new flow`);
1051
1070
  }
1052
1071
  }
1072
+ }
1053
1073
 
1054
- // No valid token — start browser OAuth flow
1055
- const bridge = (window as any)._nativeBridge;
1056
- if (!bridge?.app?.startOAuth) {
1057
- throw new Error("No native OAuth bridge");
1058
- }
1074
+ // No valid token — start browser OAuth flow
1075
+ const bridge = (window as any)._nativeBridge;
1076
+ if (!bridge?.app?.startOAuth) {
1077
+ throw new Error("No native OAuth bridge");
1078
+ }
1059
1079
 
1060
- const authUrl = `${OAUTH_CLIENT.authUri}?` + new URLSearchParams({
1061
- client_id: OAUTH_CLIENT.clientId,
1062
- redirect_uri: OAUTH_CLIENT.redirectUri,
1063
- response_type: "code",
1064
- scope: OAUTH_SCOPES,
1065
- access_type: "offline",
1066
- prompt: "consent",
1067
- login_hint: email,
1068
- }).toString();
1069
-
1070
- console.log(`[oauth] Starting browser consent for ${email}`);
1071
- const code = await bridge.app.startOAuth(authUrl);
1072
- const tokens = await exchangeCodeForTokens(code);
1073
- const token = {
1074
- access_token: tokens.access_token,
1075
- refresh_token: tokens.refresh_token,
1076
- expires_at: Date.now() + tokens.expires_in * 1000,
1077
- };
1078
- await setCachedToken(email, token);
1079
- console.log(`[oauth] Token obtained for ${email}`);
1080
- return token.access_token;
1080
+ const authUrl = `${OAUTH_CLIENT.authUri}?` + new URLSearchParams({
1081
+ client_id: OAUTH_CLIENT.clientId,
1082
+ redirect_uri: OAUTH_CLIENT.redirectUri,
1083
+ response_type: "code",
1084
+ scope: OAUTH_SCOPES,
1085
+ access_type: "offline",
1086
+ prompt: "consent",
1087
+ login_hint: email,
1088
+ }).toString();
1089
+
1090
+ console.log(`[oauth] Starting browser consent for ${email}`);
1091
+ const code = await bridge.app.startOAuth(authUrl);
1092
+ const tokens = await exchangeCodeForTokens(code);
1093
+ const token = {
1094
+ access_token: tokens.access_token,
1095
+ refresh_token: tokens.refresh_token,
1096
+ expires_at: Date.now() + tokens.expires_in * 1000,
1081
1097
  };
1098
+ await setCachedToken(email, token);
1099
+ console.log(`[oauth] Token obtained for ${email}`);
1100
+ return token.access_token;
1082
1101
  }
1083
1102
 
1084
1103
  // ── GDrive folder lookup ──
@@ -1339,7 +1358,23 @@ async function waitForNativeBridge(timeoutMs: number = 5000): Promise<void> {
1339
1358
  });
1340
1359
  }
1341
1360
 
1342
- export async function initAndroid(): Promise<void> {
1361
+ export function initAndroid(): Promise<void> {
1362
+ // C158: idempotency guard. The 2026-07-22 fold/unfold logit trail showed
1363
+ // ONE WebView reload executing the boot module TWICE — duplicate "bridge
1364
+ // installed", duplicate GDrive lookups, tripled OAuth launches. The
1365
+ // guard lives on window (not a module local) so it holds even when the
1366
+ // module itself is instantiated twice (bundle + package-path specifiers
1367
+ // resolve to distinct module instances).
1368
+ const w = window as any;
1369
+ if (w.__rmfInitAndroid) {
1370
+ console.warn("[android] initAndroid called again — duplicate suppressed (C158)");
1371
+ vlog("C158: duplicate initAndroid call suppressed");
1372
+ return w.__rmfInitAndroid;
1373
+ }
1374
+ return w.__rmfInitAndroid = initAndroidOnce();
1375
+ }
1376
+
1377
+ async function initAndroidOnce(): Promise<void> {
1343
1378
  console.log("[android] Initializing mailx (main-thread mode)...");
1344
1379
 
1345
1380
  // Main-thread path: async I/O (fetch, TCP bridge) doesn't block the UI,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store-web",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",