@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.
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/HttpService.js +8 -55
- package/dist/cjs/crypto/signatureService.js +1 -1
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/mixins/OxyServices.assets.js +13 -15
- package/dist/cjs/mixins/OxyServices.fedcm.js +31 -10
- package/dist/cjs/utils/accountUtils.js +49 -0
- package/dist/cjs/utils/avatarUtils.js +28 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/HttpService.js +8 -55
- package/dist/esm/crypto/signatureService.js +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/mixins/OxyServices.assets.js +13 -15
- package/dist/esm/mixins/OxyServices.fedcm.js +31 -10
- package/dist/esm/utils/accountUtils.js +44 -0
- package/dist/esm/utils/avatarUtils.js +25 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/HttpService.d.ts +5 -57
- package/dist/types/OxyServices.d.ts +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +12 -3
- package/dist/types/mixins/OxyServices.fedcm.d.ts +7 -2
- package/dist/types/utils/accountUtils.d.ts +36 -0
- package/dist/types/utils/avatarUtils.d.ts +16 -0
- package/package.json +2 -2
- package/src/HttpService.ts +13 -60
- package/src/OxyServices.ts +3 -0
- package/src/crypto/signatureService.ts +2 -2
- package/src/index.ts +7 -0
- package/src/mixins/OxyServices.assets.ts +16 -17
- package/src/mixins/OxyServices.fedcm.ts +34 -12
- package/src/types/expo-crypto.d.ts +16 -0
- package/src/types/expo-secure-store.d.ts +17 -0
- package/src/utils/accountUtils.ts +69 -0
- 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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
*
|
|
442
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|