feat: new UI

This commit is contained in:
Juan Di Toro 2023-10-04 21:36:30 +02:00
parent 4228d834cd
commit 7aaf2ba9ac
33 changed files with 2970 additions and 864 deletions

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2023 LumeWeb
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,2 +1,43 @@
# browser-webapp
## browser-app
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

4
components/fonts.ts Normal file
View File

@ -0,0 +1,4 @@
import { JetBrains_Mono, Inter } from 'next/font/google'
export const fontInter = Inter({ subsets: ['latin'] })
export const fontJetbrainsMono = JetBrains_Mono({ subsets: ['latin'] })

68
dist/index.html vendored
View File

@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
#browser-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
#address-bar-container {
padding: 10px;
background-color: #f1f1f1;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
#address-bar {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 3px;
margin-right: 10px;
}
#go-button {
padding: 10px 20px;
border: none;
background-color: #007bff;
color: white;
cursor: pointer;
border-radius: 3px;
}
#web-content {
flex-grow: 1;
}
#booting {
margin: 1em;
}
</style>
<title></title>
<script type="application/javascript" src="./index.js"></script>
</head>
<body>
<div id="browser-container">
<div id="address-bar-container">
<span id="booting">Booting</span>
<input type="text" id="address-bar" placeholder="Enter URL..." disabled>
<button id="go-button" disabled>Go</button>
</div>
<iframe id="web-content" src="about:blank" frameborder="0" style="width: 100%; height: 100%;"></iframe>
</div>
</body>
</html>

6
next.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

View File

@ -1,31 +1,28 @@
{
"name": "browser-webapp",
"version": "0.1.0",
"type": "module",
"devDependencies": {
"@lumeweb/presetter-kernel-module-preset": "^0.1.0-develop.1",
"presetter": "*"
},
"readme": "ERROR: No README data found!",
"_id": "browser-webapp@0.1.0",
"private": true,
"scripts": {
"prepare": "presetter bootstrap",
"build": "run build",
"semantic-release": "semantic-release"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@helia/unixfs": "^1.4.2",
"@lumeweb/kernel-dns-client": "^0.1.0-develop.7",
"@lumeweb/kernel-eth-client": "^0.1.0-develop.16",
"@lumeweb/kernel-handshake-client": "^0.1.0-develop.8",
"@lumeweb/kernel-ipfs-client": "^0.1.0-develop.24",
"@lumeweb/kernel-network-registry-client": "^0.1.0-develop.9",
"@lumeweb/kernel-peer-discovery-client": "^0.0.2-develop.16",
"@lumeweb/kernel-swarm-client": "^0.1.0-develop.10",
"@lumeweb/libkernel": "^0.1.0-develop.63",
"@lumeweb/tld-enum": "^0.1.0-develop.1",
"cheerio": "^1.0.0-rc.12",
"file-type": "^18.5.0",
"is-ipfs": "^8.0.1"
"next": "13.5.4",
"react": "^18",
"react-dom": "^18",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.4",
"postcss": "^8",
"tailwindcss": "^3",
"typescript": "^5"
}
}

6
pages/_app.tsx Normal file
View File

@ -0,0 +1,6 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

13
pages/_document.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

13
pages/api/hello.ts Normal file
View File

@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

108
pages/index.tsx Normal file
View File

