@startup-api/cloudflare 0.4.1 → 0.4.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startup-api/cloudflare",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -136,6 +136,9 @@
136
136
 
137
137
  <script>
138
138
  const API_BASE = '/users/api';
139
+ // Atmosphere (atproto) "union" logo mark, drawn on a 0 0 32 32 viewBox.
140
+ const ATMOSPHERE_MARK_PATH =
141
+ 'M16 0C18.1127 1.0372e-05 19.9555 1.15414 20.9332 2.8662C21.1045 3.16613 21.4647 3.31517 21.7979 3.22403C22.2761 3.09321 22.7795 3.02333 23.2992 3.02333C26.4347 3.02335 28.9765 5.56529 28.9765 8.70083C28.9765 9.22048 28.9066 9.72378 28.7758 10.2019C28.6846 10.5351 28.8337 10.8953 29.1336 11.0666C30.8458 12.0442 32 13.8872 32 16C32 16.0822 31.9981 16.1639 31.9946 16.2452C31.9946 16.2456 31.9944 16.246 31.9942 16.2463C31.9939 16.2465 31.9937 16.2469 31.9937 16.2473C31.8599 19.6701 29.2873 22.4 26.1329 22.4L25.9815 22.3979C24.3638 22.353 22.909 21.59 21.8719 20.3888C21.5701 20.0392 20.9949 20.0275 20.6799 20.3652C19.5117 21.6171 17.8474 22.4 16 22.4C12.4654 22.4 9.6 19.5346 9.6 16C9.6 12.4654 12.4654 9.6 16 9.6C17.4368 9.6 18.7629 10.0736 19.831 10.8731C20.0053 11.0036 20.2667 10.8843 20.2667 10.6667C20.2667 10.0776 20.7442 9.6 21.3333 9.6C21.9224 9.6 22.4 10.0776 22.4 10.6667V16C22.4158 18.5458 24.26 20.2666 26.1329 20.2667C27.9447 20.2667 29.7293 18.6564 29.8583 16.2467C29.8583 16.2466 29.8583 16.2464 29.8581 16.2463C29.858 16.2461 29.8579 16.2459 29.8579 16.2458C29.8591 16.2281 29.8597 16.2104 29.8606 16.1927C29.8634 16.1293 29.8654 16.0654 29.8658 16.001C29.8658 16.0006 29.866 16.0003 29.8663 16C29.8665 15.9997 29.8667 15.9994 29.8667 15.999C29.8663 14.5464 28.9914 13.2933 27.7308 12.7465L26.4045 12.1712C26.0876 12.0337 25.937 11.6696 26.0641 11.3484L26.5963 10.0042C26.7549 9.60324 26.8431 9.16479 26.8431 8.70083C26.8431 6.77399 25.3056 5.20644 23.3906 5.15792L23.2992 5.15667C22.8351 5.15667 22.3965 5.24487 21.9956 5.40354L20.6515 5.93568C20.3304 6.06282 19.9663 5.91222 19.8288 5.59538L19.2533 4.26917C18.7234 3.04774 17.5306 2.18836 16.1356 2.13583L16 2.13333C14.547 2.13333 13.2934 3.00824 12.7465 4.26896L12.1712 5.59529C12.0337 5.91217 11.6696 6.06282 11.3484 5.93568L10.0042 5.40354C9.62831 5.25477 9.21941 5.16798 8.7875 5.15771L8.70083 5.15667C6.74349 5.15667 5.15667 6.74349 5.15667 8.70083C5.15667 9.16475 5.24485 9.60323 5.40354 10.0042L5.93568 11.3484C6.06282 11.6696 5.91217 12.0337 5.59529 12.1712L4.26896 12.7465C3.00823 13.2934 2.13333 14.5469 2.13333 16C2.13333 17.4529 3.00832 18.7063 4.26917 19.2533L5.59538 19.8288C5.91222 19.9663 6.06282 20.3304 5.93568 20.6515L5.40354 21.9956C5.24487 22.3965 5.15667 22.835 5.15667 23.2992C5.15667 25.2565 6.74349 26.8433 8.70083 26.8433C9.1644 26.8433 9.60287 26.7551 10.0042 26.5963L11.3484 26.0641C11.6696 25.937 12.0337 26.0876 12.1712 26.4045L12.7465 27.7308C13.2934 28.9917 14.547 29.8667 16 29.8667C17.4529 29.8667 18.7063 28.9918 19.2533 27.7308L19.6555 26.8037C19.8871 26.2698 20.5083 26.0256 21.0415 26.2589C21.561 26.4862 21.8066 27.0847 21.5966 27.6115L21.2104 28.5798C20.3374 30.5922 18.3333 32 16 32C13.8872 32 12.0442 30.8458 11.0666 29.1336C10.8953 28.8337 10.5351 28.6846 10.2019 28.7758C9.72378 28.9067 9.22049 28.9767 8.70083 28.9767C5.56528 28.9767 3.02333 26.4347 3.02333 23.2992C3.02333 22.7795 3.09321 22.2761 3.22403 21.7979C3.31516 21.4647 3.16613 21.1045 2.86619 20.9332C1.15412 19.9555 0 18.1127 0 16C5.927e-07 13.8873 1.1541 12.0443 2.8662 11.0666C3.16613 10.8953 3.31517 10.5351 3.22403 10.2019C3.09323 9.72379 3.02333 9.22047 3.02333 8.70083C3.02333 5.56528 5.56528 3.02333 8.70083 3.02333C9.22047 3.02334 9.72379 3.09323 10.2019 3.22404C10.5351 3.31517 10.8953 3.16614 11.0666 2.86621C12.0443 1.15411 13.8873 0 16 0ZM16 11.7333C13.6436 11.7333 11.7333 13.6436 11.7333 16C11.7333 18.3564 13.6436 20.2667 16 20.2667C18.3564 20.2667 20.2667 18.3564 20.2667 16C20.2667 13.6436 18.3564 11.7333 16 11.7333Z';
139
142
  let currentProvider = null;
