tfrere HF Staff Cursor commited on
Commit
638248e
·
1 Parent(s): a7e92df

feat: use parent postMessage for likes instead of API calls

Browse files

Use the HF-provided postMessage protocol to like spaces from within
the iframe. The parent frame (huggingface.co) handles auth via session
cookies, bypassing CORS and OAuth token issues entirely.

Protocol: parent.postMessage({type: "LIKE_REPO_REQUEST", repo: {type: "space", name: "..."}})
Response: LIKE_REPO_RESPONSE with status (done/already_liked/not_logged_in) or error

Also removes write-discussions OAuth scope (no longer needed).

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (2) hide show
  1. README.md +0 -2
  2. src/context/AuthContext.jsx +68 -35
README.md CHANGED
@@ -6,8 +6,6 @@ colorTo: purple
6
  sdk: docker
7
  pinned: true
8
  hf_oauth: true
9
- hf_oauth_scopes:
10
- - write-discussions
11
  thumbnail: >-
12
  https://cdn-uploads.huggingface.co/production/uploads/671faa3a541a76b548647676/XWNDlOu0R4fHXR0kCW3Wd.png
13
  short_description: All about Reachy Mini, from building to getting started
 
6
  sdk: docker
7
  pinned: true
8
  hf_oauth: true
 
 
9
  thumbnail: >-
10
  https://cdn-uploads.huggingface.co/production/uploads/671faa3a541a76b548647676/XWNDlOu0R4fHXR0kCW3Wd.png
11
  short_description: All about Reachy Mini, from building to getting started
src/context/AuthContext.jsx CHANGED
@@ -5,6 +5,9 @@ const AuthContext = createContext(null);
5
 
6
  const HF_API = 'https://huggingface.co';
7
 
 
 
 
8
  /**
9
  * Fetch OAuth config (clientId, scopes) from our Express server.
10
  * Docker Spaces don't auto-inject window.huggingface.variables like static Spaces,
@@ -41,13 +44,40 @@ async function fetchUserLikedSpaces(username) {
41
  }
42
  }
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  // Provider component
45
  export function AuthProvider({ children }) {
46
  const [user, setUser] = useState(null); // { name, avatarUrl }
47
  const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
48
  const [isLoading, setIsLoading] = useState(true);
49
  const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
50
- const pendingLikes = useRef(new Set()); // Track in-flight like/unlike requests
51
 
52
  // On mount: fetch OAuth config + check if user just completed OAuth redirect
53
  useEffect(() => {
@@ -65,12 +95,11 @@ export function AuthProvider({ children }) {
65
  // Check if user just completed OAuth redirect
66
  const oauthResult = await oauthHandleRedirectIfPresent();
67
  if (oauthResult) {
68
- const { accessToken, userInfo } = oauthResult;
69
  const userData = {
70
  name: userInfo.name,
71
  preferredUsername: userInfo.preferred_username || userInfo.name,
72
  avatarUrl: userInfo.picture,
73
- accessToken: accessToken,
74
  };
75
  setUser(userData);
76
 
@@ -125,65 +154,68 @@ export function AuthProvider({ children }) {
125
  );
126
 
127
  /**
128
- * Toggle like/unlike with optimistic update.
129
- * Uses OAuth Bearer token (requires write-discussions scope).
 
130
  */
131
  const toggleLike = useCallback(
132
  async (spaceId) => {
133
- if (!user) {
134
- login();
135
- return;
136
- }
137
-
138
  const spaceIdLower = spaceId?.toLowerCase();
139
  if (!spaceIdLower) return;
140
 
141
- // Prevent duplicate requests for the same space
 
 
 
 
 
 
 
 
 
142
  if (pendingLikes.current.has(spaceIdLower)) return;
143
  pendingLikes.current.add(spaceIdLower);
144
 
145
- const wasLiked = likedSpaceIds.has(spaceIdLower);
146
- const method = wasLiked ? 'DELETE' : 'POST';
147
-
148
  // Optimistic update
149
  setLikedSpaceIds((prev) => {
150
  const next = new Set(prev);
151
- if (wasLiked) {
152
- next.delete(spaceIdLower);
153
- } else {
154
- next.add(spaceIdLower);
155
- }
156
  return next;
157
  });
158
 
159
  try {
160
- const response = await fetch(`${HF_API}/api/spaces/${spaceId}/like`, {
161
- method,
162
- headers: {
163
- Authorization: `Bearer ${user.accessToken}`,
164
- },
165
- });
166
 
167
- if (!response.ok) {
168
- throw new Error(`Like API returned ${response.status}`);
169
  }
 
 
 
 
 
 
 
 
170
  } catch (err) {
171
- console.error(`[Auth] Failed to ${wasLiked ? 'unlike' : 'like'} ${spaceId}:`, err);
172
- // Revert optimistic update on error
 
173
  setLikedSpaceIds((prev) => {
174
  const reverted = new Set(prev);
175
- if (wasLiked) {
176
- reverted.add(spaceIdLower);
177
- } else {
178
- reverted.delete(spaceIdLower);
179
- }
180
  return reverted;
181
  });
 
 
 
 
 
182
  } finally {
183
  pendingLikes.current.delete(spaceIdLower);
184
  }
185
  },
186
- [user, likedSpaceIds, login]
187
  );
188
 
