@oxyhq/core 1.7.0 → 1.8.1

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.
Files changed (35) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -0
  2. package/dist/cjs/HttpService.js +8 -55
  3. package/dist/cjs/crypto/signatureService.js +1 -1
  4. package/dist/cjs/index.js +8 -1
  5. package/dist/cjs/mixins/OxyServices.assets.js +13 -15
  6. package/dist/cjs/mixins/OxyServices.fedcm.js +31 -10
  7. package/dist/cjs/utils/accountUtils.js +49 -0
  8. package/dist/cjs/utils/avatarUtils.js +28 -0
  9. package/dist/esm/.tsbuildinfo +1 -0
  10. package/dist/esm/HttpService.js +8 -55
  11. package/dist/esm/crypto/signatureService.js +1 -1
  12. package/dist/esm/index.js +4 -0
  13. package/dist/esm/mixins/OxyServices.assets.js +13 -15
  14. package/dist/esm/mixins/OxyServices.fedcm.js +31 -10
  15. package/dist/esm/utils/accountUtils.js +44 -0
  16. package/dist/esm/utils/avatarUtils.js +25 -0
  17. package/dist/types/.tsbuildinfo +1 -0
  18. package/dist/types/HttpService.d.ts +5 -57
  19. package/dist/types/OxyServices.d.ts +1 -0
  20. package/dist/types/index.d.ts +3 -0
  21. package/dist/types/mixins/OxyServices.assets.d.ts +12 -3
  22. package/dist/types/mixins/OxyServices.fedcm.d.ts +7 -2
  23. package/dist/types/utils/accountUtils.d.ts +36 -0
  24. package/dist/types/utils/avatarUtils.d.ts +16 -0
  25. package/package.json +2 -2
  26. package/src/HttpService.ts +13 -60
  27. package/src/OxyServices.ts +3 -0
  28. package/src/crypto/signatureService.ts +2 -2
  29. package/src/index.ts +7 -0
  30. package/src/mixins/OxyServices.assets.ts +16 -17
  31. package/src/mixins/OxyServices.fedcm.ts +34 -12
  32. package/src/types/expo-crypto.d.ts +16 -0
  33. package/src/types/expo-secure-store.d.ts +17 -0
  34. package/src/utils/accountUtils.ts +69 -0
  35. package/src/utils/avatarUtils.ts +37 -0
@@ -111,12 +111,14 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
111
111
  debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
112
112
 
113
113
  // Request credential from browser's native identity flow
114
+ // mode: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
114
115
  const credential = await this.requestIdentityCredential({
115
116
  configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
116
117
  clientId,
117
118
  nonce,
118
119
  context: options.context,
119
120
  loginHint,
121
+ mode: 'button',
120
122
  });
121
123
 