@ -0,0 +1,108 @@
import React from "react"
export default function Home() {
return (
<main className={`flex min-h-screen flex-col items-center justify-between`}>
<header className={`relative h-14 px-2 pl-7 py-2 w-full bg-neutral-900`}>
<LumeIndicator connected={true} className="z-10"/>
<div className="relative h-full w-full rounded-full bg-neutral-800 border border-neutral-700 flex items-center">
<ProtocolIndicator />
<button className="absolute bg-neutral-700 text-neutral-400 px-4 py-2 right-0 rounded-r-full">
Navigate
<ArrowSvg className="inline-block ml-2 -mt-1 w-5 h-5" />
</button>
</div>
</header>
</main>
)
}
const ProtocolIndicator = ({className}: {className?: string}) => {
const [hover, setHover] = React.useState(false)
return (
<div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
className={`bg-white rounded-full ml-1 px-1 items-center justify-center flex overflow-hidden ${
hover ? "gap-x-1 pr-3" : "w-7"
} h-7 ${className}`}
>
<HandshakeLogo className="w-5 h-5" />
<span
className={`font-semibold text-[12px] text-neutral-950 transition-transform duration-150 transform-gpu ${
hover
? "translate-x-0 opacity-100"
: "w-0 opacity-0 -translate-x-[100px]"
}`}
>
Handshake
</span>
</div>
)
}
const LumeIndicator = ({ connected, className }: { className?: string, connected: boolean }) => {
const [hover, setHover] = React.useState(false)
return (
<div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
className={`absolute pr-3 p-2 h-10 left-0 flex items-center justify-center ${
connected ? "bg-primary" : "bg-neutral-300"
} hover:gap-x-2 rounded-r-full transition-all duration-150 ease-in-out ${
hover ? "w-[120px]" : "w-[20px]"
} ${className}`}
>
{connected ? <HandshakeLogo className={`w-5 h-5 text-black`} /> : null}
<span
className={`font-semibold text-[12px] text-neutral-950 transition-transform duration-150 transform-gpu ${
hover
? "translate-x-0 opacity-100"
: "w-0 opacity-0 -translate-x-[100px]"
}`}
>
{connected ? "On Lume" : "On HTTP "}
</span>
</div>
)
}
const HandshakeLogo = ({ className }: { className?: string }) => {
return (
<svg
className={className}
width="45"
height="47"
viewBox="0 0 45 47"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M38.848 15.3309L35.9273 10.1424L41.5857 10.1441C41.7369 10.1441 41.9131 10.2446 41.9958 10.3804C42.086 10.5287 42.5454 11.2862 43.0974 12.1962C43.7581 13.2855 44.5482 14.5877 45 15.3309H38.848ZM28.3822 46.7545C28.2453 47 28.0674 47 28.0097 47H25.6979C24.4744 46.9992 23.0629 46.9983 22.2227 46.9983L31.2135 30.8571C31.363 30.589 31.3597 30.2622 31.2043 29.9974C31.049 29.7326 30.7667 29.57 30.461 29.57L15.137 29.5901L12.0902 24.3145H34.409C34.4106 24.3145 34.4123 24.3136 34.4148 24.3136C34.4173 24.3136 34.419 24.3145 34.4207 24.3145C34.4332 24.3145 34.4449 24.3103 34.4574 24.3094C34.5075 24.3061 34.5576 24.3011 34.6052 24.2893C34.642 24.281 34.6779 24.2692 34.7138 24.2558C34.733 24.2483 34.7514 24.2407 34.7698 24.2324C34.9368 24.1578 35.0846 24.0354 35.179 23.862L38.858 17.0604H44.9315L28.3822 46.7545ZM20.7252 46.1411C20.5298 45.8193 20.2759 45.4012 19.9994 44.9462C19.0699 43.4119 17.8789 41.4478 17.7444 41.2325C17.6993 41.1596 17.6785 40.9819 17.7728 40.8127C17.9883 40.4255 22.0573 33.0961 23.0487 31.3096L28.9903 31.302L20.7252 46.1411ZM10.5927 36.0398L7.63948 30.7926L10.6161 25.2127L13.6019 30.382C12.674 32.1291 11.2258 34.8541 10.5927 36.0398ZM6.6289 36.8567C5.12556 36.8567 3.63057 36.8559 3.41091 36.8559C3.26392 36.8559 3.08519 36.7528 3.00418 36.6204L2.04287 35.0359C1.35802 33.9064 0.48441 32.4659 0 31.6691H6.152L9.07183 36.8576C8.43458 36.8576 7.53257 36.8567 6.6289 36.8567ZM16.6178 0.246354C16.7547 0 16.9318 0 16.9894 0L22.8015 0.000837939L13.7865 16.1421C13.779 16.1555 13.7748 16.1697 13.7681 16.184C13.7547 16.2108 13.743 16.2384 13.7322 16.2669C13.723 16.2946 13.7146 16.3214 13.7071 16.349C13.7004 16.375 13.6946 16.401 13.6904 16.427C13.6854 16.4596 13.6821 16.4915 13.6812 16.5233C13.6804 16.5376 13.6771 16.551 13.6771 16.5661C13.6771 16.5769 13.6796 16.587 13.6804 16.5979C13.6812 16.6297 13.6846 16.6607 13.6896 16.6917C13.6929 16.7177 13.6971 16.7437 13.7038 16.7697C13.7105 16.7973 13.7197 16.8233 13.7288 16.8501C13.738 16.8761 13.7472 16.9029 13.7589 16.928C13.7706 16.9532 13.784 16.9766 13.7982 17.0009C13.8124 17.0244 13.8257 17.0479 13.8424 17.0705C13.8583 17.0931 13.8767 17.1141 13.8959 17.1359C13.9143 17.156 13.9326 17.1769 13.9527 17.1962C13.9727 17.2146 13.9936 17.2305 14.0153 17.2473C14.0395 17.2666 14.0638 17.2842 14.0896 17.3001C14.0997 17.306 14.1072 17.3135 14.1172 17.3194C14.1272 17.3252 14.1381 17.3269 14.1489 17.3319C14.1932 17.3546 14.24 17.3738 14.2893 17.3889C14.3068 17.3939 14.3235 17.4006 14.3419 17.4048C14.4045 17.4199 14.4688 17.4291 14.5365 17.4291C14.5373 17.4291 14.5373 17.43 14.5381 17.43H14.5398L14.9215 17.4291C14.9232 17.4291 14.9248 17.43 14.9265 17.43L29.8655 17.4099C30.2606 18.097 30.9989 19.3899 31.6278 20.491C32.1105 21.3373 32.5097 22.0353 32.8238 22.5841H10.5944C10.5844 22.5841 10.5752 22.5875 10.566 22.5883C10.2636 22.5933 9.97216 22.7534 9.81849 23.0408L6.13864 29.9387H0.0693207C2.71854 25.1843 16.4407 0.563095 16.6178 0.246354ZM24.2873 0.878998L24.8268 1.76721C25.7748 3.33081 27.1136 5.53794 27.2556 5.76753C27.3007 5.84043 27.3215 6.01724 27.2263 6.18734L21.9504 15.6904L16.0114 15.6979L24.2873 0.878998ZM34.4048 10.9544L37.3597 16.2041L34.3472 21.7722C33.988 21.1454 33.542 20.3644 33.1236 19.6312C32.2968 18.1824 31.6938 17.1275 31.3538 16.5376C31.9961 15.3619 33.694 12.2515 34.4048 10.9544Z"
fill="currentColor"
/>
</svg>
)
}
const ArrowSvg = ({ className }: { className?: string }) => {
return (
<svg
className={className}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.14645 3.14645C8.34171 2.95118 8.65829 2.95118 8.85355 3.14645L12.8536 7.14645C13.0488 7.34171 13.0488 7.65829 12.8536 7.85355L8.85355 11.8536C8.65829 12.0488 8.34171 12.0488 8.14645 11.8536C7.95118 11.6583 7.95118 11.3417 8.14645 11.1464L11.2929 8H2.5C2.22386 8 2 7.77614 2 7.5C2 7.22386 2.22386 7 2.5 7H11.2929L8.14645 3.85355C7.95118 3.65829 7.95118 3.34171 8.14645 3.14645Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
)
}