140
143
  let initialProfile = {};
141
144
  let isInitialLoad = true;
@@ -377,10 +380,10 @@
377
380
  </div>
378
381
  <div>
379
382
  <div style="font-weight: 600;">
380
- ${c.provider.charAt(0).toUpperCase() + c.provider.slice(1)}
383
+ ${providerLabel(c.provider)}
381
384
  ${isCurrent ? '<span class="current-badge">logged in</span>' : ''}
382
385
  </div>
383
- <div style="font-size: 0.8rem; color: var(--text-faint);">${c.email || c.subject_id}</div>
386
+ <div style="font-size: 0.8rem; color: var(--text-faint);">${credentialIdentifier(c)}</div>
384
387
  </div>
385
388
  </div>
386
389
  <button class="remove-btn" onclick="removeCredential('${c.provider}')" ${isCurrent || credentials.length === 1 ? 'disabled title="' + (isCurrent ? 'Cannot remove the method you are currently logged in with' : 'Cannot remove your last login method') + '"' : ''}>
@@ -434,13 +437,25 @@
434
437
  <path d="M14.82 2.41c3.96 0 7.18 3.24 7.18 7.21 0 3.96-3.22 7.18-7.18 7.18-3.97 0-7.21-3.22-7.21-7.18 0-3.97 3.24-7.21 7.21-7.21M2 21.6h3.5V2.41H2V21.6z" fill="currentColor"/>
435
438
  </svg>`;
436
439
  } else if (provider === 'atproto') {
437
- return `<svg viewBox="0 0 24 24" width="24" height="24" style="color: #0085FF;">
438
- <path d="M12 10.5C10.9 8.4 8.2 6.3 6.3 6c-1.5-.2-1.8.7-1.5 2 .2 1 1.5 5 2.3 6 .9 1.2 2 1.4 3 1.2-1.7.3-3.2 1-1.2 3 .9.9 1.6.3 2.1-.6.5-1 .8-2.1 1-2.6.2.5.5 1.6 1 2.6.5.9 1.2 1.5 2.1.6 2-2 .5-2.7-1.2-3 1 .2 2.1 0 3-1.2.8-1 2.1-5 2.3-6 .3-1.3 0-2.2-1.5-2-1.9.3-4.6 2.4-5.7 4.5z" fill="currentColor"/>
440
+ return `<svg viewBox="0 0 32 32" width="24" height="24" style="color: #4a8ad4;">
441
+ <path fill-rule="evenodd" clip-rule="evenodd" d="${ATMOSPHERE_MARK_PATH}" fill="currentColor"/>
439
442
  </svg>`;
440
443
  }
441
444
  return '';
442
445
  }
443
446
 
447
+ // atproto is branded "Atmosphere / ATproto"; the identifier shows the handle with the DID in parens.
448
+ function providerLabel(provider) {
449
+ if (provider === 'atproto') return 'Atmosphere / ATproto';
450
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
451
+ }
452
+ function credentialIdentifier(c) {
453
+ if (c.provider === 'atproto') {
454
+ return c.handle ? `${c.handle} (${c.subject_id})` : c.subject_id;
455
+ }
456
+ return c.email || c.subject_id;
457
+ }
458
+
444
459
  loadProfile();