122
124
  if (!credential || !credential.token) {
@@ -205,7 +207,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
205
207
  // We intentionally do NOT fall back to optional mediation here because
206
208
  // this runs on app startup — showing browser UI without user action is bad UX.
207
209
  // Optional/interactive mediation should only happen when the user clicks "Sign In".
208
- let credential: { token: string } | null = null;
210
+ let credential: { token: string; isAutoSelected: boolean } | null = null;
209
211
 
210
212
  const loginHint = this.getStoredLoginHint();
211
213
 
@@ -308,7 +310,8 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
308
310
  context?: string;
309
311
  loginHint?: string;
310
312
  mediation?: 'silent' | 'optional' | 'required';
311
- }): Promise<{ token: string } | null> {
313
+ mode?: 'button' | 'widget';
314
+ }): Promise<{ token: string; isAutoSelected: boolean } | null> {
312
315
  const requestedMediation = options.mediation || 'optional';
313
316
  const isInteractive = requestedMediation !== 'silent';
314
317
 
@@ -356,7 +359,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
356
359
  try {
357
360
  debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
358
361
  // Type assertion needed as FedCM types may not be in all TypeScript versions
359
- const credential = (await (navigator.credentials as any).get({
362
+ const credentialOptions: any = {
360
363
  identity: {
361
364
  providers: [
362
365
  {
@@ -370,10 +373,12 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
370
373
  ...(options.loginHint && { loginHint: options.loginHint }),
371
374
  },
372
375
  ],
376
+ ...(options.mode && { mode: options.mode }),
373
377
  },
374
378
  mediation: requestedMediation,
375
379
  signal: controller.signal,
376
- })) as any;
380
+ };
381
+ const credential = (await (navigator.credentials as any).get(credentialOptions)) as any;
377
382
 
378
383
  debug.log('navigator.credentials.get returned:', {
379
384
  hasCredential: !!credential,
@@ -386,8 +391,9 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
386
391
  return null;
387
392
  }
388
393
 
389
- debug.log('Got valid identity credential with token');
390
- return { token: credential.token };
394
+ const isAutoSelected = !!credential.isAutoSelected;
395
+ debug.log('Got valid identity credential with token', { isAutoSelected });
396
+ return { token: credential.token, isAutoSelected };
391
397
  } catch (error) {
392
398
  const errorName = error instanceof Error ? error.name : 'Unknown';
393
399
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -438,25 +444,31 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
438
444
  /**
439
445
  * Revoke FedCM credential (sign out)
440
446
  *
441
- * This tells the browser to forget the FedCM credential for this app.
442
- * The user will need to re-authenticate next time.
447
+ * Uses IdentityCredential.disconnect() to tell the browser to forget
448
+ * the RP-IdP-account association. This resets the "returning account"
449
+ * state, which is required for silent mediation to work again.
443
450
  */
444
451
  async revokeFedCMCredential(): Promise<void> {
452
+ // Read hint before clearing so we can pass it to disconnect()
453
+ const accountHint = this.getStoredLoginHint();
454
+ this.clearLoginHint();
455
+
445
456
  if (!this.isFedCMSupported()) {
446
457
  return;
447
458
  }
448
459
 
449
460
  try {
450
- // FedCM logout API (if available)
451
- if ('IdentityCredential' in window && 'logout' in (window as any).IdentityCredential) {
461
+ if ('IdentityCredential' in window && 'disconnect' in (window as any).IdentityCredential) {
452
462
  const clientId = this.getClientId();
453
- await (window as any).IdentityCredential.logout({
463
+ await (window as any).IdentityCredential.disconnect({
454
464
  configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
455
465
  clientId,
466
+ accountHint: accountHint || '*',
456
467
  });
468
+ debug.log('FedCM credential disconnected');
457
469
  }
458
470
  } catch (error) {
459
- // Silent failure
471
+ debug.log('FedCM disconnect failed (non-critical):', error instanceof Error ? error.message : String(error));
460
472
  }
461
473
  }
462
474
 
@@ -521,6 +533,16 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
521
533
  // Storage full or blocked
522
534
  }
523
535
  }
536
+
537
+ /** @internal */
538
+ public clearLoginHint(): void {
539
+ if (typeof window === 'undefined') return;
540
+ try {
541
+ localStorage.removeItem(FEDCM_LOGIN_HINT_KEY);
542
+ } catch {
543
+ // Storage blocked
544
+ }
545
+ }
524
546
  };
525
547
  }
526
548
 
@@ -0,0 +1,16 @@
1
+ declare module 'expo-crypto' {
2
+ export enum CryptoDigestAlgorithm {
3
+ SHA256 = 'SHA-256',
4
+ SHA384 = 'SHA-384',
5
+ SHA512 = 'SHA-512',
6
+ }
7
+
8
+ export function digestStringAsync(
9
+ algorithm: CryptoDigestAlgorithm,
10
+ data: string,
11
+ ): Promise<string>;
12
+
13
+ export function getRandomBytes(byteCount: number): Uint8Array;
14
+
15
+ export function getRandomBytesAsync(byteCount: number): Promise<Uint8Array>;
16
+ }
@@ -0,0 +1,17 @@
1
+ declare module 'expo-secure-store' {
2
+ export interface SecureStoreOptions {
3
+ keychainAccessible?: number;
4
+ keychainAccessGroup?: string;
5
+ keychainService?: string;
6
+ requireAuthentication?: boolean;
7
+ }
8
+
9
+ export const WHEN_UNLOCKED: number;
10
+ export const AFTER_FIRST_UNLOCK: number;
11
+ export const WHEN_UNLOCKED_THIS_DEVICE_ONLY: number;
12
+ export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: number;
13
+
14
+ export function getItemAsync(key: string, options?: SecureStoreOptions): Promise<string | null>;
15
+ export function setItemAsync(key: string, value: string, options?: SecureStoreOptions): Promise<void>;
16
+ export function deleteItemAsync(key: string, options?: SecureStoreOptions): Promise<void>;
17
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared account types and pure helper functions.
3
+ * Used by both @oxyhq/services (React Native) and @oxyhq/auth (Web) account stores.
4
+ */
5
+
6
+ export interface QuickAccount {
7
+ sessionId: string;
8
+ userId?: string;
9
+ username: string;
10
+ displayName: string;
11
+ avatar?: string;
12
+ avatarUrl?: string;
13
+ }
14
+
15
+ /**
16
+ * Build an ordered array of QuickAccounts from a map and order list.
17
+ */
18
+ export const buildAccountsArray = (
19
+ accounts: Record<string, QuickAccount>,
20
+ order: string[]
21
+ ): QuickAccount[] => {
22
+ const result: QuickAccount[] = [];
23
+ for (const id of order) {
24
+ const account = accounts[id];
25
+ if (account) result.push(account);
26
+ }
27
+ return result;
28
+ };
29
+
30
+ /**
31
+ * Create a QuickAccount from user data returned by the API.
32
+ *
33
+ * @param sessionId - Session identifier
34
+ * @param userData - Raw user object from the API
35
+ * @param existingAccount - Previously cached account (to preserve avatarUrl if unchanged)
36
+ * @param getFileDownloadUrl - Function to generate avatar download URL from file ID
37
+ */
38
+ export const createQuickAccount = (
39
+ sessionId: string,
40
+ userData: {
41
+ name?: { full?: string; first?: string };
42
+ username?: string;
43
+ id?: string;
44
+ _id?: { toString(): string } | string;
45
+ avatar?: string;
46
+ },
47
+ existingAccount?: QuickAccount,
48
+ getFileDownloadUrl?: (fileId: string, variant: string) => string
49
+ ): QuickAccount => {
50
+ const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
51
+ const userId = userData.id || (typeof userData._id === 'string' ? userData._id : userData._id?.toString());
52
+
53
+ // Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
54
+ let avatarUrl: string | undefined;
55
+ if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
56
+ avatarUrl = existingAccount.avatarUrl;
57
+ } else if (userData.avatar && getFileDownloadUrl) {
58
+ avatarUrl = getFileDownloadUrl(userData.avatar, 'thumb');
59
+ }
60
+
61
+ return {
62
+ sessionId,
63
+ userId,
64
+ username: userData.username || '',
65
+ displayName,
66
+ avatar: userData.avatar,
67
+ avatarUrl,
68
+ };
69
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Minimal interface for services that can update asset visibility.
3
+ * Kept loose to avoid mixin type-inference issues with the OxyServices class.
4
+ */
5
+ export interface AssetVisibilityService {
6
+ assetUpdateVisibility(fileId: string, visibility: 'private' | 'public' | 'unlisted'): Promise<unknown>;
7
+ }
8
+
9
+ /**
10
+ * Updates file visibility to public for avatar use.
11
+ * Logs non-404 errors to help debug upload issues.
12
+ *
13
+ * @param fileId - The file ID to update visibility for
14
+ * @param oxyServices - OxyServices instance (or any object with assetUpdateVisibility)
15
+ * @param contextName - Context name for error logging
16
+ */
17
+ export async function updateAvatarVisibility(
18
+ fileId: string | undefined,
19
+ oxyServices: AssetVisibilityService,
20
+ contextName: string = 'AvatarUtils'
21
+ ): Promise<void> {
22
+ if (!fileId || fileId.startsWith('temp-')) {
23
+ return;
24
+ }
25
+
26
+ try {
27
+ await oxyServices.assetUpdateVisibility(fileId, 'public');
28
+ } catch (visError: unknown) {
29
+ // 404 is expected when asset doesn't exist yet — skip logging
30
+ const status = (visError instanceof Error && 'status' in visError)
31
+ ? (visError as Error & { status: number }).status
32
+ : undefined;
33
+ if (status !== 404) {
34
+ console.error(`[${contextName}] Failed to update avatar visibility for ${fileId}:`, visError);
35
+ }
36
+ }
37
+ }