2532
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -1,25 +0,0 @@
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
import { createClient as createIpfsClient } from "@lumeweb/kernel-ipfs-client";
import { createClient as createSwarmClient } from "@lumeweb/kernel-swarm-client";
import { createClient as createPeerDiscoveryClient } from "@lumeweb/kernel-peer-discovery-client";
import { createClient as createNetworkRegistryClient } from "@lumeweb/kernel-network-registry-client";
import { createClient as createHandshakeClient } from "@lumeweb/kernel-handshake-client";
import { createClient as createEthClient } from "@lumeweb/kernel-eth-client";
const dnsClient = createDnsClient();
const ipfsClient = createIpfsClient();
const swarmClient = createSwarmClient();
const peerDiscoveryClient = createPeerDiscoveryClient();
const networkRegistryClient = createNetworkRegistryClient();
const handshakeClient = createHandshakeClient();
const ethClient = createEthClient();
export {
dnsClient,
ipfsClient,
swarmClient,
peerDiscoveryClient,
networkRegistryClient,
handshakeClient,
ethClient,
};

View File

@ -1,19 +0,0 @@
import { ContentFilter } from "./types.js";
export class ContentProcessor {
private filters: ContentFilter[] = [];
registerFilter(filter: ContentFilter) {
this.filters.push(filter);
}
async process(response: Response, mimeType: string): Promise<Response> {
let processedResponse = response;
for (const filter of this.filters) {
processedResponse = await filter.process(processedResponse, mimeType);
}
return processedResponse;
}
}