189
  return (
@@ -193,6 +225,7 @@ export function AuthProvider({ children }) {
193
  isLoggedIn: !!user,
194
  isLoading,
195
  isOAuthAvailable: !!oauthConfig?.clientId,
 
196
  likedSpaceIds,
197
  login,
198
  logout,
 
5
 
6
  const HF_API = 'https://huggingface.co';
7
 
8
+ // Whether we're running inside an iframe (i.e. embedded on huggingface.co)
9
+ const isInIframe = typeof window !== 'undefined' && window.parent !== window;
10
+
11
  /**
12
  * Fetch OAuth config (clientId, scopes) from our Express server.
13
  * Docker Spaces don't auto-inject window.huggingface.variables like static Spaces,
 
44
  }
45
  }
46
 
47
+ /**
48
+ * Send a like request to the HF parent frame via postMessage.
49
+ * Returns a Promise that resolves with the response data.
50
+ * The parent frame (huggingface.co) handles auth via session cookies.
51
+ */
52
+ function likeViaPostMessage(spaceId) {
53
+ return new Promise((resolve, reject) => {
54
+ const timeout = setTimeout(() => {
55
+ window.removeEventListener('message', handler);
56
+ reject(new Error('Like request timed out'));
57
+ }, 5000);
58
+
59
+ function handler(event) {
60
+ if (event.data?.type !== 'LIKE_REPO_RESPONSE') return;
61
+ clearTimeout(timeout);
62
+ window.removeEventListener('message', handler);
63
+ resolve(event.data);
64
+ }
65
+
66
+ window.addEventListener('message', handler);
67
+ window.parent.postMessage(
68
+ { type: 'LIKE_REPO_REQUEST', repo: { type: 'space', name: spaceId } },
69
+ '*'
70
+ );
71
+ });
72
+ }
73
+
74
  // Provider component
75
  export function AuthProvider({ children }) {
76
  const [user, setUser] = useState(null); // { name, avatarUrl }
77
  const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
78
  const [isLoading, setIsLoading] = useState(true);
79
  const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
80
+ const pendingLikes = useRef(new Set()); // Track in-flight like requests
81
 
82
  // On mount: fetch OAuth config + check if user just completed OAuth redirect
83
  useEffect(() => {
 
95
  // Check if user just completed OAuth redirect
96
  const oauthResult = await oauthHandleRedirectIfPresent();
97
  if (oauthResult) {
98
+ const { userInfo } = oauthResult;
99
  const userData = {
100
  name: userInfo.name,
101
  preferredUsername: userInfo.preferred_username || userInfo.name,
102
  avatarUrl: userInfo.picture,
 
103
  };
104
  setUser(userData);
105
 
 
154
  );
155
 
156
  /**
157
+ * Like a space via the HF parent frame postMessage protocol.
158
+ * The parent (huggingface.co) handles auth via session cookies.
159
+ * Like-only (no unlike) — if already liked, it's a no-op.
160
  */
161
  const toggleLike = useCallback(
162
  async (spaceId) => {
 
 
 
 
 
163
  const spaceIdLower = spaceId?.toLowerCase();
164
  if (!spaceIdLower) return;
165
 
166
+ // Already liked → no-op (postMessage only supports like, not unlike)
167
+ if (likedSpaceIds.has(spaceIdLower)) return;
168
+
169
+ // Not in iframe → can't use postMessage, prompt OAuth login as fallback
170
+ if (!isInIframe) {
171
+ console.warn('[Auth] Not in iframe, postMessage unavailable');
172
+ return;
173
+ }
174
+
175
+ // Prevent duplicate requests
176
  if (pendingLikes.current.has(spaceIdLower)) return;
177
  pendingLikes.current.add(spaceIdLower);
178
 
 
 
 
179
  // Optimistic update
180
  setLikedSpaceIds((prev) => {
181
  const next = new Set(prev);
182
+ next.add(spaceIdLower);
 
 
 
 
183
  return next;
184
  });
185
 
186
  try {
187
+ const result = await likeViaPostMessage(spaceId);
 
 
 
 
 
188
 
189
+ if (result.error) {
190
+ throw new Error(`${result.error.code}: ${result.error.message}`);
191
  }
192
+
193
+ if (result.status === 'not_logged_in') {
194
+ // User not logged in to HF → revert and prompt login
195
+ throw new Error('not_logged_in');
196
+ }
197
+
198
+ // "done" or "already_liked" → success
199
+ console.log(`[Auth] Liked ${spaceId}: ${result.status}`, result.likes != null ? `(${result.likes} likes)` : '');
200
  } catch (err) {
201
+ console.error(`[Auth] Failed to like ${spaceId}:`, err.message);
202
+
203
+ // Revert optimistic update
204
  setLikedSpaceIds((prev) => {
205
  const reverted = new Set(prev);
206
+ reverted.delete(spaceIdLower);
 
 
 
 
207
  return reverted;
208
  });
209
+
210
+ // If not logged in, prompt OAuth login
211
+ if (err.message === 'not_logged_in') {
212
+ login();
213
+ }
214
  } finally {
215
  pendingLikes.current.delete(spaceIdLower);
216
  }
217
  },
218
+ [likedSpaceIds, login]
219
  );
220
 
221
  return (
 
225
  isLoggedIn: !!user,
226
  isLoading,
227
  isOAuthAvailable: !!oauthConfig?.clientId,
228
+ isInIframe,
229
  likedSpaceIds,
230
  login,
231
  logout,