@oxyhq/services 28.0.0 → 29.0.0
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/NOTICE +10 -9
- package/README.md +65 -2
- package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt +178 -0
- package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionProvider.kt +99 -0
- package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionStore.kt +115 -0
- package/expo-module.config.json +1 -0
- package/lib/commonjs/index.js +60 -16
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/boot/runProviderColdBoot.js +7 -0
- package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
- package/lib/commonjs/ui/client.js +12 -7
- package/lib/commonjs/ui/client.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAccountDialogScreen.js +10 -27
- package/lib/commonjs/ui/components/OxyAccountDialogScreen.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js +132 -44
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +4 -2
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/SurfaceScreen.js +31 -3
- package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +209 -84
- package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/SignInEntryView.js +25 -7
- package/lib/commonjs/ui/components/authChooser/SignInEntryView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/primitives.js +35 -22
- package/lib/commonjs/ui/components/authChooser/primitives.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/types.js +11 -0
- package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +257 -306
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +27 -37
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/oxyContextHelpers.js +0 -29
- package/lib/commonjs/ui/context/oxyContextHelpers.js.map +1 -1
- package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -17
- package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +42 -0
- package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -0
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +0 -19
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useDeviceSwitcher.js +57 -0
- package/lib/commonjs/ui/hooks/useDeviceSwitcher.js.map +1 -0
- package/lib/commonjs/ui/hooks/useI18n.js +22 -3
- package/lib/commonjs/ui/hooks/useI18n.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +46 -153
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/index.js +12 -7
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +85 -73
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaces.js +8 -7
- package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
- package/lib/commonjs/ui/runtime/createOxyRuntime.js +433 -0
- package/lib/commonjs/ui/runtime/createOxyRuntime.js.map +1 -0
- package/lib/commonjs/ui/runtime/index.js +63 -0
- package/lib/commonjs/ui/runtime/index.js.map +1 -0
- package/lib/commonjs/ui/runtime/runtimeContext.js +83 -0
- package/lib/commonjs/ui/runtime/runtimeContext.js.map +1 -0
- package/lib/commonjs/ui/runtime/types.js +6 -0
- package/lib/commonjs/ui/runtime/types.js.map +1 -0
- package/lib/commonjs/ui/runtime/useRuntimeSnapshot.js +57 -0
- package/lib/commonjs/ui/runtime/useRuntimeSnapshot.js.map +1 -0
- package/lib/commonjs/ui/server.js +1 -2
- package/lib/commonjs/ui/server.js.map +1 -1
- package/lib/commonjs/ui/session/authStore.js +45 -9
- package/lib/commonjs/ui/session/authStore.js.map +1 -1
- package/lib/commonjs/ui/session/sharedDeviceCredentialStore.js +306 -0
- package/lib/commonjs/ui/session/sharedDeviceCredentialStore.js.map +1 -0
- package/lib/commonjs/ui/stores/authStore.js +154 -55
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +2 -36
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/index.js +16 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/boot/runProviderColdBoot.js +7 -0
- package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
- package/lib/module/ui/client.js +1 -2
- package/lib/module/ui/client.js.map +1 -1
- package/lib/module/ui/components/OxyAccountDialogScreen.js +10 -27
- package/lib/module/ui/components/OxyAccountDialogScreen.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +134 -44
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +5 -3
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/SurfaceScreen.js +33 -5
- package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/module/ui/components/authChooser/AccountsMenuView.js +210 -85
- package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
- package/lib/module/ui/components/authChooser/SignInEntryView.js +25 -7
- package/lib/module/ui/components/authChooser/SignInEntryView.js.map +1 -1
- package/lib/module/ui/components/authChooser/primitives.js +35 -22
- package/lib/module/ui/components/authChooser/primitives.js.map +1 -1
- package/lib/module/ui/components/authChooser/types.js +11 -0
- package/lib/module/ui/components/authChooser/types.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +251 -303
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +27 -37
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/oxyContextHelpers.js +0 -28
- package/lib/module/ui/context/oxyContextHelpers.js.map +1 -1
- package/lib/module/ui/context/useOxyAccountGraph.js +6 -17
- package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/module/ui/hooks/accountDialogSnapshot.js +38 -0
- package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +0 -19
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/useDeviceSwitcher.js +53 -0
- package/lib/module/ui/hooks/useDeviceSwitcher.js.map +1 -0
- package/lib/module/ui/hooks/useI18n.js +24 -4
- package/lib/module/ui/hooks/useI18n.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +47 -155
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/index.js +1 -2
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +84 -72
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/navigation/surfaces.js +6 -6
- package/lib/module/ui/navigation/surfaces.js.map +1 -1
- package/lib/module/ui/runtime/createOxyRuntime.js +430 -0
- package/lib/module/ui/runtime/createOxyRuntime.js.map +1 -0
- package/lib/module/ui/runtime/index.js +15 -0
- package/lib/module/ui/runtime/index.js.map +1 -0
- package/lib/module/ui/runtime/runtimeContext.js +75 -0
- package/lib/module/ui/runtime/runtimeContext.js.map +1 -0
- package/lib/module/ui/runtime/types.js +4 -0
- package/lib/module/ui/runtime/types.js.map +1 -0
- package/lib/module/ui/runtime/useRuntimeSnapshot.js +52 -0
- package/lib/module/ui/runtime/useRuntimeSnapshot.js.map +1 -0
- package/lib/module/ui/server.js +0 -1
- package/lib/module/ui/server.js.map +1 -1
- package/lib/module/ui/session/authStore.js +46 -11
- package/lib/module/ui/session/authStore.js.map +1 -1
- package/lib/module/ui/session/sharedDeviceCredentialStore.js +302 -0
- package/lib/module/ui/session/sharedDeviceCredentialStore.js.map +1 -0
- package/lib/module/ui/stores/authStore.js +154 -55
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +2 -35
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +5 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/client.d.ts +1 -2
- package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts +11 -4
- package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts +3 -1
- package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts +18 -6
- package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +33 -1
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +35 -4
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +8 -12
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextHelpers.d.ts +0 -2
- package/lib/typescript/commonjs/ui/context/oxyContextHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +9 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -3
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts +15 -0
- package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useDeviceSwitcher.d.ts +49 -0
- package/lib/typescript/commonjs/ui/hooks/useDeviceSwitcher.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts +13 -0
- package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts +23 -18
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +1 -2
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +15 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/runtime/createOxyRuntime.d.ts +144 -0
- package/lib/typescript/commonjs/ui/runtime/createOxyRuntime.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/runtime/index.d.ts +16 -0
- package/lib/typescript/commonjs/ui/runtime/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/runtime/runtimeContext.d.ts +43 -0
- package/lib/typescript/commonjs/ui/runtime/runtimeContext.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/runtime/types.d.ts +89 -0
- package/lib/typescript/commonjs/ui/runtime/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/runtime/useRuntimeSnapshot.d.ts +23 -0
- package/lib/typescript/commonjs/ui/runtime/useRuntimeSnapshot.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/server.d.ts +0 -1
- package/lib/typescript/commonjs/ui/server.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/authStore.d.ts +60 -9
- package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/sharedDeviceCredentialStore.d.ts +32 -0
- package/lib/typescript/commonjs/ui/session/sharedDeviceCredentialStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/stores/authStore.d.ts +49 -5
- package/lib/typescript/commonjs/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +26 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts +2 -10
- package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +5 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
- package/lib/typescript/module/ui/client.d.ts +1 -2
- package/lib/typescript/module/ui/client.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts +11 -4
- package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts +3 -1
- package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/primitives.d.ts +18 -6
- package/lib/typescript/module/ui/components/authChooser/primitives.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/types.d.ts +33 -1
- package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +35 -4
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +8 -12
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextHelpers.d.ts +0 -2
- package/lib/typescript/module/ui/context/oxyContextHelpers.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +9 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -3
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts +15 -0
- package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useDeviceSwitcher.d.ts +49 -0
- package/lib/typescript/module/ui/hooks/useDeviceSwitcher.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useI18n.d.ts +13 -0
- package/lib/typescript/module/ui/hooks/useI18n.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts +23 -18
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +1 -2
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +15 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
- package/lib/typescript/module/ui/runtime/createOxyRuntime.d.ts +144 -0
- package/lib/typescript/module/ui/runtime/createOxyRuntime.d.ts.map +1 -0
- package/lib/typescript/module/ui/runtime/index.d.ts +16 -0
- package/lib/typescript/module/ui/runtime/index.d.ts.map +1 -0
- package/lib/typescript/module/ui/runtime/runtimeContext.d.ts +43 -0
- package/lib/typescript/module/ui/runtime/runtimeContext.d.ts.map +1 -0
- package/lib/typescript/module/ui/runtime/types.d.ts +89 -0
- package/lib/typescript/module/ui/runtime/types.d.ts.map +1 -0
- package/lib/typescript/module/ui/runtime/useRuntimeSnapshot.d.ts +23 -0
- package/lib/typescript/module/ui/runtime/useRuntimeSnapshot.d.ts.map +1 -0
- package/lib/typescript/module/ui/server.d.ts +0 -1
- package/lib/typescript/module/ui/server.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/authStore.d.ts +60 -9
- package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/sharedDeviceCredentialStore.d.ts +32 -0
- package/lib/typescript/module/ui/session/sharedDeviceCredentialStore.d.ts.map +1 -0
- package/lib/typescript/module/ui/stores/authStore.d.ts +49 -5
- package/lib/typescript/module/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +26 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/avatarUtils.d.ts +2 -10
- package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +6 -7
- package/plugins/withSharedDeviceSessionProvider.js +102 -0
- package/plugins/withSharedDeviceSessionReader.js +57 -0
- package/src/__tests__/esmSourcePurity.test.ts +61 -0
- package/src/index.ts +31 -9
- package/src/ui/boot/runProviderColdBoot.ts +7 -0
- package/src/ui/client.ts +1 -2
- package/src/ui/components/OxyAccountDialogScreen.tsx +10 -29
- package/src/ui/components/OxyAuthChooser.tsx +149 -50
- package/src/ui/components/OxyProvider.tsx +7 -5
- package/src/ui/components/SurfaceScreen.tsx +28 -4
- package/src/ui/components/authChooser/AccountsMenuView.tsx +246 -96
- package/src/ui/components/authChooser/SignInEntryView.tsx +31 -8
- package/src/ui/components/authChooser/primitives.tsx +37 -23
- package/src/ui/components/authChooser/types.ts +34 -1
- package/src/ui/context/OxyContext.tsx +269 -324
- package/src/ui/context/hooks/useAuthOperations.ts +32 -35
- package/src/ui/context/oxyContextHelpers.ts +0 -30
- package/src/ui/context/oxyContextTypes.ts +9 -1
- package/src/ui/context/useOxyAccountGraph.ts +5 -19
- package/src/ui/hooks/accountDialogSnapshot.ts +37 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +0 -19
- package/src/ui/hooks/queries/__tests__/userCache.test.ts +12 -1
- package/src/ui/hooks/useDeviceSwitcher.ts +110 -0
- package/src/ui/hooks/useI18n.ts +23 -3
- package/src/ui/hooks/useSessionManagement.ts +62 -211
- package/src/ui/index.ts +1 -2
- package/src/ui/navigation/routes.ts +85 -75
- package/src/ui/navigation/surfaces.ts +6 -9
- package/src/ui/runtime/createOxyRuntime.ts +623 -0
- package/src/ui/runtime/index.ts +34 -0
- package/src/ui/runtime/runtimeContext.tsx +111 -0
- package/src/ui/runtime/types.ts +110 -0
- package/src/ui/runtime/useRuntimeSnapshot.ts +52 -0
- package/src/ui/server.ts +0 -1
- package/src/ui/session/__tests__/authStore.test.ts +90 -0
- package/src/ui/session/__tests__/sharedDeviceCredentialStore.test.ts +289 -0
- package/src/ui/session/authStore.ts +78 -9
- package/src/ui/session/sharedDeviceCredentialStore.ts +318 -0
- package/src/ui/stores/authStore.ts +171 -43
- package/src/ui/types/navigation.ts +26 -0
- package/src/ui/utils/avatarUtils.ts +2 -37
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +0 -64
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +0 -1
- package/lib/commonjs/ui/stores/accountStore.js +0 -221
- package/lib/commonjs/ui/stores/accountStore.js.map +0 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +0 -60
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +0 -1
- package/lib/module/ui/stores/accountStore.js +0 -213
- package/lib/module/ui/stores/accountStore.js.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +0 -23
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/stores/accountStore.d.ts +0 -27
- package/lib/typescript/commonjs/ui/stores/accountStore.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +0 -23
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +0 -1
- package/lib/typescript/module/ui/stores/accountStore.d.ts +0 -27
- package/lib/typescript/module/ui/stores/accountStore.d.ts.map +0 -1
- package/src/ui/hooks/useSwitchableAccounts.ts +0 -75
- package/src/ui/stores/accountStore.ts +0 -264
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { DeviceDirectory } from '@oxyhq/contracts';
|
|
3
|
+
import type { DeviceContext, User } from '@oxyhq/core';
|
|
4
|
+
import type { OxyRuntime } from './createOxyRuntime';
|
|
5
|
+
import type { OxyRuntimeStatus } from './types';
|
|
6
|
+
import { useRuntimeSelector, useRuntimeSnapshot } from './useRuntimeSnapshot';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The runtime reference itself, as a context value that NEVER changes identity
|
|
10
|
+
* (ADR 0004).
|
|
11
|
+
*
|
|
12
|
+
* It is deliberately a second context beside the big `OxyContextState` one.
|
|
13
|
+
* That value is rebuilt whenever any of its fifty-odd members moves — a locale,
|
|
14
|
+
* a dialog opening — and every hook reading it re-renders with it. Hooks
|
|
15
|
+
* hanging off this one subscribe to the runtime with a selector instead, so
|
|
16
|
+
* only a change to what they actually selected wakes them. Retiring the wide
|
|
17
|
+
* context is Phase 8; this is what its replacements are built on.
|
|
18
|
+
*/
|
|
19
|
+
const OxyRuntimeHandleContext = createContext<OxyRuntime | null>(null);
|
|
20
|
+
|
|
21
|
+
export const OxyRuntimeHandleProvider = OxyRuntimeHandleContext.Provider;
|
|
22
|
+
|
|
23
|
+
const RUNTIME_MISSING_MESSAGE =
|
|
24
|
+
'useOxyRuntime() was called outside <OxyProvider>. Mount <OxyProvider clientId="…"> above this component.';
|
|
25
|
+
|
|
26
|
+
export class OxyRuntimeMissingError extends Error {
|
|
27
|
+
readonly code = 'oxy_runtime_missing';
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
super(RUNTIME_MISSING_MESSAGE);
|
|
31
|
+
this.name = 'OxyRuntimeMissingError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The runtime this provider owns. THROWS when no `<OxyProvider>` is mounted. */
|
|
36
|
+
export function useOxyRuntime(): OxyRuntime {
|
|
37
|
+
const runtime = useContext(OxyRuntimeHandleContext);
|
|
38
|
+
if (runtime === null) {
|
|
39
|
+
throw new OxyRuntimeMissingError();
|
|
40
|
+
}
|
|
41
|
+
return runtime;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ActiveAccount {
|
|
45
|
+
/**
|
|
46
|
+
* The human whose authentication backs the session — the audit actor. Equal
|
|
47
|
+
* to {@link ActiveAccount.account} unless a delegated context is active.
|
|
48
|
+
*/
|
|
49
|
+
principal: User | null;
|
|
50
|
+
/** The account the session acts AS — what a profile header renders. */
|
|
51
|
+
account: User | null;
|
|
52
|
+
/** The resolved `principal acting as account` pair, when a directory is held. */
|
|
53
|
+
context: DeviceContext | null;
|
|
54
|
+
/** Whether the actor and the subject are different accounts. */
|
|
55
|
+
isDelegated: boolean;
|
|
56
|
+
status: OxyRuntimeStatus;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function selectActiveAccount(snapshot: {
|
|
60
|
+
principal: User | null;
|
|
61
|
+
account: User | null;
|
|
62
|
+
activeContext: DeviceContext | null;
|
|
63
|
+
status: OxyRuntimeStatus;
|
|
64
|
+
}): ActiveAccount {
|
|
65
|
+
return {
|
|
66
|
+
principal: snapshot.principal,
|
|
67
|
+
account: snapshot.account,
|
|
68
|
+
context: snapshot.activeContext,
|
|
69
|
+
isDelegated: snapshot.activeContext?.isDelegated ?? false,
|
|
70
|
+
status: snapshot.status,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function activeAccountsEqual(a: ActiveAccount, b: ActiveAccount): boolean {
|
|
75
|
+
return (
|
|
76
|
+
a.principal === b.principal &&
|
|
77
|
+
a.account === b.account &&
|
|
78
|
+
a.context === b.context &&
|
|
79
|
+
a.isDelegated === b.isDelegated &&
|
|
80
|
+
a.status === b.status
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Who this app is running as, with the actor and the subject kept apart.
|
|
86
|
+
*
|
|
87
|
+
* Unlike `useOxy()`, this re-renders only when one of those five facts moves —
|
|
88
|
+
* a locale change, a dialog opening or a session-list refresh does not reach it.
|
|
89
|
+
*/
|
|
90
|
+
export function useActiveAccount(): ActiveAccount {
|
|
91
|
+
return useRuntimeSelector(useOxyRuntime(), selectActiveAccount, activeAccountsEqual);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const selectDirectory = (snapshot: { device: DeviceDirectory | null }): DeviceDirectory | null =>
|
|
95
|
+
snapshot.device;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The device directory — every principal on this device and the contexts each
|
|
99
|
+
* may act as (ADR 0002).
|
|
100
|
+
*
|
|
101
|
+
* `null` until something calls `runtime.refreshDirectory()`. A client that
|
|
102
|
+
* never asks for one never pays for the round trip.
|
|
103
|
+
*/
|
|
104
|
+
export function useDeviceDirectory(): DeviceDirectory | null {
|
|
105
|
+
return useRuntimeSelector(useOxyRuntime(), selectDirectory);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The whole runtime snapshot. Prefer a narrower hook where one exists. */
|
|
109
|
+
export function useOxySnapshot(): ReturnType<OxyRuntime['getSnapshot']> {
|
|
110
|
+
return useRuntimeSnapshot(useOxyRuntime());
|
|
111
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { DeviceDirectory, DeviceSessionState } from '@oxyhq/contracts';
|
|
2
|
+
import type { ClientSession, DeviceContext, User } from '@oxyhq/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The runtime's coarse verdict about this device's session.
|
|
6
|
+
*
|
|
7
|
+
* `'booting'` is NOT "signed out but we are still looking": it is the honest
|
|
8
|
+
* "undetermined" that every readiness gate in the SDK exists to express, and it
|
|
9
|
+
* ends exactly once at the first cold-boot resolution. `'error'` is reserved for
|
|
10
|
+
* a resolved boot that produced no account AND recorded a failure — a failed
|
|
11
|
+
* sign-in against a signed-out device is `'signed_out'` with an `error`, because
|
|
12
|
+
* the device really is signed out.
|
|
13
|
+
*/
|
|
14
|
+
export type OxyRuntimeStatus = 'booting' | 'authenticated' | 'signed_out' | 'error';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether a bearer usable for private calls is planted right now.
|
|
18
|
+
*
|
|
19
|
+
* `'refreshing'` covers both halves of the gap the SDK already tracked with two
|
|
20
|
+
* separate booleans: a bearer that is present but whose readiness gate is shut
|
|
21
|
+
* (a commit in flight), and an authenticated account whose bearer has been
|
|
22
|
+
* invalidated and is being re-minted. Both mean the same thing to a caller —
|
|
23
|
+
* wait, do not treat this as signed out.
|
|
24
|
+
*/
|
|
25
|
+
export type OxyTokenStatus = 'missing' | 'refreshing' | 'ready' | 'error';
|
|
26
|
+
|
|
27
|
+
export interface OxyRuntimeError {
|
|
28
|
+
message: string;
|
|
29
|
+
code: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The ONE observable state of an `OxyRuntime` (ADR 0004).
|
|
34
|
+
*
|
|
35
|
+
* Frozen and referentially stable: a new object is published only when
|
|
36
|
+
* something actually changed, so `useSyncExternalStore` can compare by identity
|
|
37
|
+
* and a selector over an unchanged field never re-renders.
|
|
38
|
+
*
|
|
39
|
+
* Every field is either a fact the runtime owns or a pure derivation of one.
|
|
40
|
+
* There is no second writable store: `sessions`, `activeSessionId`, `status`
|
|
41
|
+
* and `tokenStatus` are computed by the one snapshot builder, so two of them can
|
|
42
|
+
* never disagree about the same device.
|
|
43
|
+
*/
|
|
44
|
+
export interface OxyRuntimeSnapshot {
|
|
45
|
+
status: OxyRuntimeStatus;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The device DIRECTORY (ADR 0002) — principals and the contexts each may act
|
|
49
|
+
* as. `null` until something asks for one; the flat lane never pays for it.
|
|
50
|
+
*/
|
|
51
|
+
device: DeviceDirectory | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The flat, server-authoritative device state. Kept beside {@link device}
|
|
55
|
+
* because it is what the compatibility projections below are derived from,
|
|
56
|
+
* and because the two describe the same device at the same `revision`.
|
|
57
|
+
*/
|
|
58
|
+
deviceState: DeviceSessionState | null;
|
|
59
|
+
|
|
60
|
+
/** The active `principal acting as account` pair, when a directory is held. */
|
|
61
|
+
activeContext: DeviceContext | null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The human whose authentication backs the session — the audit actor.
|
|
65
|
+
*
|
|
66
|
+
* Equal to {@link account} on a device with no directory and on any
|
|
67
|
+
* non-delegated context: the flat lane cannot tell the two apart, and saying
|
|
68
|
+
* so by making them the same object is more honest than leaving this `null`.
|
|
69
|
+
*/
|
|
70
|
+
principal: User | null;
|
|
71
|
+
|
|
72
|
+
/** The account the session acts AS — what a profile header renders. */
|
|
73
|
+
account: User | null;
|
|
74
|
+
|
|
75
|
+
tokenStatus: OxyTokenStatus;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the first cold boot has concluded. Monotonic: once `true` it never
|
|
79
|
+
* reverts, so a later transient signed-out state is never mistaken for
|
|
80
|
+
* "still booting".
|
|
81
|
+
*/
|
|
82
|
+
authResolved: boolean;
|
|
83
|
+
|
|
84
|
+
/** A context activation / account switch is in flight. */
|
|
85
|
+
switching: boolean;
|
|
86
|
+
|
|
87
|
+
error: OxyRuntimeError | null;
|
|
88
|
+
|
|
89
|
+
// ── Compatibility projections ────────────────────────────────────────────
|
|
90
|
+
// Derived, never separately written. They exist because every app still
|
|
91
|
+
// renders the flat session list; Phase 7 moves them onto the directory.
|
|
92
|
+
|
|
93
|
+
sessions: ClientSession[];
|
|
94
|
+
activeSessionId: string | null;
|
|
95
|
+
|
|
96
|
+
/** A sign-in / sign-out operation is in flight. */
|
|
97
|
+
isLoading: boolean;
|
|
98
|
+
|
|
99
|
+
/** The local display/bookkeeping storage has resolved. */
|
|
100
|
+
storageReady: boolean;
|
|
101
|
+
|
|
102
|
+
/** A bearer is planted on the client instance right now. */
|
|
103
|
+
hasAccessToken: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The commit gate. Shut while a session commit is mid-flight so a consumer
|
|
107
|
+
* cannot fire a private call between the token landing and the projection.
|
|
108
|
+
*/
|
|
109
|
+
tokenReady: boolean;
|
|
110
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
import type { OxyRuntime } from './createOxyRuntime';
|
|
3
|
+
import type { OxyRuntimeSnapshot } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to the whole runtime snapshot.
|
|
7
|
+
*
|
|
8
|
+
* The runtime publishes a frozen object only when a fact actually changed, so
|
|
9
|
+
* identity comparison is exact: a locale change, a dialog opening or an
|
|
10
|
+
* unrelated query settling does not wake anything subscribed here.
|
|
11
|
+
*/
|
|
12
|
+
export function useRuntimeSnapshot(runtime: OxyRuntime): OxyRuntimeSnapshot {
|
|
13
|
+
return useSyncExternalStore(runtime.subscribe, runtime.getSnapshot, runtime.getSnapshot);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Subscribe to one derived value of the runtime snapshot.
|
|
18
|
+
*
|
|
19
|
+
* The selection is cached against the snapshot it was computed from, which is
|
|
20
|
+
* what makes a selector returning a fresh object (`{user, isAuthenticated}`)
|
|
21
|
+
* safe here: `useSyncExternalStore` calls `getSnapshot` several times per
|
|
22
|
+
* commit and would otherwise see a new object every call and loop.
|
|
23
|
+
*
|
|
24
|
+
* `selector` must be stable — hoist it to module scope or wrap it in
|
|
25
|
+
* `useCallback`. An inline arrow re-subscribes on every render.
|
|
26
|
+
*/
|
|
27
|
+
export function useRuntimeSelector<T>(
|
|
28
|
+
runtime: OxyRuntime,
|
|
29
|
+
selector: (snapshot: OxyRuntimeSnapshot) => T,
|
|
30
|
+
isEqual?: (a: T, b: T) => boolean,
|
|
31
|
+
): T {
|
|
32
|
+
const cache = useRef<{ snapshot: OxyRuntimeSnapshot; value: T } | null>(null);
|
|
33
|
+
|
|
34
|
+
const getSelection = useCallback((): T => {
|
|
35
|
+
const snapshot = runtime.getSnapshot();
|
|
36
|
+
const cached = cache.current;
|
|
37
|
+
if (cached !== null && cached.snapshot === snapshot) {
|
|
38
|
+
return cached.value;
|
|
39
|
+
}
|
|
40
|
+
const value = selector(snapshot);
|
|
41
|
+
if (cached !== null && isEqual?.(cached.value, value) === true) {
|
|
42
|
+
// Same value under the caller's equality: keep the PREVIOUS reference so a
|
|
43
|
+
// structurally-equal recomputation does not read as a change.
|
|
44
|
+
cache.current = { snapshot, value: cached.value };
|
|
45
|
+
return cached.value;
|
|
46
|
+
}
|
|
47
|
+
cache.current = { snapshot, value };
|
|
48
|
+
return value;
|
|
49
|
+
}, [runtime, selector, isEqual]);
|
|
50
|
+
|
|
51
|
+
return useSyncExternalStore(runtime.subscribe, getSelection, getSelection);
|
|
52
|
+
}
|
package/src/ui/server.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which apps mirror their device credential into the device-wide shared slot.
|
|
3
|
+
*
|
|
4
|
+
* The mirror is what makes a later-installed official app able to JOIN this
|
|
5
|
+
* device's session instead of asking for the Commons identity key. It runs on
|
|
6
|
+
* native, in `'account'` mode only — the identity vault is excluded so that a
|
|
7
|
+
* background persist inside the vault can never change which session five other
|
|
8
|
+
* apps boot into.
|
|
9
|
+
*/
|
|
10
|
+
import type { AuthStateStore } from '@oxyhq/core';
|
|
11
|
+
|
|
12
|
+
const CRED_STATE = { sessionId: 'sess-1', userId: 'user-1', deviceId: 'dev-1', deviceSecret: 'ds-1' };
|
|
13
|
+
|
|
14
|
+
/** Track every credential handed to the shared slot by the store under test. */
|
|
15
|
+
function makeSharedSpy() {
|
|
16
|
+
const published: { deviceId: string; deviceSecret: string }[] = [];
|
|
17
|
+
return {
|
|
18
|
+
published,
|
|
19
|
+
store: {
|
|
20
|
+
read: async () => ({ state: 'absent' as const }),
|
|
21
|
+
publish: async (credential: { deviceId: string; deviceSecret: string }) => {
|
|
22
|
+
published.push(credential);
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
clear: async () => undefined,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the platform store with the native seams stubbed. `jest.resetModules()`
|
|
32
|
+
* + `require` rather than `jest.isolateModules` for the same reason as
|
|
33
|
+
* `sharedDeviceCredentialStore.test.ts`: a factory registered inside an isolate
|
|
34
|
+
* leaked into the next one.
|
|
35
|
+
*/
|
|
36
|
+
function loadAuthStore(
|
|
37
|
+
os: 'ios' | 'android' | 'web',
|
|
38
|
+
sessionMode: 'account' | 'identity',
|
|
39
|
+
shared: ReturnType<typeof makeSharedSpy>,
|
|
40
|
+
): AuthStateStore {
|
|
41
|
+
jest.resetModules();
|
|
42
|
+
jest.doMock('../sharedDeviceCredentialStore', () => ({
|
|
43
|
+
createPlatformSharedDeviceCredentialStore: () => (os === 'web' ? null : shared.store),
|
|
44
|
+
}));
|
|
45
|
+
const rn = require('react-native') as { Platform: { OS: string } };
|
|
46
|
+
rn.Platform.OS = os;
|
|
47
|
+
// `authStore` selects web-vs-native with `isReactNative()`, which reads
|
|
48
|
+
// `navigator.product` — NOT `Platform.OS`. Setting only the latter leaves it on
|
|
49
|
+
// the web branch, where nothing mirrors and every case passes for the wrong
|
|
50
|
+
// reason.
|
|
51
|
+
Object.defineProperty(globalThis.navigator, 'product', {
|
|
52
|
+
value: os === 'web' ? 'Gecko' : 'ReactNative',
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
const mod = require('../authStore') as typeof import('../authStore');
|
|
56
|
+
return mod.createPlatformAuthStateStore({ sessionMode });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
jest.dontMock('../sharedDeviceCredentialStore');
|
|
61
|
+
Object.defineProperty(globalThis.navigator, 'product', { value: 'Gecko', configurable: true });
|
|
62
|
+
jest.resetModules();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('createPlatformAuthStateStore', () => {
|
|
66
|
+
test('an ordinary native app publishes its credential to the shared slot', async () => {
|
|
67
|
+
const shared = makeSharedSpy();
|
|
68
|
+
const store = loadAuthStore('android', 'account', shared);
|
|
69
|
+
await store.save(CRED_STATE);
|
|
70
|
+
expect(shared.published).toEqual([{ deviceId: 'dev-1', deviceSecret: 'ds-1' }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('the identity vault does NOT publish', async () => {
|
|
74
|
+
// The vault's credential is a perfectly valid device credential, so this is a
|
|
75
|
+
// deliberate abstention, not an impossibility: publishing it would let every
|
|
76
|
+
// ordinary app on the device join the vault's session as a side effect of the
|
|
77
|
+
// vault persisting a token.
|
|
78
|
+
const shared = makeSharedSpy();
|
|
79
|
+
const store = loadAuthStore('android', 'identity', shared);
|
|
80
|
+
await store.save(CRED_STATE);
|
|
81
|
+
expect(shared.published).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('web has no shared slot to publish into', async () => {
|
|
85
|
+
const shared = makeSharedSpy();
|
|
86
|
+
const store = loadAuthStore('web', 'account', shared);
|
|
87
|
+
await store.save(CRED_STATE);
|
|
88
|
+
expect(shared.published).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform half of the shared DeviceSession credential.
|
|
3
|
+
*
|
|
4
|
+
* `@oxyhq/core` owns and tests the RULES; what is pinned here is the mapping from
|
|
5
|
+
* each platform's failure shapes onto them — and that mapping is where the
|
|
6
|
+
* dangerous mistake lives. `expo-secure-store` returns `null` for a missing item
|
|
7
|
+
* and THROWS when the keychain refuses; the Android broker answers with an
|
|
8
|
+
* explicit status. Collapsing either failure into "absent" is what would let a
|
|
9
|
+
* locked device look like one that never had a session.
|
|
10
|
+
*/
|
|
11
|
+
import type { SharedDeviceCredentialStore } from '@oxyhq/core';
|
|
12
|
+
|
|
13
|
+
const CRED = { deviceId: 'dev-shared', deviceSecret: 'ds-shared' };
|
|
14
|
+
const STORAGE_KEY = 'oxy_shared_device_session_v1';
|
|
15
|
+
|
|
16
|
+
/** A minimal in-memory `expo-secure-store` that records the options it was given. */
|
|
17
|
+
function makeSecureStoreMock() {
|
|
18
|
+
const items = new Map<string, string>();
|
|
19
|
+
const calls: { key: string; options?: Record<string, unknown> }[] = [];
|
|
20
|
+
let readError: Error | null = null;
|
|
21
|
+
let writeError: Error | null = null;
|
|
22
|
+
/** When set, `setItemAsync` resolves but nothing lands — the phantom-write case. */
|
|
23
|
+
let swallowWrites = false;
|
|
24
|
+
return {
|
|
25
|
+
items,
|
|
26
|
+
calls,
|
|
27
|
+
setReadError: (error: Error | null) => {
|
|
28
|
+
readError = error;
|
|
29
|
+
},
|
|
30
|
+
setWriteError: (error: Error | null) => {
|
|
31
|
+
writeError = error;
|
|
32
|
+
},
|
|
33
|
+
setSwallowWrites: (value: boolean) => {
|
|
34
|
+
swallowWrites = value;
|
|
35
|
+
},
|
|
36
|
+
module: {
|
|
37
|
+
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 3,
|
|
38
|
+
getItemAsync: async (key: string, options?: Record<string, unknown>) => {
|
|
39
|
+
calls.push({ key, options });
|
|
40
|
+
if (readError) {
|
|
41
|
+
throw readError;
|
|
42
|
+
}
|
|
43
|
+
return items.get(key) ?? null;
|
|
44
|
+
},
|
|
45
|
+
setItemAsync: async (key: string, value: string, options?: Record<string, unknown>) => {
|
|
46
|
+
calls.push({ key, options });
|
|
47
|
+
if (writeError) {
|
|
48
|
+
throw writeError;
|
|
49
|
+
}
|
|
50
|
+
if (!swallowWrites) {
|
|
51
|
+
items.set(key, value);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
deleteItemAsync: async (key: string, options?: Record<string, unknown>) => {
|
|
55
|
+
calls.push({ key, options });
|
|
56
|
+
items.delete(key);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type BrokerModule = {
|
|
63
|
+
read(): Promise<unknown>;
|
|
64
|
+
write(deviceId: string, deviceSecret: string): Promise<boolean>;
|
|
65
|
+
clear(): Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load the module fresh, with `Platform.OS` set and both native seams mocked.
|
|
70
|
+
*
|
|
71
|
+
* Two things here are load-bearing and were each got wrong first:
|
|
72
|
+
*
|
|
73
|
+
* - `jest.resetModules()` + `require`, NOT `jest.isolateModules`. The module
|
|
74
|
+
* memoises its keychain and broker handles at module scope, so it must be
|
|
75
|
+
* re-required per case — and with `isolateModules` a mock factory registered
|
|
76
|
+
* in one block leaked into the next block's module, so a test's own mock was
|
|
77
|
+
* silently ignored and its assertions read the PREVIOUS test's store.
|
|
78
|
+
* - `Platform.OS` is set on the instance in the CURRENT registry. A reset hands
|
|
79
|
+
* out a fresh `react-native` mock whose `OS` is back to its `'web'` default,
|
|
80
|
+
* so mutating an instance captured earlier leaves the code under test on the
|
|
81
|
+
* web branch and every platform assertion passes vacuously.
|
|
82
|
+
*/
|
|
83
|
+
function loadStore(
|
|
84
|
+
os: 'ios' | 'android' | 'web',
|
|
85
|
+
seams: { secureStore?: ReturnType<typeof makeSecureStoreMock>; broker?: BrokerModule | null } = {},
|
|
86
|
+
): SharedDeviceCredentialStore | null {
|
|
87
|
+
jest.resetModules();
|
|
88
|
+
jest.doMock('expo-modules-core', () => ({
|
|
89
|
+
requireOptionalNativeModule: () => seams.broker ?? null,
|
|
90
|
+
requireNativeModule: () => {
|
|
91
|
+
throw new Error('not available');
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
// NOT `{ virtual: true }`: a virtual mock is keyed to the directory of the
|
|
95
|
+
// file that declared it, so one declared here would be invisible to the module
|
|
96
|
+
// under test two directories up — it would silently keep resolving
|
|
97
|
+
// `unsupported`. `expo-secure-store` is installed in the workspace, so the
|
|
98
|
+
// ordinary mock registers against its real resolved path. With no mock given,
|
|
99
|
+
// model the module being ABSENT.
|
|
100
|
+
const secureStoreModule = seams.secureStore?.module;
|
|
101
|
+
jest.doMock('expo-secure-store', () => {
|
|
102
|
+
if (!secureStoreModule) {
|
|
103
|
+
throw new Error("Cannot find module 'expo-secure-store'");
|
|
104
|
+
}
|
|
105
|
+
return secureStoreModule;
|
|
106
|
+
});
|
|
107
|
+
const rn = require('react-native') as { Platform: { OS: string } };
|
|
108
|
+
rn.Platform.OS = os;
|
|
109
|
+
const mod = require('../sharedDeviceCredentialStore') as typeof import('../sharedDeviceCredentialStore');
|
|
110
|
+
return mod.createPlatformSharedDeviceCredentialStore();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
jest.dontMock('expo-modules-core');
|
|
115
|
+
jest.dontMock('expo-secure-store');
|
|
116
|
+
jest.resetModules();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('createPlatformSharedDeviceCredentialStore', () => {
|
|
120
|
+
test('web has no shared slot at all', () => {
|
|
121
|
+
// Each web origin is its own device by design; there is nothing to join.
|
|
122
|
+
expect(loadStore('web')).toBeNull();
|
|
123
|
+
// Positive control: `null` must mean "web", not "the harness never reached
|
|
124
|
+
// the platform branch" — the failure mode this whole harness had once.
|
|
125
|
+
expect(loadStore('android', { broker: null })).not.toBeNull();
|
|
126
|
+
expect(loadStore('ios')).not.toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('iOS uses the keychain, not the Android broker', async () => {
|
|
130
|
+
// The branch is on the platform because `expo-secure-store` LOADS on Android
|
|
131
|
+
// too — it would just ignore the access group and read a package-private
|
|
132
|
+
// item, which is indistinguishable from an empty shared slot.
|
|
133
|
+
const secureStore = makeSecureStoreMock();
|
|
134
|
+
const broker: BrokerModule = {
|
|
135
|
+
read: jest.fn(async () => ({ status: 'present', ...CRED })),
|
|
136
|
+
write: jest.fn(async () => true),
|
|
137
|
+
clear: jest.fn(async () => undefined),
|
|
138
|
+
};
|
|
139
|
+
const store = loadStore('ios', { secureStore, broker });
|
|
140
|
+
await store?.read();
|
|
141
|
+
expect(broker.read).not.toHaveBeenCalled();
|
|
142
|
+
expect(secureStore.calls.length).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('Android uses the broker, not the keychain', async () => {
|
|
146
|
+
const secureStore = makeSecureStoreMock();
|
|
147
|
+
const broker: BrokerModule = {
|
|
148
|
+
read: jest.fn(async () => ({ status: 'absent' })),
|
|
149
|
+
write: jest.fn(async () => true),
|
|
150
|
+
clear: jest.fn(async () => undefined),
|
|
151
|
+
};
|
|
152
|
+
const store = loadStore('android', { secureStore, broker });
|
|
153
|
+
await expect(store?.read()).resolves.toEqual({ state: 'absent' });
|
|
154
|
+
expect(broker.read).toHaveBeenCalled();
|
|
155
|
+
expect(secureStore.calls).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('iOS keychain-group slot', () => {
|
|
160
|
+
test('a missing item is absent; a THROWN read is unavailable', async () => {
|
|
161
|
+
const secureStore = makeSecureStoreMock();
|
|
162
|
+
const store = loadStore('ios', { secureStore });
|
|
163
|
+
|
|
164
|
+
await expect(store?.read()).resolves.toEqual({ state: 'absent' });
|
|
165
|
+
|
|
166
|
+
secureStore.setReadError(new Error('User interaction is not allowed'));
|
|
167
|
+
const locked = await store?.read();
|
|
168
|
+
// The pair that must never be confused. `absent` authorises seeding the slot.
|
|
169
|
+
expect(locked?.state).toBe('unavailable');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('an unparseable item is unavailable, never absent', async () => {
|
|
173
|
+
const secureStore = makeSecureStoreMock();
|
|
174
|
+
secureStore.items.set(STORAGE_KEY, 'not-json-at-all');
|
|
175
|
+
const store = loadStore('ios', { secureStore });
|
|
176
|
+
// Something IS stored. Reporting absent would authorise overwriting whatever
|
|
177
|
+
// a newer build put there.
|
|
178
|
+
expect((await store?.read())?.state).toBe('unavailable');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('reads and writes carry the dedicated service AND the access group', async () => {
|
|
182
|
+
const secureStore = makeSecureStoreMock();
|
|
183
|
+
const store = loadStore('ios', { secureStore });
|
|
184
|
+
await store?.publish(CRED);
|
|
185
|
+
|
|
186
|
+
// Without `keychainAccessGroup` the item is package-private and no sibling
|
|
187
|
+
// app can ever join. Without the dedicated `keychainService` it shares the
|
|
188
|
+
// legacy shared-IDENTITY namespace inside the group — the exact conflation
|
|
189
|
+
// this phase exists to end.
|
|
190
|
+
for (const call of secureStore.calls) {
|
|
191
|
+
expect(call.options).toMatchObject({
|
|
192
|
+
keychainService: 'oxy_device_session',
|
|
193
|
+
keychainAccessGroup: 'group.so.oxy.shared',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
expect(secureStore.calls.length).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('the item is written device-only so it never rides an OS backup to a new handset', async () => {
|
|
200
|
+
const secureStore = makeSecureStoreMock();
|
|
201
|
+
const store = loadStore('ios', { secureStore });
|
|
202
|
+
await store?.publish(CRED);
|
|
203
|
+
const write = secureStore.calls.find((c) => c.options?.keychainAccessible !== undefined);
|
|
204
|
+
expect(write?.options?.keychainAccessible).toBe(secureStore.module.WHEN_UNLOCKED_THIS_DEVICE_ONLY);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('a publish round-trips and reads back as present', async () => {
|
|
208
|
+
const secureStore = makeSecureStoreMock();
|
|
209
|
+
const store = loadStore('ios', { secureStore });
|
|
210
|
+
await expect(store?.publish(CRED)).resolves.toBe(true);
|
|
211
|
+
await expect(store?.read()).resolves.toEqual({ state: 'present', credential: CRED });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('a write that silently does not land reports false', async () => {
|
|
215
|
+
// A keychain write can resolve without the item landing under the access
|
|
216
|
+
// group (a missing entitlement is the usual cause). A phantom credential
|
|
217
|
+
// would send a fresh install into a mint that can never succeed.
|
|
218
|
+
const secureStore = makeSecureStoreMock();
|
|
219
|
+
secureStore.setSwallowWrites(true);
|
|
220
|
+
const store = loadStore('ios', { secureStore });
|
|
221
|
+
await expect(store?.publish(CRED)).resolves.toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('a throwing write reports false rather than propagating', async () => {
|
|
225
|
+
const secureStore = makeSecureStoreMock();
|
|
226
|
+
secureStore.setWriteError(new Error('keychain error -34018'));
|
|
227
|
+
const store = loadStore('ios', { secureStore });
|
|
228
|
+
await expect(store?.publish(CRED)).resolves.toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('the slot is unsupported when expo-secure-store is not installed', async () => {
|
|
232
|
+
const store = loadStore('ios');
|
|
233
|
+
// No `expo-secure-store` mock registered → the dynamic import rejects.
|
|
234
|
+
expect((await store?.read())?.state).toBe('unsupported');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Android broker slot', () => {
|
|
239
|
+
test('narrows a present payload', async () => {
|
|
240
|
+
const store = loadStore('android', {
|
|
241
|
+
broker: { read: async () => ({ status: 'present', ...CRED }), write: async () => true, clear: async () => undefined },
|
|
242
|
+
});
|
|
243
|
+
await expect(store?.read()).resolves.toEqual({ state: 'present', credential: CRED });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('an explicit unavailable status stays unavailable', async () => {
|
|
247
|
+
const store = loadStore('android', {
|
|
248
|
+
broker: {
|
|
249
|
+
read: async () => ({ status: 'unavailable', reason: 'GeneralSecurityException' }),
|
|
250
|
+
write: async () => true,
|
|
251
|
+
clear: async () => undefined,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
expect((await store?.read())?.state).toBe('unavailable');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('a THROWN broker call is unavailable, never absent', async () => {
|
|
258
|
+
const store = loadStore('android', {
|
|
259
|
+
broker: {
|
|
260
|
+
read: async () => {
|
|
261
|
+
throw new Error('binder transaction failed');
|
|
262
|
+
},
|
|
263
|
+
write: async () => true,
|
|
264
|
+
clear: async () => undefined,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
expect((await store?.read())?.state).toBe('unavailable');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('an unrecognised payload from an older sibling is unavailable, never absent', async () => {
|
|
271
|
+
const store = loadStore('android', {
|
|
272
|
+
broker: { read: async () => ({ ok: true }), write: async () => true, clear: async () => undefined },
|
|
273
|
+
});
|
|
274
|
+
expect((await store?.read())?.state).toBe('unavailable');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('the slot is unsupported when the native module is not linked', async () => {
|
|
278
|
+
const store = loadStore('android', { broker: null });
|
|
279
|
+
expect((await store?.read())?.state).toBe('unsupported');
|
|
280
|
+
await expect(store?.publish(CRED)).resolves.toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('publish forwards the broker’s own read-back verdict', async () => {
|
|
284
|
+
const unverified = loadStore('android', {
|
|
285
|
+
broker: { read: async () => ({ status: 'absent' }), write: async () => false, clear: async () => undefined },
|
|
286
|
+
});
|
|
287
|
+
await expect(unverified?.publish(CRED)).resolves.toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|