View File

@ -1,48 +0,0 @@
import { ContentFilter } from "src/types.js";
import { getTld } from "@lumeweb/libresolver";
import tldEnum from "@lumeweb/tld-enum";
import * as cheerio from "cheerio";
export default class URLRewriteFilter implements ContentFilter {
async process(response: Response, mimeType: string): Promise<Response> {
if (mimeType !== "text/html") {
return response;
}
let html = await response.text();
const $ = cheerio.load(html);
["a", "link", "script", "img"].forEach((tag) => {
$.root()
.find(tag)
.each((index, element) => {
let attrName = ["a", "link"].includes(tag) ? "href" : "src";
let urlValue = $(element).attr(attrName);
if (urlValue) {
if (!isICANN(urlValue)) {
if (urlValue.startsWith("/")) {
$(element).attr(attrName, `/browse${urlValue}`);
} else if (urlValue.startsWith("http")) {
$(element).attr(attrName, `/browse/${urlValue}`);
}
}
}
});
});
return new Response($.html(), {
headers: response.headers,
});
}
}
function isICANN(url: string) {
try {
const parsedUrl = new URL(url);
const domain = parsedUrl.hostname;
return tldEnum.list.includes(getTld(domain));
} catch (e) {
return false;
}
}

View File

@ -1,104 +0,0 @@
import * as kernel from "@lumeweb/libkernel/kernel";
import {
dnsClient,
ethClient,
handshakeClient,
ipfsClient,
networkRegistryClient,
peerDiscoveryClient,
swarmClient,
} from "./clients.js";
import { ed25519 } from "@lumeweb/libkernel";
document.addEventListener("DOMContentLoaded", () =>
document.getElementById("go-button")?.addEventListener("click", () => {
let input = (
document.getElementById("address-bar") as HTMLInputElement
).value.trim();
// If the input doesn't contain a protocol, assume it's http
if (!input.match(/^https?:\/\//)) {
input = `http://${input}`;
}
try {
// Try to parse it as a URL
const url = new URL(input);
// Update the iframe's src attribute
const iframe = document.getElementById(
"web-content",
) as HTMLIFrameElement;
iframe.src = `/browse/${url.hostname}${url.pathname}${url.search}${url.hash}`;
} catch (e) {
// Handle invalid URLs here, if needed
console.error("Invalid URL:", e);
}
}),
);
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
async function boot() {
await kernel.init();
await kernel.login(ed25519.utils.randomPrivateKey());
const reg = await navigator.serviceWorker.register("/sw.js");
await reg.update();
await kernel.serviceWorkerReady();
BOOT_FUNCTIONS.push(
async () =>
await swarmClient.addRelay(
"2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d",
),
);
// IRC
BOOT_FUNCTIONS.push(
async () =>
await peerDiscoveryClient.register(
"zdiN5eJ3RfHpZHTYorGxBt1GCsrGJYV9GprwVWkj8snGsjWSrptFm8BtQX",
),
);
BOOT_FUNCTIONS.push(
async () => await networkRegistryClient.registerType("content"),
);
BOOT_FUNCTIONS.push(
async () => await networkRegistryClient.registerType("blockchain"),
);
BOOT_FUNCTIONS.push(async () => await handshakeClient.register());
BOOT_FUNCTIONS.push(async () => await ethClient.register());
BOOT_FUNCTIONS.push(async () => await ipfsClient.register());
const resolvers = [
"zdiJdDdBJWAdYFTcRa9So5TQQ9f1pYMiMy4dqYcKp9imomQtR11LJUyJyV", // CID
"zdiKvnZYNjDqXaM8uF3pGEs7Tt6jqGc7t7M4eqbvJwpkTnrZymncfUW9Cj", // ENS
"zrjEH3iojPLr7986o7iCn9THBmJmHiuDWmS1G6oT8DnfuFM", // HNS
];
for (const resolver of resolvers) {
BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver));
}
await bootup();
await Promise.all([
ethClient.ready(),
handshakeClient.ready(),
ipfsClient.ready(),
]);
document.getElementById("booting")!.style.display = "none";
(document.getElementById("address-bar") as HTMLInputElement).disabled = false;
(document.getElementById("go-button") as HTMLButtonElement).disabled = false;
}
async function bootup() {
for (const entry of Object.entries(BOOT_FUNCTIONS)) {
await entry[1]();
}
}
document.addEventListener("DOMContentLoaded", boot);

