aboutsummaryrefslogtreecommitdiff
path: root/front-end/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-03-11 21:21:18 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-03-11 21:21:18 -0400
commit748cdbf4880d830fd794e92856e8c35a46e4f884 (patch)
tree27d539f1f8d65d07f25c2e63947c5358ab48d9c3 /front-end/src
parent3883de080e263d2f076f65b4600a5021d3d64a21 (diff)
feat(app): #1 update libs & add curl support
Diffstat (limited to 'front-end/src')
-rw-r--r--front-end/src/buttons.scss10
-rw-r--r--front-end/src/components/Bookmarks.vue1
-rw-r--r--front-end/src/components/Boomarks/AddOrUpdateForm.vue109
-rw-r--r--front-end/src/components/Settings.vue8
-rw-r--r--front-end/src/components/Settings/Bookmarks.vue84
-rw-r--r--front-end/src/components/Settings/Registation.vue2
-rw-r--r--front-end/src/main.ts4
-rw-r--r--front-end/src/store/socialMfaPlugin.ts74
-rw-r--r--front-end/src/store/websiteLookup.ts74
9 files changed, 257 insertions, 109 deletions
diff --git a/front-end/src/buttons.scss b/front-end/src/buttons.scss
index 7088deb..44df2c2 100644
--- a/front-end/src/buttons.scss
+++ b/front-end/src/buttons.scss
@@ -1,5 +1,13 @@
.btn{
- @apply focus:ring-2 focus:outline-none font-medium rounded text-sm px-4 py-2 text-center text-white;
+ @apply focus:ring-2 focus:outline-none font-medium rounded text-sm px-2.5 py-2 text-center text-white;
+
+ &.sm{
+ @apply text-xs px-2 py-1;
+ }
+
+ &.lg{
+ @apply text-lg px-4 py-3;
+ }
&.round{
@apply rounded-full;
diff --git a/front-end/src/components/Bookmarks.vue b/front-end/src/components/Bookmarks.vue
index cc3cd6a..274b0b4 100644
--- a/front-end/src/components/Bookmarks.vue
+++ b/front-end/src/components/Bookmarks.vue
@@ -387,7 +387,6 @@ const upload = (() => {
<span class="sr-only">Search</span>
</button>
</form>
-
</div>
<div class="relative ml-3 md:ml-10">
diff --git a/front-end/src/components/Boomarks/AddOrUpdateForm.vue b/front-end/src/components/Boomarks/AddOrUpdateForm.vue
index a4a3f1d..59af737 100644
--- a/front-end/src/components/Boomarks/AddOrUpdateForm.vue
+++ b/front-end/src/components/Boomarks/AddOrUpdateForm.vue
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { computed, toRefs } from 'vue';
-import { join, split } from 'lodash-es';
+import { isEmpty, join, split } from 'lodash-es';
+import { useStore } from '../../store';
+import { useWait } from '@vnuge/vnlib.browser';
const emit = defineEmits(['submit'])
const props = defineProps<{
@@ -15,40 +17,86 @@ const tags = computed({
set: (value:string) => v$.value.Tags.$model = split(value, ',')
})
+const { websiteLookup:lookup } = useStore()
+const { setWaiting, waiting } = useWait()
+
+const execLookup = async () => {
+ //url must be valid before searching
+ if(v$.value.Url.$invalid) return
+
+ setWaiting(true)
+
+ try{
+ const { title, description, keywords } = await lookup.execLookup(v$.value.Url.$model);
+
+ //Set the title and description
+ if(title){
+ v$.value.Name.$model = title;
+ v$.value.Name.$dirty = true;
+ }
+
+ if(description){
+ v$.value.Description.$model = description;
+ v$.value.Description.$dirty = true;
+ }
+
+ if(keywords){
+ v$.value.Tags.$model = keywords;
+ v$.value.Tags.$dirty = true;
+ }
+ }
+ catch(e){
+ //Mostly ignore errors
+ console.error(e)
+ }
+ finally{
+ setWaiting(false)
+ }
+}
+
+const showSearchButton = computed(() => lookup.isSupported && !isEmpty(v$.value.Url.$model))
+
</script>
<template>
- <form class="grid grid-cols-1 gap-4 p-4" @submit.prevent="emit('submit')">
+ <form id="bm-add-or-update-form" class="grid grid-cols-1 gap-4 p-4" @submit.prevent="emit('submit')">
<fieldset>
- <label for="url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">URL</label>
- <input type="text" id="url" class="input" placeholder="https://www.example.com"
- v-model="v$.Url.$model"
- :class="{'dirty': v$.Url.$dirty, 'error': v$.Url.$invalid}"
- required
- >
+ <label for="url" class="flex justify-between mb-2 text-sm font-medium text-gray-900 dark:text-white">
+ URL
+ </label>
+ <div class="flex gap-2">
+ <input type="text" id="url" class="input" placeholder="https://www.example.com" v-model="v$.Url.$model"
+ :class="{'dirty': v$.Url.$dirty, 'error': v$.Url.$invalid}" required>
+
+ <div class="">
+ <button :disabled="!showSearchButton || waiting" @click.prevent="execLookup"
+ class="btn blue search-btn">
+ <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
+ viewBox="0 0 20 20">
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
+ d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
+ </svg>
+ </button>
+ </div>
+ </div>
</fieldset>
<fieldset>
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
- <input type="text" id="Name" class="input" placeholder="Hello World"
- v-model="v$.Name.$model"
- :class="{'dirty': v$.Name.$dirty, 'error': v$.Name.$invalid}"
- required
- >
+ <input type="text" id="Name" class="input" placeholder="Hello World" v-model="v$.Name.$model"
+ :class="{'dirty': v$.Name.$dirty, 'error': v$.Name.$invalid}" required>
</fieldset>
- <fieldset>
+ <fieldset>
<label for="tags" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Tags</label>
- <input type="text" id="tags" class="input" placeholder="tag1,tag2,tag3"
- v-model="tags"
- :class="{'dirty': v$.Tags.$dirty, 'error': v$.Tags.$invalid}"
- >
+ <input type="text" id="tags" class="input" placeholder="tag1,tag2,tag3" v-model="tags"
+ :class="{'dirty': v$.Tags.$dirty, 'error': v$.Tags.$invalid}">
</fieldset>
<fieldset>
- <label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
+ <label for="description"
+ class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea type="text" id="description" rows="5" class="input" placeholder="This is a bookmark"
v-model="v$.Description.$model"
- :class="{'dirty': v$.Description.$dirty, 'error': v$.Description.$invalid}"
- />
+ :class="{'dirty': v$.Description.$dirty, 'error': v$.Description.$invalid}" />
</fieldset>
-
+
<div class="flex justify-end">
<button type="submit" class="btn blue">
Submit
@@ -57,12 +105,17 @@ const tags = computed({
</form>
</template>
-<style scoped lang="scss">input.search {
- @apply ps-10 p-2.5 border block w-full text-sm rounded;
- @apply bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500;
-}
+<style scoped lang="scss">
+
+#bm-add-or-update-form {
+ .search-btn{
-button.search {
- @apply p-2.5 ms-2 text-sm font-medium text-white bg-blue-700 rounded border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
+ @apply my-auto px-3 py-2.5;
+
+ &:disabled{
+ @apply bg-gray-600;
+ }
+ }
}
+
</style> \ No newline at end of file
diff --git a/front-end/src/components/Settings.vue b/front-end/src/components/Settings.vue
index 83d3f79..504f38a 100644
--- a/front-end/src/components/Settings.vue
+++ b/front-end/src/components/Settings.vue
@@ -19,7 +19,7 @@ const darkMode = useDark();
<h2 class="text-2xl font-bold">Settings</h2>
<div class="flex flex-col w-full max-w-3xl gap-10 mt-3">
- <div class="">
+ <div class="mb-6">
<h3 class="text-xl font-bold">
General
</h3>
@@ -41,7 +41,7 @@ const darkMode = useDark();
</div>
</div>
- <div class="">
+ <div class="mb-6">
<h3 class="text-xl font-bold">Boomarks</h3>
<div class="relative mt-4">
@@ -51,7 +51,7 @@ const darkMode = useDark();
<PasswordReset />
- <div class="">
+ <div class="mb-8">
<h3 class="text-xl font-bold">Multi Factor Auth</h3>
<div class="relative mt-4 py-2.5">
@@ -66,7 +66,7 @@ const darkMode = useDark();
<Oauth2Apps />
</div>
- <div v-if="store.registation.status?.can_invite" class="mb-10">
+ <div v-if="store.registation.status?.can_invite" class="mt-6 mb-10">
<Registation />
</div>
diff --git a/front-end/src/components/Settings/Bookmarks.vue b/front-end/src/components/Settings/Bookmarks.vue
index a4ab55a..aa4ed31 100644
--- a/front-end/src/components/Settings/Bookmarks.vue
+++ b/front-end/src/components/Settings/Bookmarks.vue
@@ -1,13 +1,14 @@
<script setup lang="ts">
import { apiCall, useWait } from '@vnuge/vnlib.browser';
import { useStore, type DownloadContentType, TabId } from '../../store';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
-const { bookmarks } = useStore();
+const { bookmarks, websiteLookup } = useStore();
const downloadAnchor = ref();
const { waiting } = useWait()
+const curlSupported = computed(() => websiteLookup.isSupported);
const downloadBookmarks = (contentType: DownloadContentType) => {
apiCall(async () => {
@@ -58,8 +59,10 @@ javascript: (function() {
</div>
<p class="p-0.5 my-auto text-sm flex flex-row">
<span class="">
- <svg class="w-6 h-5 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>
+ <svg class="w-6 h-5 text-gray-800 dark:text-white" aria-hidden="true"
+ xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
+ d="M5 12h14M5 12l4-4m-4 4 4 4" />
</svg>
</span>
<span>
@@ -72,47 +75,66 @@ javascript: (function() {
<MenuButton :disabled="waiting" class="flex items-center gap-3 btn light">
<div class="hidden lg:inline">Download</div>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none">
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4c0 .6.4 1 1 1h14c.6 0 1-.4 1-1v-4c0-.6-.4-1-1-1h-2m-1-5-4 5-4-5m9 8h0"/>
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
+ d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4c0 .6.4 1 1 1h14c.6 0 1-.4 1-1v-4c0-.6-.4-1-1-1h-2m-1-5-4 5-4-5m9 8h0" />
</svg>
</MenuButton>
- <transition
- enter-active-class="transition duration-100 ease-out"
- enter-from-class="transform scale-95 opacity-0"
- enter-to-class="transform scale-100 opacity-100"
+ <transition enter-active-class="transition duration-100 ease-out"
+ enter-from-class="transform scale-95 opacity-0" enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
- leave-from-class="transform scale-100 opacity-100"
- leave-to-class="transform scale-95 opacity-0"
- >
- <MenuItems class="absolute z-10 bg-white divide-y divide-gray-100 rounded-b shadow right-2 lg:left-0 min-w-32 lg:end-0 dark:bg-gray-700">
- <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton">
+ leave-from-class="transform scale-100 opacity-100" leave-to-class="transform scale-95 opacity-0">
+ <MenuItems
+ class="absolute z-10 bg-white divide-y divide-gray-100 rounded-b shadow right-2 lg:left-0 min-w-32 lg:end-0 dark:bg-gray-700">
+ <ul class="py-2 text-sm text-gray-700 dark:text-gray-200"
+ aria-labelledby="dropdownDefaultButton">
<!-- Use the `active` state to conditionally style the active item. -->
<MenuItem as="template" v-slot="{ }">
- <li>
- <button @click="downloadBookmarks('text/html')" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
- HTML
- </button>
- </li>
+ <li>
+ <button @click="downloadBookmarks('text/html')"
+ class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
+ HTML
+ </button>
+ </li>
</MenuItem>
<MenuItem as="template" v-slot="{ }">
- <li>
- <button @click="downloadBookmarks('text/csv')" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
- CSV
- </button>
- </li>
+ <li>
+ <button @click="downloadBookmarks('text/csv')"
+ class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
+ CSV
+ </button>
+ </li>
</MenuItem>
- <MenuItem as="template" v-slot="{ }">
- <li>
- <button @click="downloadBookmarks('application/json')" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
- JSON
- </button>
- </li>
+ <MenuItem as="template" v-slot="{ }">
+ <li>
+ <button @click="downloadBookmarks('application/json')"
+ class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
+ JSON
+ </button>
+ </li>
</MenuItem>
</ul>
</MenuItems>
</transition>
</Menu>
</div>
+ <div class="mt-3">
+ <h4 class="mb-2 font-bold">Features</h4>
+ <p class="text-sm text-gray-500 dark:text-gray-400">
+ Some features for Simple-Bookmark use tools and applications that are already installed on
+ your server such as curl.
+ </p>
+ <div class="flex flex-row gap-2 mt-4">
+ <span class="w-3 h-3 my-auto rounded-full" :class="[curlSupported ? 'bg-green-500' : 'bg-amber-500']"></span>
+ <span class="my-auto font-bold">
+ curl
+ </span>
+ <span class="my-auto text-sm text-gray-500 ms-4 dark:text-gray-400">
+ Curl is used to fetch website details like title, description and tags.
+ {{ curlSupported ? '(supported)' : '(not supported)' }}
+ </span>
+ </div>
+ </div>
</div>
-
+
<a ref="downloadAnchor" class="hidden"></a>
</template> \ No newline at end of file
diff --git a/front-end/src/components/Settings/Registation.vue b/front-end/src/components/Settings/Registation.vue
index a0f208e..d0dfaa7 100644
--- a/front-end/src/components/Settings/Registation.vue
+++ b/front-end/src/components/Settings/Registation.vue
@@ -58,7 +58,7 @@ const onCancel = () => {
<div class="">
<div class="flex flex-row justify-between w-full">
- <h3 class="text-xl font-bold">Registation</h3>
+ <h3 class="text-xl font-bold">Invite Links</h3>
<div class="flex flex-row justify-end">
<button class="btn blue" @click="toggleOpen(true)">Invite User</button>
diff --git a/front-end/src/main.ts b/front-end/src/main.ts
index c5be406..2f2ca8e 100644
--- a/front-end/src/main.ts
+++ b/front-end/src/main.ts
@@ -30,6 +30,7 @@ import { mfaSettingsPlugin } from './store/mfaSettingsPlugin'
import { socialMfaPlugin } from './store/socialMfaPlugin'
import { bookmarkPlugin } from './store/bookmarks'
import { registationPlugin } from './store/registation';
+import { siteLookupPlugin } from './store/websiteLookup';
//Setup the vnlib api
configureApi({
@@ -67,9 +68,10 @@ store.use(profilePlugin('/account/profile'))
//Enable mfa with totp settings plugin (optional pki config)
.use(mfaSettingsPlugin('/account/mfa', '/account/pki'))
//Setup social mfa plugin
- .use(socialMfaPlugin())
+ .use(socialMfaPlugin("/account/social/portals"))
//Add the oauth2 apps plugin
.use(bookmarkPlugin('/bookmarks'))
+ .use(siteLookupPlugin('/lookup', 2000))
.use(registationPlugin('/register'))
//Setup oauth apps plugin (disabled for now)
//.use(oauth2AppsPlugin('/oauth/apps', '/oauth/scopes'))
diff --git a/front-end/src/store/socialMfaPlugin.ts b/front-end/src/store/socialMfaPlugin.ts
index d8d7bb1..e0ec972 100644
--- a/front-end/src/store/socialMfaPlugin.ts
+++ b/front-end/src/store/socialMfaPlugin.ts
@@ -1,26 +1,19 @@
-// Copyright (C) 2024 Vaughn Nugent
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'pinia'
import { MaybeRef } from 'vue';
-import { useSocialOauthLogin, useUser, SocialOAuthPortal, fromPortals, useAxios } from '@vnuge/vnlib.browser'
+import {
+ useUser,
+ useOauthLogin,
+ useSocialDefaultLogout,
+ fetchSocialPortals,
+ fromSocialPortals,
+ fromSocialConnections,
+} from '@vnuge/vnlib.browser'
import { get } from '@vueuse/core';
import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia'
import { defer } from 'lodash-es';
-type SocialMfaPlugin = ReturnType<typeof useSocialOauthLogin>
+type SocialMfaPlugin = ReturnType<typeof useOauthLogin>
declare module 'pinia' {
export interface PiniaCustomProperties {
@@ -35,43 +28,40 @@ export const socialMfaPlugin = (portalEndpoint?: MaybeRef<string>): PiniaPlugin
const { } = storeToRefs(store)
const { logout } = useUser()
- /**
- * Override the logout function to default to a social logout,
- * if the social logout fails, then we will logout the user
- */
- const setLogoutMethod = (socialOauth: SocialMfaPlugin) => {
- const logoutFunc = socialOauth.logout;
+ //Create social login from available portals
+ const defaultSocial = useSocialDefaultLogout(
+ useOauthLogin([]),
+ logout //fallback to default logout
+ );
- (socialOauth as any).logout = async () => {
- if (await logoutFunc() === false) {
- await logout()
- }
- }
- }
-
- const _loadPromise = new Promise<SocialMfaPlugin>((resolve, reject) => {
+ const _loadPromise = new Promise<SocialMfaPlugin>((resolve, _) => {
- if(get(portalEndpoint) == null) {
- const socialOauth = useSocialOauthLogin([])
- setLogoutMethod(socialOauth)
- return resolve(socialOauth)
+ if (get(portalEndpoint) == null) {
+ return resolve(defaultSocial)
}
+ /*
+ Try to load social methods from server, if it fails, then we will
+ fall back to default
+ */
+
defer(async () => {
+
try {
- //Get axios instance
- const axios = useAxios(null)
- //Get all enabled portals
- const { data } = await axios.get<SocialOAuthPortal[]>(get(portalEndpoint)!);
- //Setup social providers from server portals
- const socialOauth = useSocialOauthLogin(fromPortals(data));
- setLogoutMethod(socialOauth);
+ const portals = await fetchSocialPortals(get(portalEndpoint)!);
+ const social = fromSocialPortals(portals);
+ const methods = fromSocialConnections(social);
+
+ //Create social login from available portals
+ const login = useOauthLogin(methods);
+ const socialOauth = useSocialDefaultLogout(login, logout);
resolve(socialOauth)
} catch (error) {
- reject(error)
+ //Let failure fall back to default
+ resolve(defaultSocial)
}
})
})
diff --git a/front-end/src/store/websiteLookup.ts b/front-end/src/store/websiteLookup.ts
new file mode 100644
index 0000000..7d4f3ca
--- /dev/null
+++ b/front-end/src/store/websiteLookup.ts
@@ -0,0 +1,74 @@
+
+import 'pinia'
+import { MaybeRef, Ref, shallowRef, watch } from 'vue';
+import { WebMessage, apiCall, useAxios } from '@vnuge/vnlib.browser'
+import { get, set } from '@vueuse/core';
+import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia'
+import { defer, noop } from 'lodash-es';
+
+export interface WebsiteLookupResult {
+ title: string | undefined,
+ description: string | undefined,
+ keywords: string[] | undefined,
+}
+
+export interface LookupApi{
+ isSupported: Ref<boolean>,
+ timeout: Ref<number>,
+ execLookup(url:string): Promise<WebsiteLookupResult>
+}
+
+declare module 'pinia' {
+ export interface PiniaCustomProperties {
+ websiteLookup:{
+ isSupported: boolean,
+ execLookup(url: string): Promise<WebsiteLookupResult>
+ }
+ }
+}
+
+const urlToBase64UrlEncoded = (url: string) => {
+ return btoa(url)
+ .replace(/-/g, '+')
+ .replace(/_/g, '/')
+ .replace(/\./g, '=') //Fix padding
+}
+
+export const siteLookupPlugin = (lookupEndpoint: MaybeRef<string>, to: number): PiniaPlugin => {
+
+ return ({ store }: PiniaPluginContext) => {
+
+ const { loggedIn } = storeToRefs(store)
+ const axios = useAxios(null)
+
+ const isSupported = shallowRef(false)
+ const timeout = shallowRef(to)
+
+ const checkIsSupported = () => {
+ return apiCall(async () => {
+ //Execute test with the 'support' query parameter
+ const { data } = await axios.get<WebMessage>(`${get(lookupEndpoint)}?support`)
+ set(isSupported, data.success)
+ });
+ }
+
+ const execLookup = async (url:string) => {
+ const base64Url = urlToBase64UrlEncoded(url)
+
+ //Execute test with the 'support' query parameter
+ const { data } = await axios.get<WebMessage<WebsiteLookupResult>>(`${get(lookupEndpoint)}?timeout=${get(timeout)}&url=${base64Url}`)
+ return data.getResultOrThrow();
+ }
+
+ //If login status changes, recheck support
+ watch([loggedIn], ([li]) => li ? defer(checkIsSupported) : noop(), { immediate: true })
+
+ return {
+ websiteLookup: {
+ isSupported,
+ execLookup,
+ timeout
+ } as LookupApi
+ } as any
+ }
+} \ No newline at end of file