445
460
  // loadCredentials(); // Handled within loadProfile if data exists
446
461
  </script>
@@ -239,7 +239,7 @@ export class AtprotoProvider extends OAuthProvider {
239
239
  const did = tokenData.sub || flow.did;
240
240
  const { name, picture } = await fetchProfile(flow.pds, did, flow.handle);
241
241
 
242
- const profile: UserProfile = { id: did, name: name || flow.handle || did, picture };
242
+ const profile: UserProfile = { id: did, name: name || flow.handle || did, picture, handle: flow.handle };
243
243
  const token: OAuthTokenResponse = {
244
244
  access_token: tokenData.access_token,
245
245
  refresh_token: tokenData.refresh_token,
@@ -304,10 +304,12 @@ export class AtprotoProvider extends OAuthProvider {
304
304
  status,
305
305
  headers: {
306
306
  'Content-Type': 'text/html; charset=utf-8',
307
- // Same hardening as the auth error page: only same-origin styles/form, never framed, never cached
308
- // (the re-rendered form reflects the user-supplied handle).
309
- 'Content-Security-Policy':
310
- "default-src 'none'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
307
+ // Hardening: same-origin styles only, never framed, never cached (the form reflects the
308
+ // user-supplied handle). NOTE: deliberately NO `form-action` directive — submitting this form
309
+ // hits our endpoint, which 302-redirects to the user's *own* authorization server (any PDS).
310
+ // `form-action` is enforced across the whole redirect chain, so `'self'` (or any fixed list)
311
+ // would block that cross-origin redirect and the login would silently fail.
312
+ 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
311
313
  'X-Content-Type-Options': 'nosniff',
312
314
  'Referrer-Policy': 'no-referrer',
313
315
  'Cache-Control': 'no-store',
@@ -13,6 +13,8 @@ export interface UserProfile {
13
13
  name?: string;
14
14
  picture?: string;
15
15
  verified_email?: boolean;
16
+ /** atproto only: the user's handle (e.g. `alice.bsky.social`), distinct from the `id` (a DID). */
17
+ handle?: string;
16
18
  }
17
19
 
18
20
  import type { Entitlements } from '../entitlements/types';
@@ -193,10 +193,10 @@ function renderCredentialsList(credentials: any[], currentProvider?: string): st
193
193
  </div>
194
194
  <div>
195
195
  <div style="font-weight: 600;">
196
- ${c.provider.charAt(0).toUpperCase() + c.provider.slice(1)}
196
+ ${providerLabel(c.provider)}
197
197
  ${isCurrent ? '<span class="current-badge">logged in</span>' : ''}
198
198
  </div>
199
- <div style="font-size: 0.8rem; color: #666;">${c.email || c.subject_id}</div>
199
+ <div style="font-size: 0.8rem; color: #666;">${credentialIdentifier(c)}</div>
200
200
  </div>
201
201
  </div>
202
202
  <button class="remove-btn" onclick="removeCredential('${c.provider}')" ${isCurrent || credentials.length === 1 ? 'disabled title="' + (isCurrent ? 'Cannot remove the method you are currently logged in with' : 'Cannot remove your last login method') + '"' : ''}>
@@ -208,6 +208,20 @@ function renderCredentialsList(credentials: any[], currentProvider?: string): st
208
208
  .join('');
209
209
  }
210
210
 
211
+ /** Human-friendly provider name. atproto is branded "Atmosphere / ATproto"; others are capitalized. */
212
+ function providerLabel(provider: string): string {
213
+ if (provider === 'atproto') return 'Atmosphere / ATproto';
214
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
215
+ }
216
+
217
+ /** The identifier shown under the provider name. atproto shows the handle with the DID in parens. */
218
+ function credentialIdentifier(c: any): string {
219
+ if (c.provider === 'atproto') {
220
+ return c.handle ? `${c.handle} (${c.subject_id})` : c.subject_id;
221
+ }
222
+ return c.email || c.subject_id;
223
+ }
224
+
211
225
  function getProviderIcon(provider: string): string {
212
226
  if (provider === 'google') {
213
227
  return '<svg viewBox="0 0 24 24" width="24" height="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.66l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>';
@@ -233,6 +233,7 @@ export class UserDO extends DurableObject {
233
233
  provider: row.provider,
234
234
  subject_id: c.subject_id,
235
235
  email: c.profile_data?.email,
236
+ handle: c.profile_data?.handle,
236
237
  created_at: c.created_at,
237
238
  })),
238
239
  );