View File

@ -1,123 +0,0 @@
import exchangeCommunicationKeys from "./messages/exchangeCommunicationKeys.js";
import {
deleteQuery,
getAuthStatus,
getAuthStatusKnown,
getLoggedInDefer,
getQueries,
getQuery,
resetLoggedInDefer,
setAuthStatus,
setAuthStatusKnown,
getAuthStatusDefer,
} from "./vars.js";
const kernelMessageHandlers = {
exchangeCommunicationKeys,
};
export async function handleIncomingMessage(event: MessageEvent) {
if (event.source === null) {
return;
}
if (event.source === window) {
return;
}
const data = event.data?.data;
if (event.data.method === "log") {
if (data?.isErr === false) {
console.log(data.message);
return;
}
console.error(data.message);
}
if (event.data.method === "kernelAuthStatus") {
setAuthStatus(data);
if (!getAuthStatusKnown()) {
getAuthStatusDefer().resolve();
setAuthStatusKnown(true);
console.log("bootloader is now initialized");
if (!getAuthStatus().loginComplete) {
console.log("user is not logged in: waiting until login is confirmed");
} else {
getLoggedInDefer().resolve();
}
if (getAuthStatus().logoutComplete) {
resetLoggedInDefer();
setAuthStatusKnown(false);
}
}
return;
}
if (!("nonce" in event.data)) {
(event.source as WindowProxy).postMessage(
{
nonce: "N/A",
method: "response",
err: "message sent to kernel with no nonce",
},
event.origin,
);
return;
}
if (!("method" in event.data)) {
(event.source as WindowProxy).postMessage(
{
nonce: event.data.nonce,
method: "response",
err: "message sent to kernel with no method",
},
event.origin,
);
return;
}
if (event.data.method in kernelMessageHandlers) {
let response;
try {
response = await kernelMessageHandlers[event.data.method](
event.data.data,
);
} catch (e: any) {
response = { err: (e as Error).message };
}
(event.source as WindowProxy).postMessage(
{
nonce: event.data.nonce,
data: response,
},
event.origin,
);
return;
}
if (!(event.data.nonce in getQueries())) {
return;
}
let receiveResult = getQuery(event.data.nonce);
if (event.data.method === "response") {
deleteQuery(event.data.nonce);
}
receiveResult?.(event.data);
if (["moduleCall", "response"].includes(event.data.method)) {
return;
}
(event.source as WindowProxy).postMessage(
{
nonce: event.data.nonce,
method: "response",
err:
"unrecognized method (user may need to log in): " + event.data.method,
},
event.origin,
);
return;
}

View File

@ -1,11 +0,0 @@
import { bytesToHex, hexToBytes } from "@lumeweb/libweb";
import {
getCommunicationPubKey,
setFrontendCommunicationPubkey,
} from "../vars.js";
export default function (data: any) {
setFrontendCommunicationPubkey(hexToBytes(data));
return bytesToHex(getCommunicationPubKey());
}

View File

@ -1,10 +0,0 @@
const extToMimes = new Map(
Object.entries({
html: "text/html",
xhtml: "application/xhtml+xml",
xml: "application/xml",
})
);
Object.freeze(extToMimes);
export default extToMimes;

View File

@ -1,39 +0,0 @@
import { ContentProcessor } from "./contentProcessor.js";
import { ContentProvider } from "./types.js";
import { DNSRecord, DNSResult } from "@lumeweb/libresolver";
import { URL } from "url";
export class ProviderManager {
private providers: ContentProvider[] = [];
private _processor = new ContentProcessor();
get processor(): ContentProcessor {
return this._processor;
}
register(provider: ContentProvider) {
this.providers.push(provider);
}
async fetch(dnsResult: DNSResult, path: string): Promise<Response> {
for (const record of dnsResult.records) {
for (const provider of this.providers) {
if (provider.supports(record.value)) {
const content = await provider.fetchContent(record.value, path);
if (content.headers.get("Content-Type")) {
return this._processor.process(
content,
content.headers.get("Content-Type")!,
);
}
return content;
}
}
}
throw new Error("No suitable provider found.");
}
}

View File

@ -1,141 +0,0 @@
import { ContentProvider } from "src/types.js";
import { ipfsPath, ipnsPath, path as checkPath } from "is-ipfs";
import { createClient } from "@lumeweb/kernel-ipfs-client";
import { CID } from "multiformats/cid";
import type { UnixFSStats } from "@helia/unixfs";
import * as nodePath from "path";
import { fileTypeFromBuffer } from "file-type";
import extToMimes from "../mimes.js";
import { URL } from "url";
export default class IPFSProvider implements ContentProvider {
private _client = createClient();
async fetchContent(
uri: string,
path: string,
query?: string,
): Promise<Response> {
let cid = translatePath(uri);
let stat: UnixFSStats | null = null;
let urlPath = path;
const parsedPath = nodePath.parse(urlPath);
let err;
try {
if (ipnsPath(cid)) {
const cidHash = cid.replace("/ipns/", "");
cid = await this._client.ipns(cidHash);
cid = `/ipfs/${cid}`;
}
if (ipfsPath(cid)) {
cid = CID.parse(cid.replace("/ipfs/", "")).toV1().toString();
stat = await this._client.stat(cid);
}
} catch (e) {
err = (e as Error).message;
}
if (!err && stat?.type === "directory") {
if (!parsedPath.base.length || !parsedPath.ext.length) {
let found = false;
for (const indexFile of ["index.html", "index.htm"]) {
try {
const subPath = nodePath.join(urlPath, indexFile);
await this._client.stat(cid, {
path: subPath,
});
urlPath = subPath;
found = true;
break;
} catch {}
}
if (!found) {
err = "404";
}
} else {
try {
await this._client.stat(cid, {
path: urlPath,
});
} catch {
err = "404";
}
}
if (err) {
throw new Error(err);
}
}
let bufferRead = 0;
const fileTypeBufferLength = 4100;
const mimeBuffer: Uint8Array[] = [];
let reader = await this._client.cat(cid, { path: urlPath });
for await (const chunk of reader.iterable()) {
if (bufferRead < fileTypeBufferLength) {
if (chunk.length >= fileTypeBufferLength) {
mimeBuffer.push(chunk.slice(0, fileTypeBufferLength));
bufferRead += fileTypeBufferLength;
} else {
mimeBuffer.push(chunk);
bufferRead += chunk.length;
}
if (bufferRead >= fileTypeBufferLength) {
reader.abort();
break;
}
} else {
reader.abort();
break;
}
}
let mime;
if (bufferRead >= fileTypeBufferLength) {
const totalLength = mimeBuffer.reduce((acc, val) => acc + val.length, 0);
const concatenated = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of mimeBuffer) {
concatenated.set(chunk, offset);
offset += chunk.length;
}
mime = await fileTypeFromBuffer(concatenated);
if (!mime) {
const ext = nodePath.parse(urlPath).ext.replace(".", "");
if (extToMimes.has(ext)) {
mime = extToMimes.get(ext);
}
}
}
reader = await this._client.cat(cid, { path: urlPath });
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of reader.iterable()) {
controller.enqueue(chunk);
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": mime ?? undefined,
},
});
}
supports(uri: string): boolean {
return checkPath(translatePath(uri));
}
}
function translatePath(uri: string) {
return uri.replace(/:\/\//, "/").replace(/^/, "/");
}

View File

@ -1,69 +0,0 @@
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
import { ProviderManager } from "./providerManager.js";
import IPFSProvider from "./providers/ipfs.js";
import URLRewriteFilter from "./filters/urlRewrite.js";
const dnsClient = createDnsClient();
const providerManager = new ProviderManager();
providerManager.register(new IPFSProvider());
providerManager.processor.registerFilter(new URLRewriteFilter());
globalThis.postMessage = async (...args) => {
// @ts-ignore
let ret = await clients.matchAll({ includeUncontrolled: true });
ret.forEach((item) => item.postMessage(...args));
if (!ret.length) {
const cb = (event) => {
// @ts-ignore
postMessage(...args);
self.removeEventListener("activate", cb);
};
self.addEventListener("activate", cb);
}
};
self.addEventListener("activate", (event) => {
// @ts-ignore
event.waitUntil(
(async () => {
// @ts-ignore
await clients.claim();
// @ts-ignore
})(),
);
});
addEventListener("fetch", (event: any) => {
event.respondWith(
(async () => {
const req = event.request;
const url = new URL(req.url);
if (
["/index.html", "/index.js", "/"].includes(url.pathname) ||
!url.pathname.startsWith("/browse/")
) {
return fetch(event.request).then((response: any) => {
response.redirectToFinalURL = true;
return response;
});
}
let realUrl = url.pathname.replace(/^\/browse\//, "").replace(/\/$/, "");
if (!realUrl.match(/^https?:\/\//)) {
realUrl = `http://${realUrl}`;
}
// Use your existing communication framework to resolve DNS.
const dnsResult = await dnsClient.resolve(new URL(realUrl).hostname);
if (!dnsResult.error && dnsResult.records.length > 0) {
return providerManager.fetch(dnsResult, new URL(realUrl).pathname);
}
return new Response("Sorry, that is not a valid web3 website.");
})(),
);
});

View File

@ -1,12 +0,0 @@
export interface ContentProvider {
supports: (uri: string) => boolean;
fetchContent: (
uri: string,
path: string,
query?: string,
) => Promise<Response>;
}
export interface ContentFilter {
process: (response: Response, mineType: string) => Promise<Response>;
}

View File

@ -1,53 +0,0 @@
import {
getKernelLoaded,
getLoginComplete,
getLogoutComplete,
} from "./vars.js";
import { objAsString } from "@lumeweb/libkernel";
export function sendAuthUpdate() {
window.parent.postMessage(
{
method: "kernelAuthStatus",
data: {
loginComplete: getLoginComplete(),
kernelLoaded: getKernelLoaded(),
logoutComplete: getLogoutComplete(),
},
},
"*",
);
}
function bootloaderWLog(isErr: boolean, ...inputs: any) {
// Build the message, each item gets its own line. We do this because items
// are often full objects.
let message = "[lumeweb-kernel-bootloader]";
for (let i = 0; i < inputs.length; i++) {
message += "\n";
message += objAsString(inputs[i]);
}
// Create the log by sending it to the parent.
window.parent.postMessage(
{
method: "log",
data: {
isErr,
message,
},
},
"*",
);
}
export function log(...inputs: any) {
bootloaderWLog(false, ...inputs);
}
export function logErr(...inputs: any) {
bootloaderWLog(true, ...inputs);
}
export function reloadKernel() {
window.location.reload();
}

View File

@ -1,103 +0,0 @@
import { x25519 } from "@noble/curves/ed25519";
import { defer } from "@lumeweb/libkernel/module";
import { KernelAuthStatus } from "@lumeweb/libkernel";
let queriesNonce = 1;
let queries: any = {};
let authStatus: KernelAuthStatus;
let authStatusKnown = false;
let authStatusDefer = defer();
let loggedInDefer = defer();
const store = new Map<string, any>(
Object.entries({
loginComplete: false,
logoutComplete: false,
kernelLoaded: "not yet",
communicationKey: null,
frontendCommunicationPubKey: null,
}),
);
export function setLoginComplete(status: boolean) {
store.set("loginComplete", status);
}
export function getLoginComplete(): boolean {
return store.get("loginComplete");
}
export function setLogoutComplete(status: boolean) {
store.set("logoutComplete", status);
}
export function getLogoutComplete(): boolean {
return store.get("logoutComplete");
}
export function setKernelLoaded(status: string) {
store.set("kernelLoaded", status);
}
export function getKernelLoaded(): string {
return store.get("kernelLoaded");
}
export function getCommunicationKey(): Uint8Array {
if (!store.get("communicationKey")) {
store.set("communicationKey", x25519.utils.randomPrivateKey());
}
return store.get("communicationKey");
}
export function getCommunicationPubKey() {
return x25519.getPublicKey(getCommunicationKey());
}
export function getFrontendCommunicationPubkey(): Uint8Array {
return store.get("frontendCommunicationPubKey");
}
export function setFrontendCommunicationPubkey(key: Uint8Array) {
store.set("frontendCommunicationPubKey", key);
}
export function getAuthStatusDefer() {
return authStatusDefer;
}
export function getQueriesNonce(): number {
return queriesNonce;
}
export function addQuery(nonce: any, func: Function) {
queries[nonce] = func;
}
export function increaseQueriesNonce() {
queriesNonce++;
}
export function setAuthStatus(status: KernelAuthStatus) {
authStatus = status;
}
export function getAuthStatusKnown() {
return authStatusKnown;
}
export function setAuthStatusKnown(status: boolean) {
authStatusKnown = status;
}
export function getAuthStatus(): KernelAuthStatus {
return authStatus;
}
export function getLoggedInDefer() {
return loggedInDefer;
}
export function resetLoggedInDefer() {
loggedInDefer = defer();
}
export function getQueries() {
return queries;
}
export function getQuery(nonce: any) {
return queries[nonce];
}
export function deleteQuery(nonce: any) {
delete queries[nonce];
}

76
styles/globals.css Normal file
View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 113 49% 55%;
--primary-foreground: black;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 0 0% 32%;
--input: 0 0% 32%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

76
tailwind.config.ts Normal file
View File

@ -0,0 +1,76 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}