init (working)

This commit is contained in:
polwex 2025-03-14 20:00:50 +07:00
commit f9266b8491
55 changed files with 11900 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
target/
node_modules/
bunlib

92
README.md Normal file
View File

@ -0,0 +1,92 @@
# Login with hyperware
Hyperware provides a secure PKI hosted in the Base network. Every hyperware node has a keypair which can be used to identify itself or sign messages.
For apps distributed inside hyperware, users are logged in by default, as the node reads the user identity from the hyperware node itself.
This package provides a library and example clients for using a Hyperware identity to login to applications and servers running outside hyperware.
There are three ways of using your Hyperware ID to authenticate to external applications:
1. Node-initiated
2. Server-initiated OTP
3. TOTP
## Node-initiated authentication
This method is suited for hybrid apps which have a pubilc facing frontend but also want to exclusive features to Hyperware users.
The setup requires two parts:
1. A hyperware package with a login frontend loaded inside hyperware ("the Client")
2. Handling logic on the server ("the Server").
The basic idea is that users will login to your server using the hyperware package. Your server will issue an authentication token (not a cookie, as browser cookies are associated with the request origin, so here the cookie is already used for the browser to login to the user's hyperware web UI). The hyperware package will store the authentication token in the main process state. When a user accesses the package frontend, it will first check for the existence of an authentication token. If none exists, it will serve a login page. If it does, it will request your public facing frontend adding the auth token as a header in every request.
### Server-side (Your public facing server)
You can see an example server as a barebones NextJS application in the `nextclient` folder.
1. Import the `hyperware-login` JavaScript library (the `lib` folder in this repo) to your app.
2. Set an endpoint in your server expecting an HTTP `POST` request with the following body:
```json
{
"node": <the user's node id name>,
"message": <the message that was signed as an array of numbers>,
"signature": <the signature as an array of numbers>
}
```
3. Instantiate a Kimap client by running:
```ts
import "KimapClient" from "hyperware-login"
const kimap = new KimapClient(process.env.BASESCAN_API_KEY!);
```
4. Verify the signature sent to the endpoint by running:
```ts
const verificationResponse = await kimap.verifySignature(
node,
message,
signature,
);
```
The `kimap.verifySignature()` method will return a `Promise<{ok: boolean} | {error: string}>`
If the verification is successful, generate an authentication token of your choice as a string.
5. Send a response to the request. In the example, if the verification is successful, the NextJS endpoint returns `{ok: true}`, and an HTTP header `x-hyperware-auth` with the authentication token if the verification was successful. If the verification was unsuccessful the server returns `{error: string}` with different error messages, depending if the signature verification failed or there was any other error in the process.
6. Handle users accessing your public frontend from within hyperware. In the example, requests from hyperware have an HTTP header `hypr-auth` with the authentication token issued by your server.
### Client-side (Hyperware package)
You can see a barebones example in the `client` folder. You can load it by cloning the repo and running `kit bs` inside the `client` folder.
- Make a hyperware package with a UI.
- The package backend should expect requests, to three kinds of paths: the login frontend, the login API endpoint, and anything else, which will be redirected to your server. In the example, these are respectively `GET /hypr-login`, `POST /hypr-login` and else.
- Make a simple login interface in HTML (as in `client/pkg/ui/index.html`) and have the package serve that UI from a non root path. In the provided example the path is `/hypr-login`.
- The frontend should have a button that triggers an ajax `POST` request to the package backend in a non root path. In the example (see `client/pkg/ui/script.js`) we send the request to `<package_path>/hypr-login`.
- The request body should be a json object with the following shape:
```json
{
"Sign": null
}
```
- Handle that request in the package backend (`client/loginex/src/lib.rs` in the example):
- You can see an example handling the login request in the `handle_login_request()` function. The login request will do the following:
1. Generate a signature with the user's hyperware identity's keypair.
2. Send an HTTP `POST` request to the public facing server with the following json body:
```json
{
"node": <the user's node id name>,
"message": <the message that was signed as an array of numbers>,
"signature": <the signature as an array of numbers>
}
```
3. Await the response from the server.
4. If the response is positive, grab the authentication token from the response. In the provided example the token is in the `x-hyperware-auth` header of the response. Save the authentication token to the process state.
5. Send the result back to the hyperware frontend as a json object of the following shape:
```ts
{ok: boolean} | {error: string}
```
You are encouraged to handle the errors in your frontend to inform the user of the problem or however you see fit.
If the response is successful, redirect the frontend to the root path of the package. Now that an authentication token has been set, this will request and render your public frontend.
## OTP (One Time Password)
## TOTP (Time-based One Time Password)

4379
client/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
client/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[workspace]
resolver = "2"
members = [
"loginex",
]
[profile.release]
panic = "abort"
opt-level = "s"
lto = true

View File

@ -0,0 +1,59 @@
interface contacts {
enum capability {
read-name-only,
read,
add,
remove,
}
variant request {
/// requires ReadNameOnly capability
/// lazy-load-blob: none.
get-names,
/// requires Read capability
/// lazy-load-blob: none.
get-all-contacts,
/// requires Read capability
/// lazy-load-blob: none.
get-contact(string),
/// requires Add capability
/// lazy-load-blob: none.
add-contact(string),
/// requires Add capability
/// lazy-load-blob: none.
/// tuple<node, field, value>
add-field(tuple<string, string, string>),
/// requires Remove capability
/// lazy-load-blob: none.
remove-contact(string),
/// requires Remove capability
/// lazy-load-blob: none.
/// tuple<node, field>
remove-field(tuple<string, string>),
}
variant response {
/// lazy-load-blob: none.
get-names(list<string>),
/// lazy-load-blob: required; JSON all-contacts dict in blob.
get-all-contacts,
/// lazy-load-blob: required; JSON contact dict in blob.
get-contact,
/// lazy-load-blob: none.
add-contact,
/// lazy-load-blob: none.
add-field,
/// lazy-load-blob: none.
remove-contact,
/// lazy-load-blob: none.
remove-field,
/// any failed request will receive this response
/// lazy-load-blob: none.
err(string),
}
}
world contacts-sys-v0 {
import contacts;
include process-v1;
}

25
client/loginex/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "loginex"
version = "0.1.0"
edition = "2021"
[features]
simulation-mode = []
[dependencies]
hex = "0.4.3"
lol_html = "2.2.0"
anyhow = "1.0"
hyperware_process_lib = { version = "1.0.3", features = ["logging"] }
process_macros = "0.1.0"
regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
url = "2.5.4"
wit-bindgen = "0.36.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "hyperware:process"

1
client/loginex/src/icon Normal file
View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwMCIgaGVpZ2h0PSIxMDAwIiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI5NjAiIGhlaWdodD0iOTYwIiByeD0iNDgwIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMjA5MV8xMDI4MikiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CjxyZWN0IHg9IjIwIiB5PSIyMCIgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI5NjAiIHJ4PSI0ODAiIHN0cm9rZT0idXJsKCNwYWludDFfbGluZWFyXzIwOTFfMTAyODIpIiBzdHJva2Utd2lkdGg9IjQwIi8+CjxwYXRoIGQ9Ik01ODEuMTE2IDU4NS43ODNMNjY0LjUzOCA2NjkuMTJDNjE3LjY5OSA3MTUuNzU4IDU1Ni42MjUgNzM5IDQ5NS4yNDUgNzM5QzQzMy44NjUgNzM5IDM3Mi42MzggNzE1LjYwNSAzMjYuMTA1IDY2OS4xMkMyODAuNzk3IDYyMy44NTggMjU2IDU2My45MTcgMjU2IDUwMEMyNTYgNDM2LjA4MyAyODAuOTUgMzc2LjE0MiAzMjYuMTA1IDMzMC44OEMzNzEuMjYgMjg1LjYxOSA0MzEuMjYyIDI2MSA0OTUuMjQ1IDI2MUM1NTkuMjI3IDI2MSA2MTkuMjMgMjg1LjkyNCA2NjQuNTM4IDMzMC44OEw1ODAuOTYzIDQxNC41MjNDNTQ1LjYwNCAzODMuOTQgNTA1LjE5NCAzNzQuNzY2IDQ2MS4yNjQgMzkxLjEyN0MzOTQuOTg1IDQxNS44OTkgMzY2LjgyMSA0ODkuMjk2IDM5Ny40MzUgNTUyLjc1NEMzOTguNjU5IDU1NS4yMDEgMzk4Ljk2NSA1NTguNzE4IDM5OC4zNTMgNTYxLjMxN0MzOTUuNzUxIDU3Mi40OCAzOTIuNjg5IDU4My40OSAzODkuNzgxIDU5NC4zNDZDMzg2LjEwNyA2MDcuOTU1IDM5My45MTQgNjE1Ljc1NCA0MDcuNTM3IDYxMi4yMzdDNDE4LjU1OCA2MDkuMzMxIDQyOS41NzkgNjA2LjEyIDQ0MC42IDYwMy42NzRDNDQzLjM1NSA2MDIuOTA5IDQ0Ny4wMjkgNjAzLjIxNSA0NDkuNjMxIDYwNC40MzhDNDkwLjUgNjIzLjM5OSA1MjkuOTkxIDYyMC44IDU2Ny43OTkgNTk2LjE4MUM1NzIuNTQ0IDU5My4xMjMgNTc2LjY3NyA1ODkuNDUzIDU4MS4xMTYgNTg1Ljc4M1oiIGZpbGw9InVybCgjcGFpbnQyX2xpbmVhcl8yMDkxXzEwMjgyKSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzIwOTFfMTAyODIiIHgxPSI1MDAiIHkxPSIwIiB4Mj0iNTAwIiB5Mj0iMTAwMCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjM1NDIyIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8yMDkxXzEwMjgyIiB4MT0iNzgyLjUiIHkxPSI3My41IiB4Mj0iMTg1LjUiIHkyPSI4OTQuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjM1NDIyIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0E1MzEwQyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Ml9saW5lYXJfMjA5MV8xMDI4MiIgeDE9Ijc1NCIgeTE9Ijg5LjUiIHgyPSIyNTYiIHkyPSIxMDE1LjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0YzNTQyMiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNBNzMyMEQiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K

354
client/loginex/src/lib.rs Normal file
View File

@ -0,0 +1,354 @@
use crate::hyperware::process::contacts;
use anyhow;
use hyperware_process_lib::http::client::send_request_await_response;
use hyperware_process_lib::http::server::{send_response, HttpBindingConfig, WsBindingConfig};
use hyperware_process_lib::http::{Method, StatusCode};
use hyperware_process_lib::logging::{info, init_logging, Level};
use hyperware_process_lib::{
await_message, call_init, eth, get_blob, get_typed_state, homepage, http, hypermap, kiprintln,
set_state, Address, Capability, LazyLoadBlob, Message, Request,
};
use process_macros::SerdeJsonInto;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;
mod proxy;
// DEVS: replace with your Web2 site URL, login endpoint and login nonce
// TODO if testing with memedeck etc. disable the auth check below, see the other TODO comment
// const WEB2_URL: &str = "https://www.memedeck.xyz";
const WEB2_URL: &str = "http://localhost:3000";
const WEB2_LOGIN_ENDPOINT: &str = "http://localhost:3000/api/hypr-login";
const WEB2_LOGIN_NONCE: &str = "lorem ipsum";
wit_bindgen::generate!({
path: "target/wit",
world: "contacts-sys-v0",
generate_unused_types: true,
additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
});
#[derive(Debug, Serialize, Deserialize)]
enum FrontendRequest {
Sign,
CheckAuth,
Logout,
Debug(String),
}
#[derive(Debug, Serialize, Deserialize)]
enum LoginRequest {
Sign(SignRequest),
Verify { from: Address, data: SignResponse },
}
#[derive(Debug, Serialize, Deserialize)]
struct SignRequest {
pub site: String,
pub time: u64,
pub nonce: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, SerdeJsonInto)]
struct SignResponse {
pub body: SignRequest,
pub message: Vec<u8>,
pub signature: Vec<u8>,
}
const ICON: &str = include_str!("icon");
#[derive(Debug, Serialize, Deserialize)]
struct ProxyStateV1 {
// TODO this should probably be generic and go on login:sys:sys
pub auth: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "version")]
enum VersionedState {
/// State fully stored in memory, persisted using serde_json.
/// Future state version will use SQLite.
V1(ProxyStateV1),
}
impl VersionedState {
fn load() -> Self {
get_typed_state(|bytes| serde_json::from_slice(bytes))
.unwrap_or(Self::V1(ProxyStateV1 { auth: None }))
}
fn _save(&self) {
set_state(&serde_json::to_vec(&self).expect("Failed to serialize state!"));
}
fn save_auth(&self, auth: String) {
let ns = Self::V1(ProxyStateV1 { auth: Some(auth) });
set_state(&serde_json::to_vec(&ns).expect("Failed to serialize state!"));
}
fn wipe_auth(&self) {
let ns = Self::V1(ProxyStateV1 { auth: None });
set_state(&serde_json::to_vec(&ns).expect("Failed to serialize state!"));
}
fn get_auth(&self) -> Option<String> {
match self {
Self::V1(ps) => ps.auth.clone(),
}
}
}
call_init!(initialize);
fn initialize(our: Address) {
init_logging(Level::DEBUG, Level::INFO, None, None, None).unwrap();
info!("begin");
homepage::add_to_homepage("Login", Some(ICON), Some("/"), None);
let mut state = VersionedState::load();
let auth = state.get_auth();
kiprintln!("auth {:#?}", auth);
let mut http_server = http::server::HttpServer::new(5);
let http_config = HttpBindingConfig::default().secure_subdomain(false);
http_server
.serve_ui("ui", vec!["/hypr-login"], http_config.clone())
.expect("Failed to serve UI");
http_server.bind_http_path("/", http_config).unwrap();
http_server
.bind_ws_path("/", WsBindingConfig::default())
.unwrap();
// let http_config = HttpBindingConfig::default().secure_subdomain(true);
// http_server
// .serve_ui("ui", vec!["/hypr-login"], http_config.clone())
// .expect("Failed to serve UI");
// http_server.secure_bind_http_path("/").unwrap();
main_loop(&our, &mut state, &mut http_server);
}
fn main_loop(
our: &Address,
state: &mut VersionedState,
http_server: &mut http::server::HttpServer,
) {
loop {
match await_message() {
Err(_send_error) => {
// ignore send errors, local-only process
continue;
}
Ok(Message::Request {
source,
body,
capabilities,
..
}) => {
// ignore messages from other nodes -- technically superfluous check
// since manifest does not acquire networking capability
if source.node() != our.node {
continue;
}
let _ = handle_request(our, &source, &body, capabilities, state, http_server);
}
_ => continue, // ignore responses
}
}
}
fn handle_request(
our: &Address,
source: &Address,
body: &[u8],
_capabilities: Vec<Capability>,
state: &mut VersionedState,
http_server: &mut http::server::HttpServer,
) -> anyhow::Result<()> {
// source node is ALWAYS ourselves since networking is disabled
if source.process == "http-server:distro:sys" {
// receive HTTP requests and websocket connection messages from our server
let server_request = http_server.parse_request(body).unwrap();
match server_request {
http::server::HttpServerRequest::Http(request) => {
handle_http_request(our, state, &request)?;
}
// TODO handle websockets
_ => (),
};
};
Ok(())
}
/// Handle HTTP requests from our own frontend.
fn handle_http_request(
our: &Address,
state: &mut VersionedState,
http_request: &http::server::IncomingHttpRequest,
) -> anyhow::Result<()> {
let method = http_request.method().unwrap();
let url = http_request.url().unwrap();
let paths = url.path_segments().unwrap();
let last = paths.last().unwrap();
if last == "hypr-login" {
handle_login_request(our, state, method)?;
} else {
handle_page_request(our, state, http_request)?;
}
Ok(())
}
fn handle_login_request(
our: &Address,
state: &mut VersionedState,
method: Method,
) -> anyhow::Result<()> {
match method {
Method::POST => {
let blob = get_blob().unwrap();
let request_bytes = blob.bytes();
let request = serde_json::from_slice::<FrontendRequest>(request_bytes)?;
match request {
FrontendRequest::Sign => {
let target = Address::new(our.node(), ("login", "login", "sys"));
let lr = LoginRequest::Sign(SignRequest {
site: WEB2_URL.to_string(),
nonce: Some(WEB2_LOGIN_NONCE.to_string()),
time: get_now(),
});
// Get the signature from login:sys:sys
let res: SignResponse = Request::to(target)
.body(serde_json::to_vec(&lr)?)
.send_and_await_response(10)??
.body()
.try_into()?;
// Send signature to designated endpoint on Web2 app
attempt_login(our, state, res)?;
}
FrontendRequest::CheckAuth => {
let auth = state.get_auth();
let json = match auth {
None => serde_json::Value::Null,
Some(s) => json!(s),
};
send_response(StatusCode::OK, None, serde_json::to_vec(&json)?)
}
FrontendRequest::Logout => {
state.wipe_auth();
let json = serde_json::to_vec(&json!(true))?;
send_response(StatusCode::OK, None, json);
}
FrontendRequest::Debug(s) => {
let value = serde_json::from_str::<serde_json::Value>(&s)?;
kiprintln!("frontend log\n{:#?}\n", value);
}
}
}
_ => {
send_response(StatusCode::METHOD_NOT_ALLOWED, None, vec![]);
}
};
Ok(())
}
fn attempt_login(
our: &Address,
state: &mut VersionedState,
signature_response: SignResponse,
) -> anyhow::Result<()> {
let mut json_headers = HashMap::new();
json_headers.insert("Content-type".to_string(), "application/json".to_string());
let node = our.node();
let message = signature_response.message;
let signature = signature_response.signature;
let json =
serde_json::to_vec(&json!({"node":node, "message": message, "signature": signature}))?;
let url = Url::parse(WEB2_LOGIN_ENDPOINT)?;
let res = send_request_await_response(Method::POST, url, Some(json_headers), 5000, json)?;
let resbody = res.body();
let resjson = serde_json::from_slice::<serde_json::Value>(resbody)?;
let okres = resjson.get("ok");
match okres {
None => {
send_json_response(
StatusCode::OK,
&json!({"error": "Signature verification failed"}),
)?;
}
Some(_) => {
let auth_header = res.headers().get("x-hyperware-auth");
match auth_header {
None => {
send_json_response(
StatusCode::OK,
&json!({"error": "No auth string found in response"}),
)?;
}
Some(av) => {
let auth_string = av.to_str()?;
state.save_auth(auth_string.to_string());
send_json_response(StatusCode::OK, &json!({"ok": "go ahead"}))?;
}
}
}
};
Ok(())
}
fn handle_page_request(
our: &Address,
state: &mut VersionedState,
http_request: &http::server::IncomingHttpRequest,
) -> anyhow::Result<()> {
// TODO remove before release
let auth = state.get_auth();
// let auth = Some(String::new());
match auth {
None => {
let package = our.package_id();
let process = our.process();
let redirect_to = format!("/{}:{}/hypr-login", process, package);
let mut redirect_headers = HashMap::new();
redirect_headers.insert("Location".to_string(), redirect_to);
send_response(
StatusCode::TEMPORARY_REDIRECT,
Some(redirect_headers),
vec![],
);
Ok(())
}
Some(auth_token) => proxy::run_proxy(http_request, WEB2_URL, &auth_token),
}
}
fn _invalid_node(
hypermap: &hypermap::Hypermap,
node: &str,
) -> Option<(contacts::Response, Option<LazyLoadBlob>)> {
if hypermap
.get(&node)
.map(|(tba, _, _)| tba != eth::Address::ZERO)
.unwrap_or(false)
{
None
} else {
Some((
contacts::Response::Err("Node name invalid or does not exist".to_string()),
None,
))
}
}
fn send_json_response<T: serde::Serialize>(status: StatusCode, data: &T) -> anyhow::Result<()> {
let json_data = serde_json::to_vec(data)?;
send_response(
status,
Some(HashMap::from([(
String::from("Content-Type"),
String::from("application/json"),
)])),
json_data,
);
Ok(())
}
fn get_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}

599
client/loginex/src/proxy.rs Normal file
View File

@ -0,0 +1,599 @@
use std::collections::HashMap;
use hyperware_process_lib::http::server::IncomingHttpRequest;
use hyperware_process_lib::kiprintln;
use lol_html::html_content::ContentType;
use lol_html::{element, text, HtmlRewriter, Settings};
use regex::Regex;
use url::Url;
use hyperware_process_lib::{
get_blob, http::client::send_request_await_response, http::server::send_response,
};
fn replace_domain(original_url: &Url, new_domain: &str) -> anyhow::Result<Url> {
let mut new_url = Url::parse(new_domain)?;
new_url.set_path(original_url.path());
Ok(new_url)
}
fn split_first_path_segment(url: &Url) -> Result<(String, Url), url::ParseError> {
let mut new_url = url.clone();
// Get the first segment
let first_segment = url
.path_segments()
.and_then(|mut segments| segments.next().map(|s| s.to_string()))
.unwrap_or_default();
// Collect remaining segments
let segments: Vec<_> = url
.path_segments()
.map(|segments| segments.skip(1).collect::<Vec<_>>())
.unwrap_or_default();
// Create new path from remaining segments
let new_path = if segments.is_empty() {
"/"
} else {
&format!("/{}", segments.join("/"))
};
new_url.set_path(new_path);
Ok((first_segment, new_url))
}
fn replace_files(input: &str, output: &str) -> anyhow::Result<String> {
// TODO single quotes?
let file_ext_regex =
Regex::new(r#"\\\"\/[^"]*\.(css|js|ttf|woff2|ico|png|svg|jpg|jpeg|webp|html)[^"]*\\\""#)?;
let replaced = file_ext_regex
.replace_all(input, |caps: &regex::Captures| {
let capture = caps[0].to_string();
let quoteless = capture.replace(r#"\""#, "");
let news = format!(r#"\"/{}{}\""#, output, quoteless);
news
})
.to_string();
Ok(replaced)
}
fn replace_urls_css(input: &str, output: &str) -> anyhow::Result<String> {
// TODO single quotes?
let file_ext_regex = Regex::new(r#"url\((\/[^)]+)\)"#)?;
let replaced = file_ext_regex
.replace_all(input, |caps: &regex::Captures| {
let capture = caps[1].to_string();
let news = format!(r#"url(/{}{})"#, output, capture);
news
})
.to_string();
Ok(replaced)
}
fn window_shenanigans(s: &str) -> anyhow::Result<()> {
let rx = Regex::new(r#"window.location.(href|replace)[^)]+\)"#)?;
let mut count = 0;
for _mtch in rx.find_iter(s) {
count = count + 1;
}
if count > 1 {
kiprintln!("{} matches", count);
};
Ok(())
}
fn replace_urls_js(input: &str, output: &str) -> anyhow::Result<String> {
window_shenanigans(input);
// let file_ext_regex = Regex::new(r#"http"#)?;
// let replaced = file_ext_regex
// .replace_all(input, |caps: &regex::Captures| {
// let capture = caps[0].to_string();
// let news = format!(r#"/{}{})"#, output, capture);
// kiprintln!("js\n{}\n{}\njs", capture, news);
// news
// })
// .to_string();
// Ok(replaced)
Ok(input.to_owned())
}
fn modify_html(html_bytes: &[u8], prefix: &str) -> anyhow::Result<Vec<u8>> {
// Ensure prefix is clean (no leading/trailing slashes for consistency)
let prefix = prefix.trim_matches('/');
// List of attributes that can contain URLs
let url_attributes = vec![
"href",
"src",
"action",
"background",
"cite",
"data",
"icon",
"longdesc",
"manifest",
"poster",
"profile",
"usemap",
"classid",
"codebase",
"archive",
"code",
];
//
// Build a selector for elements with any of these attributes
let selector: String = url_attributes
.iter()
.map(|attr| format!("[{}]", attr))
.collect::<Vec<String>>()
.join(",");
// Output buffer for the rewritten HTML
let mut output = Vec::new();
// Create an HTML rewriter with element content handlers
let mut rewriter = HtmlRewriter::new(
Settings {
element_content_handlers: vec![
// Handler for elements with URL attributes
element!("head", move |el| {
el.prepend(&mother_script(prefix), ContentType::Html);
Ok(())
}),
element!(selector, move |el| {
for attr in &url_attributes {
if let Some(value) = el.get_attribute(attr) {
if value.starts_with('/') {
let new_value =
format!(r#"/{}/{}"#, prefix, value.trim_start_matches('/'));
el.set_attribute(attr, &new_value)?;
}
}
}
Ok(())
}),
text!("script", |el| {
let text_content = el.as_str();
// window_shenanigans(text_content);
let replaced = replace_files(text_content, prefix)?;
el.replace(&replaced, ContentType::Text);
Ok(())
}),
],
..Settings::default()
},
|c: &[u8]| output.extend_from_slice(c),
);
// Write the input HTML to the rewriter and finalize
rewriter.write(html_bytes)?;
rewriter.end()?;
Ok(output)
}
pub fn run_proxy(
request: &IncomingHttpRequest,
web2_url: &str,
auth_token: &str,
) -> anyhow::Result<()> {
let blob = get_blob().unwrap();
let body = blob.bytes().to_vec();
let url = replace_domain(&request.url()?, web2_url)?;
let (first_path_segment, url) = split_first_path_segment(&url)?;
let mut headers = HashMap::new();
headers.insert("hypr-auth".to_string(), auth_token.to_string());
kiprintln!("fetching\n{}", url.to_string());
let response = send_request_await_response(request.method()?, url, Some(headers), 6000, body)?;
let resheaders = response.headers();
// DEVS: choose which headers are necessary for the hyperware client
// don't put them all, that doesn't work
let content_type = resheaders.get("content-type").unwrap().to_str()?;
let mime_regex = Regex::new(";.*")?;
let mime = mime_regex.replace_all(content_type, "").to_string();
// kiprintln!("response headers from proxy {:#?}\n{}", resheaders, mime);
let mut headers = HashMap::new();
headers.insert("Content-type".to_string(), content_type.to_owned());
let body = match mime.as_str() {
"text/html" => {
let html = modify_html(response.body(), &first_path_segment)?;
html
}
"text/css" => {
let text = String::from_utf8_lossy(response.body()).to_string();
let replaced = replace_urls_css(&text, &first_path_segment)?;
replaced.as_bytes().to_vec()
}
"application/javascript" => {
let text = String::from_utf8_lossy(response.body()).to_string();
let replaced = replace_urls_js(&text, &first_path_segment)?;
replaced.as_bytes().to_vec()
}
_ => response.body().to_vec(),
};
// let body = modify_html(response.body(), &first_path_segment)?;
send_response(response.status(), Some(headers), body);
Ok(())
}
fn mother_script(prefix: &str) -> String {
let script_text = format!(
r#"
<script>
console.log("mother script running");
// Store original functions
const originalFetch = window.fetch;
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalCreateElement = document.createElement;
function getURL(resource){{
if (typeof resource === "string") {{
const isURL = URL.canParse(resource);
if (isURL) return URL.parse(resource)
else {{
const urlTry = (window.location.origin + resource);
if (URL.canParse(urlTry)) return URL.parse(urlTry)
else return null
}}
}}
else if ("pathname" in resource) return resource
else if ("method" in resource) return URL.parse(resource.url)
else {{
console.warn("weird fetch", resource);
return null
}}
}}
function reRouteToHyperware(url){{
console.log("Rewriting URL:", url);
if (url.origin === window.location.origin)
url.pathname = '{0}' + url.pathname;
else console.warn("external fetch?", url.href);
return url
}}
// Log all network activity for debugging
window.fetch = async function(resource, options) {{
const url1 = getURL(resource);
if (!url) return
const url = reRouteToHyperware(url1);
const res = await originalFetch(url, options);
const resc = res.clone();
const headers = {{}};
for (let [k, v] of resc.headers.entries()){{
headers[k] = v;
}}
console.warn({{url, status: resc.status, headers}});
return res
}};
XMLHttpRequest.prototype.open = function(method, url, ...rest) {{
console.log("XHR intercepted:", url);
let newUrl = url;
if (url.includes('/_next/') || url.includes('/static/chunks/')) {{
console.log("Rewriting XHR URL:", url);
newUrl = '{0}' + url;
}}
// return originalXHROpen.call(this, method, newUrl, ...rest);
}};
// Most important: intercept script tag creation since webpack uses this
document.createElement = function(tagName) {{
const element = originalCreateElement.call(document, tagName);
console.log("dynamic element created", tagName);
const ltn = tagName.toLowerCase();
if (ltn === 'link' || ltn === "a") {{
let originalHref = '';
Object.defineProperty(element, 'href', {{
get: function() {{
return originalHref;
}},
set: function(value) {{
console.log("setting href", {{new: value, old: element.getAttribute("href")}});
const uri = typeof value === "string" ? value : value.toString();
const url = getURL(uri);
originalHref = url;
return url;
}}
}});
}}
if (['script', 'img', 'video', 'audio', 'track', 'iframe'].includes(ltn)) {{
// Add a property setter interceptor for the src attribute
let originalSrc = '';
Object.defineProperty(element, 'src', {{
get: function() {{
return originalSrc;
}},
set: function(value) {{
const uri = typeof value === "string" ? value : value.toString();
console.log("setting src", {{new: value, old: element.src}});
// if (uri.includes("vercel")){{
// console.log({{element}});
// return ""
// }}
const url = getURL(uri);
if (!url) return
originalSrc = url
return url;
}}
}});
}}
if (ltn === 'img') {{
const originalSetAttribute = element.setAttribute;
element.setAttribute = function(name, value) {{
let newValue = value;
console.log("setting img", {{name, value}});
if (name === "src" || name === "href"){{
const uri = typeof value === "string" ? value : value.toString();
const url1 = getURL(uri);
if (url1) newValue = reRouteToHyperware(url1);
}}
return originalSetAttribute.call(this, name, newValue);
}};
}}
if (ltn === 'meta') {{
const originalSetAttribute = element.setAttribute;
element.setAttribute = function(name, value) {{
if (name === 'http-equiv' && value.toLowerCase() === 'refresh') {{
console.warn("META REFRESH DETECTED");
console.trace("Meta refresh stack trace");
debugger;
}}
return originalSetAttribute.call(this, name, value);
}};
}}
else console.log("element created", {{tagName, element}});
return element;
}};
// Add monitoring of script tags to see what's happening
function observeMutations(){{
let oldHref = document.location.href;
// let oldHref2 = window.location.href;
const body = document.querySelector("body");
const observer = new MutationObserver(mutations => {{
if (oldHref !== document.location.href){{
console.warn("mutation", {{new: document.location.href, old: oldHref}});
// console.warn("mutationw", {{neww: window.location.href, oldw: oldHref2}});
}};
for (let mutation of mutations){{
console.log(mutation.type, mutation)
for (let node of mutation.addedNodes){{
const attrs = node.getAttributeNames();
const data = Array.from(attrs).reduce((acc, item) => ({{...acc, [item]: node.getAttribute(item)}}), {{}});
data.outerHTML = node.outerHTML;
data.tagName = node.tagName;
fuckNext();
}}
}}
}});
observer.observe(body, {{childList: true, subtree: true, attributes: true, attributeOldValue: true}})
}}
// next router crap
let fuckingNext = false;
// Wait for Next.js to initialize
const fuckNext = () => {{
if (fuckingNext) return
if (window.next && window.next.router) {{
console.log("next router", window.next.router);
// Store original router methods
const originalPush = window.next.router.push;
const originalReplace = window.next.router.replace;
const originalPrefetch = window.next.router.prefetch;
// Override router methods
window.next.router.push = function() {{
console.log("Router push intercepted:", arguments);
// Return without calling the original to prevent navigation
return new Promise(() => {{}}); // Return a never-resolving promise
}};
window.next.router.replace = function() {{
console.log("Router replace intercepted:", arguments);
// Return without calling the original to prevent navigation
return new Promise(() => {{}});
}};
window.next.router.refresh= function() {{
console.log("Router refresh intercepted:", arguments);
// Return without calling the original to prevent navigation
return new Promise(() => {{}});
}};
window.next.router.prefetch = function() {{
console.log("Router prefetch intercepted:", arguments);
return originalPrefetch.call(arguments)
}};
window.next.router.hmrRefresh = function() {{
console.log("Router hmrRefresh intercepted:", arguments);
// Return without calling the original to prevent navigation
return new Promise(() => {{}});
}};
window.next.router.forward = function() {{
console.log("Router forward intercepted:", arguments);
// Return without calling the original to prevent navigation
return new Promise(() => {{}});
}};
window.next.router.back= function() {{
console.log("Router back intercepted:", arguments);
// Return without calling the original to prevent navigation
return new Promise(() => {{}});
}};
console.log("Next.js router methods intercepted");
fuckingNext = true;
}} else {{
console.log("Next.js router not found");
}}
}};
// Listeners
// Start observing when body is available
document.addEventListener("DOMContentLoaded",observeMutations);
navigation.addEventListener("navigate", (e) => {{
console.log("navigation", e.destination)
console.log("navigation", e.info)
console.log("navigation", e.navigationType)
console.log("navigation", e.userInitiated)
console.log("navigation", e.canIntercept)
e.intercept({{
async handler(){{
console.log("intercepting navigation")
}}
}})
}})
window.addEventListener("hashchange", (e) => {{
console.warn("navigation", window.location)
}})
window.addEventListener('beforeunload', function(e) {{
remoteLog("unloading!!", e);
}});
window.addEventListener('popstate', function(e) {{
remoteLog("popstate", e);
}});
window.addEventListener('pageswap', function(e) {{
remoteLog("pageswap", e);
}});
const originalListener = window.addEventListener;
window.addEventListener = function(type, listener, options) {{
if (type === 'popstate') {{
console.log("Popstate event listener added");
// Replace with no-op function
return originalListener.call(this, type, function(e) {{
console.log("Popstate event prevented", e);
e.preventDefault();
e.stopImmediatePropagation();
}}, options);
}}
return originalListener.call(this, type, listener, options);
}};
// // prevent redirects
// const originalLocationDescriptor = Object.getOwnPropertyDescriptor(window, 'location');
// // Override location to prevent redirects
// let interceptedLocation = {{ ...location }};
// Object.defineProperty(window, 'location', {{
// get: function() {{
// return interceptedLocation;
// }},
// set: function(value) {{
// console.log("PREVENTED REDIRECT to:", value);
// console.trace("Redirect stack trace");
// // Don't actually set it, just log it
// return interceptedLocation;
// }}
// }});
// // Also overwrite properties on our fake location
// for (let prop in interceptedLocation) {{
// if (typeof interceptedLocation[prop] !== 'function') {{
// Object.defineProperty(interceptedLocation, prop, {{
// get: function() {{
// return location[prop];
// }},
// set: function(value) {{
// console.log(`PREVENTED location.${{prop}} change to:`, value);
// console.trace(`location.${{prop}} change stack trace`);
// // Don't actually apply the change
// }}
// }});
// }}
// }}
// Store logs in sessionStorage
const originalConsoleLog = console.warn;
console.warn= function(...args) {{{{
remoteLog(...args);
// originalConsoleLog.apply(this, args);
}}}};
// console.log = function() {{{{
// const jon = JSON.stringify(arguments);
// remoteLog(jon);
// console.log(arguments)
// originalConsoleLog.apply(this, arguments);
// }}}};
console.log("mother script finished loading");
async function remoteLog(...args){{
const logObj = Array.from(args).reduce((acc, item, i) => ({{...acc, [i.toString()]: safeStringify(item)}}), {{}});
await originalFetch("http://localhost:8283", {{
method: 'POST',
keepalive: true, // This allows the request to outlive the page
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{
Debug: logObj
}})
}})}}
function safeStringify(obj, depth = 0) {{
if (depth > 3) return "[Object depth limit exceeded]";
try {{
if (obj === null) return "null";
if (obj === undefined) return "undefined";
const type = typeof obj;
// Handle primitive types
if (type !== 'object' && type !== 'function') {{
return String(obj);
}}
// Handle special objects
if (obj instanceof Error) {{
return `Error: ${{obj.message}}\n${{obj.stack || ''}}`;
}}
if (obj instanceof HTMLElement) {{
return `[${{obj.tagName.toLowerCase()}}${{obj.id ? '#'+obj.id : ''}}]`;
}}
if (Array.isArray(obj)) {{
const items = obj.map(item => safeStringify(item, depth + 1));
return `[${{items.join(', ')}}]`;
}}
// Handle regular objects
const pairs = [];
for (const key in obj) {{
try {{
if (Object.prototype.hasOwnProperty.call(obj, key)) {{
const value = safeStringify(obj[key], depth + 1);
pairs.push(`${{key}}: ${{value}}`);
// Limit number of properties to avoid huge objects
if (pairs.length >= 10) {{
pairs.push("...");
break;
}}
}}
}} catch (e) {{
pairs.push(`${{key}}: [Error during serialization]`);
}}
}}
return `{{${{pairs.join(', ')}}}}`;
}} catch (e) {{
return `[Non-serializable: ${{e.message}}]`;
}}
}}
</script>
"#,
prefix
);
script_text
}

18
client/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "Login example",
"description": "Trying that login thingy",
"image": "",
"properties": {
"package_name": "login-example",
"current_version": "1.0.0",
"publisher": "sortugdev.os",
"mirrors": [],
"code_hashes": {
"1.0.0": ""
},
"wit_version": 1,
"dependencies": []
},
"external_url": "https://hyperware.ai",
"animation_url": ""
}

BIN
client/pkg/api.zip Normal file

Binary file not shown.

BIN
client/pkg/loginex.wasm Normal file

Binary file not shown.

23
client/pkg/manifest.json Normal file
View File

@ -0,0 +1,23 @@
[
{
"process_name": "loginex",
"process_wasm_path": "/loginex.wasm",
"on_exit": "None",
"request_networking": false,
"request_capabilities": [
"homepage:homepage:sys",
"http-server:distro:sys",
"http-client:distro:sys",
"login:login:sys",
"vfs:distro:sys"
],
"grant_capabilities": [
"homepage:homepage:sys",
"http-server:distro:sys",
"http-client:distro:sys",
"login:login:sys",
"vfs:distro:sys"
],
"public": false
}
]

18
client/pkg/scripts.json Normal file
View File

@ -0,0 +1,18 @@
{
"get_names.wasm": {
"root": false,
"public": false,
"request_networking": false,
"request_capabilities": [
"contacts:contacts:sys",
{
"process": "contacts:contacts:sys",
"params": "ReadNameOnly"
}
],
"grant_capabilities": [
"contacts:contacts:sys"
],
"wit_version": 1
}
}

91
client/pkg/ui/index.html Normal file
View File

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/hyperware.css">
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@400,700,500,600,300&display=swap" rel="stylesheet">
<script src="/our.js"></script>
<script>
document.title = "Hyperware Login Example - " + window.our.node;
</script>
<style>
h1,
h2,
h3,
h4,
h5,
h6,
p,
a,
li {
font-family: 'Kode Mono', monospace;
}
#title {
display: flex;
align-items: center;
padding: 20px;
max-width: 720px;
min-width: 300px;
margin: 0 auto;
}
#title h1 {
margin: 0;
flex-grow: 1;
text-align: right;
}
#title button {
margin-right: 20px;
}
#form-wrapper {
max-width: 720px;
margin: 0 auto;
}
main {
margin: 0 auto;
padding: 20px;
max-width: 960px;
min-width: 300px;
}
#login-button {
display: block;
margin: auto;
}
#error-div {
color: darkred;
}
</style>
</head>
<body>
<span id="title">
<a id="back-button" href="/"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 576 512"
height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path
d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z">
</path>
</svg></a>
<h1>Hyperware Login</h1>
</span>
<main>
<article id="form-wrapper">
<form id="form">
<button type="submit" id="login-button">Login to </button>
</form>
</article>
<div id="error-div"></div>
<script src="/loginex:login-example:sortugdev.os/script.js"></script>
</main>
</body>
</html>

71
client/pkg/ui/script.js Normal file
View File

@ -0,0 +1,71 @@
// Devs: Replace this with the name of your hyperware package
const APP_PATH = '/loginex:login-example:sortugdev.os';
// Devs: Replace this with the name of your app
const APP_NAME = "Moomdeck";
document.addEventListener("DOMContentLoaded", checkAuth);
async function checkAuth() {
const res = await call_app({ CheckAuth: null });
const j = await res.json();
console.log({j})
if (j) {
const button = document.getElementById('login-button');
button.innerText = "Logout"
document.getElementById('form').addEventListener('submit', logout);
} else {
document.getElementById('form').addEventListener('submit', login);
}
}
async function call_app(body) {
return await fetch(APP_PATH + "/hypr-login", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
const logout = async (e) => {
e.preventDefault();
await call_app({Logout: null});
window.location.reload();
}
const login = (e) => {
e.preventDefault();
const body = {
Sign: null
};
send(body)
};
document.getElementById('login-button').innerText += ` ${APP_NAME}`;
const errorDiv = document.getElementById("error-div");
async function send(body) {
try {
const res = await call_app(body);
let j = await res.json();
console.log("signature...?", j);
if ("error" in j) errorDiv += j.error;
else redirect();
} catch (e) {
console.log(`${e}`)
}
}
async function redirect() {
window.location.replace(APP_PATH);
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(APP_PATH + '/sw.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}

19
client/pkg/ui/sw.js Normal file
View File

@ -0,0 +1,19 @@
const WEB2_URL = "https://www.memedeck.xyz";
const APP_PATH = "/loginex:login-example:sortugdev.os";
self.addEventListener('fetch', event => {
event.request.url = "/lmao";
console.log(event.request.url, "modified?")
// Create a new Headers object based on the original request's headers
const modifiedHeaders = new Headers(event.request.headers);
// Add a custom header as an example modification
modifiedHeaders.append('X-Custom-Header', 'my-value');
// Create a modified request with the new headers
const modifiedRequest = new Request(event.request, {
headers: modifiedHeaders
});
// Respond with the result of fetching the modified request
event.respondWith(fetch(modifiedRequest));
});

41
hyperware-hybrid-app/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/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 `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## 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/app/building-your-application/deploying) for more details.

View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

957
hyperware-hybrid-app/package-lock.json generated Normal file
View File

@ -0,0 +1,957 @@
{
"name": "hyperware-hybrid-app",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hyperware-hybrid-app",
"version": "0.1.0",
"dependencies": {
"hyperware-login": "file:../lib",
"next": "15.2.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
},
"../lib": {
"name": "hyperware-signature-js-lib",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@ethersproject/bytes": "^5.8.0",
"@noble/ed25519": "^2.2.3",
"ethers": "^6.13.5"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@next/env": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.2.tgz",
"integrity": "sha512-yWgopCfA9XDR8ZH3taB5nRKtKJ1Q5fYsTOuYkzIIoS8TJ0UAUKAGF73JnGszbjk2ufAQDj6mDdgsJAFx5CLtYQ==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz",
"integrity": "sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz",
"integrity": "sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz",
"integrity": "sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz",
"integrity": "sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz",
"integrity": "sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz",
"integrity": "sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz",
"integrity": "sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.2.tgz",
"integrity": "sha512-52nWy65S/R6/kejz3jpvHAjZDPKIbEQu4x9jDBzmB9jJfuOy5rspjKu4u77+fI4M/WzLXrrQd57hlFGzz1ubcQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@types/node": {
"version": "20.17.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@types/react": {
"version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001704",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz",
"integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT",
"optional": true
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/hyperware-login": {
"resolved": "../lib",
"link": true
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT",
"optional": true
},
"node_modules/nanoid": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz",
"integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/next/-/next-15.2.2.tgz",
"integrity": "sha512-dgp8Kcx5XZRjMw2KNwBtUzhngRaURPioxoNIVl5BOyJbhi9CUgEtKDO7fx5wh8Z8vOVX1nYZ9meawJoRrlASYA==",
"license": "MIT",
"dependencies": {
"@next/env": "15.2.2",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.2.2",
"@next/swc-darwin-x64": "15.2.2",
"@next/swc-linux-arm64-gnu": "15.2.2",
"@next/swc-linux-arm64-musl": "15.2.2",
"@next/swc-linux-x64-gnu": "15.2.2",
"@next/swc-linux-x64-musl": "15.2.2",
"@next/swc-win32-arm64-msvc": "15.2.2",
"@next/swc-win32-x64-msvc": "15.2.2",
"sharp": "^0.33.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"optional": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
}
}
}

View File

@ -0,0 +1,23 @@
{
"name": "hyperware-hybrid-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^19.0.0",
"hyperware-login": "file:../lib",
"react-dom": "^19.0.0",
"next": "15.2.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="207.85339mm"
height="212.0607mm"
viewBox="0 0 207.85339 212.0607"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.3.1 (9b9bdc1480, 2023-11-25, custom)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.0868961"
inkscape:cx="382.28126"
inkscape:cy="400.68228"
inkscape:window-width="2510"
inkscape:window-height="1371"
inkscape:window-x="50"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-0.27483167,-2.8325584)"><rect
style="vector-effect:non-scaling-stroke;fill:#e5ff44;fill-opacity:1;stroke:none;stroke-width:0.264583;-inkscape-stroke:hairline"
id="rect2"
width="183.83772"
height="184.75897"
x="9.7677469"
y="14.452851" /><path
style="fill:#000000"
d="M 23.429211,213.83557 C 12.319378,210.43918 2.438871,199.11529 0.817349,187.9205 0.32877,184.54741 0.133717,146.55637 0.383898,103.49598 0.882546,17.670433 0.481078,21.494955 9.989743,11.986296 19.562608,2.4134317 15.206152,2.8353267 104.48198,2.8353267 c 89.27583,0 84.91937,-0.421895 94.49224,9.1509693 9.58696,9.586966 9.15097,4.978313 9.15097,96.729124 0,91.75082 0.43599,87.14217 -9.15097,96.72913 -9.53208,9.53207 -5.54334,9.12475 -92.25534,9.421 -43.470493,0.14852 -80.950845,-0.31497 -83.289669,-1.02998 z m 39.687202,-31.67559 c 8.736853,-6.66392 10.048914,-11.38133 10.048914,-36.1301 0,-21.88556 0,-21.88556 4.359089,-26.24465 4.3591,-4.3591 4.3591,-4.3591 23.975024,-4.3591 19.61593,0 19.61593,0 23.97502,4.3591 4.3591,4.35909 4.3591,4.35909 4.38363,26.47002 0.0223,20.11479 0.32523,22.63935 3.35535,27.96413 6.05242,10.63581 22.05389,15.90641 32.26326,10.62694 2.29811,-1.1884 6.29121,-4.56711 8.87356,-7.50824 4.69518,-5.34751 4.69518,-5.34751 4.6896,-72.0299 -0.005,-63.564319 -0.14204,-66.906213 -2.92403,-71.468868 -9.36209,-15.354431 -34.65898,-13.881841 -42.92692,2.498873 -3.04561,6.034058 -3.35535,8.762975 -3.35535,29.561137 0,22.913398 0,22.913398 -4.3591,27.272488 -4.35909,4.3591 -4.35909,4.3591 -23.97502,4.3591 -19.615924,0 -19.615924,0 -23.975024,-4.3591 -4.359089,-4.35909 -4.359089,-4.35909 -4.359089,-27.735916 0,-26.438146 -1.193321,-30.866888 -10.159845,-37.705988 -4.499226,-3.431727 -6.712043,-4.016813 -15.19173,-4.016813 -8.514964,0 -10.684691,0.579126 -15.268455,4.075333 -10.073594,7.683504 -10.242943,9.063193 -9.763915,79.546984 0.426429,62.74455 0.426429,62.74455 4.111836,68.12106 5.740374,8.37442 12.601707,11.75236 22.638818,11.14542 6.288634,-0.38028 9.738092,-1.5082 13.584377,-4.44191 z M 39.8429,167.87594 c -2.211047,-2.44318 -2.468032,-9.01848 -2.468032,-63.1478 0,-56.640653 0.170614,-60.575063 2.727144,-62.888691 3.106905,-2.81171 8.060453,-3.184446 12.283585,-0.924294 2.780501,1.488078 2.8845,3.769328 2.8845,63.272405 0,54.12862 -0.288523,62.0172 -2.343423,64.0721 -3.164164,3.16417 -10.055899,2.96204 -13.083774,-0.38372 z m 110.22931,0.38372 c -2.05391,-2.05391 -2.34342,-9.87078 -2.34342,-63.27241 0,-66.938987 -0.18088,-65.615833 8.96957,-65.615833 8.85291,0 8.92566,0.534803 8.92566,65.615833 0,50.97438 -0.31535,59.43734 -2.32147,62.30148 -2.75682,3.93592 -9.752,4.44927 -13.23034,0.97093 z"
id="path2" /></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

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

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -0,0 +1,43 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import KimapClient from "hyperware-login";
import { NextRequest } from "next/server";
type Data =
| {
ok: boolean;
}
| { error: string };
export async function POST(req: NextRequest) {
try {
const { node, message, signature } = await req.json();
console.log({ node, message, signature });
const kimap = new KimapClient(process.env.BASESCAN_API_KEY!);
const verificationResponse = await kimap.verifySignature(
node,
message,
signature,
);
console.log({ verificationResponse });
if ("error" in verificationResponse)
return Response.json(verificationResponse, { status: 500 });
else {
// const isGud = true;
const isGud = verificationResponse.ok;
if (isGud) {
// generate some cookie-ish string here
const auth = "henlo";
const authToken = `${node}:${auth}`;
return Response.json(
{ ok: isGud },
{ headers: { "x-hyperware-auth": authToken } },
);
}
return Response.json({ error: "invalid signature" });
}
} catch (e) {
return Response.json({ error: "bad request" }, { status: 500 });
}
}

View File

@ -0,0 +1,42 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View File

@ -0,0 +1,39 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Hyperware Hybrid App Example",
description: "visit https://github.com/hyperware-ai/hyperware-login",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<link rel="icon" href="/favicon.png" type="image/png" sizes="16x16" />
<link
precedence="default"
href="https://db.onlinewebfonts.com/c/cecb4be234bd82f3bc858201426b2b59?family=CHANEY+Ultra+Extended"
rel="stylesheet"
/>
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}

View File

@ -0,0 +1,94 @@
import Image from "next/image";
import styles from "./page.module.css";
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol>
<li>
<h1>FUCK THIS</h1>
</li>
</ol>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
}

View File

@ -0,0 +1,197 @@
.page {
--gray-rgb: 0, 0, 0;
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
min-height: 100svh;
padding: 80px;
gap: 64px;
font-family: var(--font-geist-sans);
}
@media (prefers-color-scheme: dark) {
.page {
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
}
}
.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
.main ol {
font-family: var(--font-geist-mono);
padding-left: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: -0.01em;
list-style-position: inside;
}
.main li:not(:last-of-type) {
margin-bottom: 8px;
}
.main code {
font-family: inherit;
background: var(--gray-alpha-100);
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.ctas {
display: flex;
gap: 16px;
justify-content: center;
}
.ctas a {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
border: 1px solid transparent;
transition:
background 0.2s,
color 0.2s,
border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
a.primary {
background: var(--foreground);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--gray-alpha-200);
min-width: 158px;
}
.footer {
grid-row-start: 3;
display: flex;
gap: 24px;
}
.footer a {
display: flex;
align-items: center;
gap: 8px;
}
.footer img {
flex-shrink: 0;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
.footer a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
}
@media (max-width: 600px) {
.page {
padding: 32px;
padding-bottom: 80px;
}
.main {
align-items: center;
}
.main ol {
text-align: center;
}
.ctas {
flex-direction: column;
}
.ctas a {
font-size: 14px;
height: 40px;
padding: 0 16px;
}
a.secondary {
min-width: auto;
}
.footer {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
}
@media (prefers-color-scheme: dark) {
.hyw {
font-family: CHANEY Ultra Extended, sans-serif;
color: rgb(229 255 68 / var(--un-text-opacity));
}
}
@media (prefers-color-scheme: light) {
.hyw {
font-family: CHANEY Ultra Extended, sans-serif;
color: darkgreen;
}
}
.hywlogo {
font-size: 2.5rem;
}
.appheader {
margin: auto;
width: 70%;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,98 @@
import { headers, cookies } from "next/headers";
import Image from "next/image";
import styles from "./page.module.css";
export default async function Home() {
const hdrs = await headers();
const hypr_auth = hdrs.get("hypr-auth");
console.log(hdrs.get("hypr-auth"), "lol?");
const cokis = await cookies();
console.log({ cokis });
return (
<div className={styles.page}>
<main className={styles.main}>
<div className={styles.appheader}>
<Image
className={styles.logo}
src="/logo.svg"
alt="Hyperware logo"
width={180}
height={80}
priority
/>
<h3 className={`${styles.hyw} ${styles.hywlogo}`}>Hyperware Login</h3>
</div>
{hypr_auth ? <HyperLogged token={hypr_auth} /> : <LoginPrompt />}
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://hyperware.ai"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to hyperware.ai
</a>
</footer>
</div>
);
}
function HyperLogged({ token }: { token: string }) {
const [node, tok] = token.split(":");
return <div className={styles.ctas}>Hi {node}!</div>;
}
function LoginPrompt() {
return (
<div>
<p style={{ textAlign: "center", marginBottom: "2rem" }}>
You are not logged in
</p>
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Get on Hyperware
</a>
<a
href="https://hyperware.ai"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read More
</a>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"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,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

12
lib/dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import { ethers } from "ethers";
import type { AsyncRes } from "./types.js";
declare class KimapClient {
apiKey: string;
contract: ethers.Contract;
constructor(apiKey: string);
fetchAbi(): Promise<any>;
getPublicKey(name: string): AsyncRes<string>;
namehash(name: string): Promise<string>;
verifySignature(node: string, messageArray: number[], signatureArray: number[]): AsyncRes<boolean>;
}
export default KimapClient;

147
lib/dist/index.js vendored Normal file
View File

@ -0,0 +1,147 @@
import { ethers } from "ethers";
import * as ed from "@noble/ed25519";
const KIMAP_ABI = [
"function get(bytes32 namehash) external view returns (address tba, address owner, bytes memory data)",
"function leaf(bytes32 parenthash, bytes calldata label) external pure returns (bytes32 namehash)",
"function tbaOf(bytes32 entry) external view returns (address tba)",
];
const KIMAP_ADDRESS = "0x000000000044C6B8Cb4d8f0F889a3E47664EAeda";
// const KIMAP_ADDRESS = "0x969cAbCE3625224BA3d340ea4dC2f929301188Ad";
const RPC_URL = "https://base-mainnet.g.alchemy.com/v2/jWsKWKv217Jz-9Nnnu7uJpW6gqmBcNog";
class KimapClient {
apiKey;
contract;
constructor(apiKey) {
this.apiKey = apiKey;
const provider = ethers.getDefaultProvider(RPC_URL);
const contract = new ethers.Contract(KIMAP_ADDRESS, KIMAP_ABI, provider);
this.contract = contract;
}
async fetchAbi() {
const url = `https://api.basescan.org/api?module=contract&action=getabi&address=${KIMAP_ADDRESS}&apikey=${this.apiKey}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === "1") {
return data.result; // ABI as a JSON string
}
else {
throw new Error("Failed to fetch ABI: " + data.message);
}
}
catch (error) {
console.error("Error fetching ABI:", error);
throw error;
}
}
async getPublicKey(name) {
const hash = await this.namehash(name);
console.log({ name, hash, contract: KIMAP_ADDRESS });
// const res = await this.contract!.get(hash);
// console.log({ res });
const netKeyNoteNamehash = await this.contract.leaf(hash, ethers.toUtf8Bytes("~net-key"));
const stoof = await this.contract.get(hash);
console.log({ stoof });
const [tba, owner, data] = await this.contract.get(netKeyNoteNamehash);
if (!data || data === "0x") {
return { error: "no data" };
}
return { ok: data };
}
async namehash(name) {
let hash = ethers.ZeroHash;
const labels = name.split(".").reverse();
for (const part of labels) {
const newhash = await this.contract.leaf(hash, ethers.toUtf8Bytes(part));
hash = newhash;
}
return hash;
}
async verifySignature(node, messageArray, signatureArray) {
// This is not a Browser transaction signature, this doesn't work
try {
const publicKey = await this.getPublicKey(node);
console.log({ publicKey });
if ("error" in publicKey)
return { error: "no public key found for " + node };
const publicKeyBytes = ethers.getBytes(publicKey.ok);
let messageBytes = new Uint8Array(messageArray);
let signatureBytes = new Uint8Array(signatureArray);
const isValid = await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
return { ok: isValid };
}
catch (e) {
return { error: `${e}` };
}
}
}
export default KimapClient;
// export function getClient() {
// const client = new KimapClient(KIMAP_ADDRESS, Bun.env.BASESCAN_API_KEY!);
// return client;
// }
// async setContract() {
// const abi = await this.fetchAbi();
// // console.log(JSON.parse(abi));
// const provider = ethers.getDefaultProvider(RPC_URL);
// const contract = new ethers.Contract(
// this.contractAddress,
// abi,
// provider,
// );
// this.contract = contract;
// }
// async verifySignature(
// node: string,
// messageBytes: BytesLike,
// signatureBytes: BytesLike,
// ): Promise<boolean> {
// // This is not a Browser transaction signature, this doesn't work
// const publicKey = await this.getPublicKey(node);
// console.log({ publicKey });
// if ("error" in publicKey) return false;
// // const messageBytes = ethers.toUtf8Bytes(message);
// const r = ethers.hexlify(signatureBytes.slice(0, 32));
// const s = ethers.hexlify(signatureBytes.slice(32, 64));
// const v = Number(ethers.hexlify(signatureBytes.slice(64, 65)));
// const signature = { r, s, v };
// const messageHash = ethers.hashMessage(messageBytes);
// const recovered = ethers.recoverAddress(messageHash, signature);
// // const lol: ethers.SignatureLike = "";
// console.log({ messageHash, recovered, publicKey });
// return false;
// }
// }
// const toCheck = {
// data: {
// body: {
// site: "f",
// time: 1741297185312,
// nonce: "ff",
// },
// message: [
// 115, 111, 114, 116, 117, 103, 100, 101, 118, 111, 46, 111, 115, 64, 108,
// 111, 103, 105, 110, 58, 108, 111, 103, 105, 110, 58, 115, 121, 115, 123,
// 34, 83, 105, 103, 110, 34, 58, 123, 34, 115, 105, 116, 101, 34, 58, 34,
// 102, 34, 44, 34, 116, 105, 109, 101, 34, 58, 49, 55, 52, 49, 50, 57, 55,
// 49, 56, 53, 51, 49, 50, 44, 34, 110, 111, 110, 99, 101, 34, 58, 34, 102,
// 102, 34, 125, 125,
// ],
// signature: [
// 41, 45, 105, 167, 143, 76, 68, 129, 157, 100, 71, 117, 117, 151, 89, 80,
// 40, 118, 14, 21, 235, 190, 98, 239, 156, 131, 225, 216, 167, 160, 96, 43,
// 132, 148, 254, 29, 49, 164, 131, 181, 205, 197, 136, 63, 175, 223, 57,
// 231, 253, 87, 130, 12, 118, 153, 72, 201, 246, 125, 200, 70, 4, 176, 98,
// 9,
// ],
// },
// };
// async function run() {
// const client = new KimapClient(KIMAP_ADDRESS, BASESCAN_API_KEY);
// // await client.setContract();
// // client.getPublicKey("sortugdevo.os");
// const messageBytes = new Uint8Array(toCheck.data.message);
// const signatureBytes = new Uint8Array(toCheck.data.signature);
// client.verifySignature("sortugdevo.os", messageBytes, signatureBytes);
// }
// run();

6
lib/dist/types.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export type Result<T> = {
ok: T;
} | {
error: string;
};
export type AsyncRes<T> = Promise<Result<T>>;

1
lib/dist/types.js vendored Normal file
View File

@ -0,0 +1 @@
export {};

168
lib/package-lock.json generated Normal file
View File

@ -0,0 +1,168 @@
{
"name": "hyperware-signature-js-lib",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hyperware-signature-js-lib",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@ethersproject/bytes": "^5.8.0",
"@noble/ed25519": "^2.2.3",
"ethers": "^6.13.5"
}
},
"node_modules/@adraffy/ens-normalize": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
"license": "MIT"
},
"node_modules/@ethersproject/bytes": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz",
"integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==",
"funding": [
{
"type": "individual",
"url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"license": "MIT",
"dependencies": {
"@ethersproject/logger": "^5.8.0"
}
},
"node_modules/@ethersproject/logger": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz",
"integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==",
"funding": [
{
"type": "individual",
"url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"license": "MIT"
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/ed25519": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.2.3.tgz",
"integrity": "sha512-iHV8eI2mRcUmOx159QNrU8vTpQ/Xm70yJ2cTk3Trc86++02usfqFoNl6x0p3JN81ZDS/1gx6xiK0OwrgqCT43g==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@types/node": {
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/aes-js": {
"version": "4.0.0-beta.5",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
"license": "MIT"
},
"node_modules/ethers": {
"version": "6.13.5",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz",
"integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/ethers-io/"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "1.10.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.2",
"@types/node": "22.7.5",
"aes-js": "4.0.0-beta.5",
"tslib": "2.7.0",
"ws": "8.17.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"license": "MIT"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

18
lib/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "hyperware-signature-js-lib",
"version": "1.0.0",
"type": "module",
"description": "JavaScript library to validate hyperware signatures",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Hyperware",
"license": "ISC",
"dependencies": {
"@ethersproject/bytes": "^5.8.0",
"@noble/ed25519": "^2.2.3",
"ethers": "^6.13.5"
}
}

163
lib/src/index.ts Normal file
View File

@ -0,0 +1,163 @@
import { ethers, type BytesLike } from "ethers";
import type { AsyncRes } from "./types.js";
import * as ed from "@noble/ed25519";
const KIMAP_ABI = [
"function get(bytes32 namehash) external view returns (address tba, address owner, bytes memory data)",
"function leaf(bytes32 parenthash, bytes calldata label) external pure returns (bytes32 namehash)",
"function tbaOf(bytes32 entry) external view returns (address tba)",
];
const KIMAP_ADDRESS = "0x000000000044C6B8Cb4d8f0F889a3E47664EAeda";
// const KIMAP_ADDRESS = "0x969cAbCE3625224BA3d340ea4dC2f929301188Ad";
const RPC_URL =
"https://base-mainnet.g.alchemy.com/v2/jWsKWKv217Jz-9Nnnu7uJpW6gqmBcNog";
class KimapClient {
apiKey;
contract;
constructor(apiKey: string) {
this.apiKey = apiKey;
const provider = ethers.getDefaultProvider(RPC_URL);
const contract = new ethers.Contract(KIMAP_ADDRESS, KIMAP_ABI, provider);
this.contract = contract;
}
async fetchAbi() {
const url = `https://api.basescan.org/api?module=contract&action=getabi&address=${KIMAP_ADDRESS}&apikey=${this.apiKey}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.status === "1") {
return data.result; // ABI as a JSON string
} else {
throw new Error("Failed to fetch ABI: " + data.message);
}
} catch (error) {
console.error("Error fetching ABI:", error);
throw error;
}
}
async getPublicKey(name: string): AsyncRes<string> {
const hash = await this.namehash(name);
console.log({ name, hash, contract: KIMAP_ADDRESS });
// const res = await this.contract!.get(hash);
// console.log({ res });
const netKeyNoteNamehash = await this.contract.leaf(
hash,
ethers.toUtf8Bytes("~net-key"),
);
const stoof = await this.contract.get(hash);
console.log({ stoof });
const [tba, owner, data] = await this.contract.get(netKeyNoteNamehash);
if (!data || data === "0x") {
return { error: "no data" };
}
return { ok: data };
}
async namehash(name: string) {
let hash = ethers.ZeroHash;
const labels = name.split(".").reverse();
for (const part of labels) {
const newhash = await this.contract!.leaf(hash, ethers.toUtf8Bytes(part));
hash = newhash;
}
return hash;
}
async verifySignature(
node: string,
messageArray: number[],
signatureArray: number[],
): AsyncRes<boolean> {
// This is not a Browser transaction signature, this doesn't work
try {
const publicKey = await this.getPublicKey(node);
console.log({ publicKey });
if ("error" in publicKey)
return { error: "no public key found for " + node };
const publicKeyBytes = ethers.getBytes(publicKey.ok);
let messageBytes = new Uint8Array(messageArray);
let signatureBytes = new Uint8Array(signatureArray);
const isValid = await ed.verifyAsync(
signatureBytes,
messageBytes,
publicKeyBytes,
);
return { ok: isValid };
} catch (e) {
return { error: `${e}` };
}
}
}
export default KimapClient;
// export function getClient() {
// const client = new KimapClient(KIMAP_ADDRESS, Bun.env.BASESCAN_API_KEY!);
// return client;
// }
// async setContract() {
// const abi = await this.fetchAbi();
// // console.log(JSON.parse(abi));
// const provider = ethers.getDefaultProvider(RPC_URL);
// const contract = new ethers.Contract(
// this.contractAddress,
// abi,
// provider,
// );
// this.contract = contract;
// }
// async verifySignature(
// node: string,
// messageBytes: BytesLike,
// signatureBytes: BytesLike,
// ): Promise<boolean> {
// // This is not a Browser transaction signature, this doesn't work
// const publicKey = await this.getPublicKey(node);
// console.log({ publicKey });
// if ("error" in publicKey) return false;
// // const messageBytes = ethers.toUtf8Bytes(message);
// const r = ethers.hexlify(signatureBytes.slice(0, 32));
// const s = ethers.hexlify(signatureBytes.slice(32, 64));
// const v = Number(ethers.hexlify(signatureBytes.slice(64, 65)));
// const signature = { r, s, v };
// const messageHash = ethers.hashMessage(messageBytes);
// const recovered = ethers.recoverAddress(messageHash, signature);
// // const lol: ethers.SignatureLike = "";
// console.log({ messageHash, recovered, publicKey });
// return false;
// }
// }
// const toCheck = {
// data: {
// body: {
// site: "f",
// time: 1741297185312,
// nonce: "ff",
// },
// message: [
// 115, 111, 114, 116, 117, 103, 100, 101, 118, 111, 46, 111, 115, 64, 108,
// 111, 103, 105, 110, 58, 108, 111, 103, 105, 110, 58, 115, 121, 115, 123,
// 34, 83, 105, 103, 110, 34, 58, 123, 34, 115, 105, 116, 101, 34, 58, 34,
// 102, 34, 44, 34, 116, 105, 109, 101, 34, 58, 49, 55, 52, 49, 50, 57, 55,
// 49, 56, 53, 51, 49, 50, 44, 34, 110, 111, 110, 99, 101, 34, 58, 34, 102,
// 102, 34, 125, 125,
// ],
// signature: [
// 41, 45, 105, 167, 143, 76, 68, 129, 157, 100, 71, 117, 117, 151, 89, 80,
// 40, 118, 14, 21, 235, 190, 98, 239, 156, 131, 225, 216, 167, 160, 96, 43,
// 132, 148, 254, 29, 49, 164, 131, 181, 205, 197, 136, 63, 175, 223, 57,
// 231, 253, 87, 130, 12, 118, 153, 72, 201, 246, 125, 200, 70, 4, 176, 98,
// 9,
// ],
// },
// };
// async function run() {
// const client = new KimapClient(KIMAP_ADDRESS, BASESCAN_API_KEY);
// // await client.setContract();
// // client.getPublicKey("sortugdevo.os");
// const messageBytes = new Uint8Array(toCheck.data.message);
// const signatureBytes = new Uint8Array(toCheck.data.signature);
// client.verifySignature("sortugdevo.os", messageBytes, signatureBytes);
// }
// run();

2
lib/src/types.ts Normal file
View File

@ -0,0 +1,2 @@
export type Result<T> = { ok: T } | { error: string };
export type AsyncRes<T> = Promise<Result<T>>;

25
lib/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "nodenext",
"moduleResolution": "nodenext",
"moduleDetection": "force",
"declaration": true,
"jsx": "react-jsx",
"allowJs": true,
"rootDir": "src",
"outDir": "dist",
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

3706
login/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
login/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[workspace]
resolver = "2"
members = [
"login",
]
[profile.release]
panic = "abort"
opt-level = "s"
lto = true

View File

@ -0,0 +1,6 @@
interface all {}
world login-sys-v0 {
import all;
include process-v1;
}

22
login/login/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "login"
version = "0.1.0"
edition = "2021"
[features]
simulation-mode = []
[dependencies]
anyhow = "1.0.97"
hyperware_process_lib = "1.0.3"
process_macros = "0.1"
rmp-serde = "1.3.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.36.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "hyperware:process"

1
login/login/src/icon Normal file
View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwMCIgaGVpZ2h0PSIxMDAwIiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI5NjAiIGhlaWdodD0iOTYwIiByeD0iNDgwIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMjA5MV8xMDI4MikiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CjxyZWN0IHg9IjIwIiB5PSIyMCIgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI5NjAiIHJ4PSI0ODAiIHN0cm9rZT0idXJsKCNwYWludDFfbGluZWFyXzIwOTFfMTAyODIpIiBzdHJva2Utd2lkdGg9IjQwIi8+CjxwYXRoIGQ9Ik01ODEuMTE2IDU4NS43ODNMNjY0LjUzOCA2NjkuMTJDNjE3LjY5OSA3MTUuNzU4IDU1Ni42MjUgNzM5IDQ5NS4yNDUgNzM5QzQzMy44NjUgNzM5IDM3Mi42MzggNzE1LjYwNSAzMjYuMTA1IDY2OS4xMkMyODAuNzk3IDYyMy44NTggMjU2IDU2My45MTcgMjU2IDUwMEMyNTYgNDM2LjA4MyAyODAuOTUgMzc2LjE0MiAzMjYuMTA1IDMzMC44OEMzNzEuMjYgMjg1LjYxOSA0MzEuMjYyIDI2MSA0OTUuMjQ1IDI2MUM1NTkuMjI3IDI2MSA2MTkuMjMgMjg1LjkyNCA2NjQuNTM4IDMzMC44OEw1ODAuOTYzIDQxNC41MjNDNTQ1LjYwNCAzODMuOTQgNTA1LjE5NCAzNzQuNzY2IDQ2MS4yNjQgMzkxLjEyN0MzOTQuOTg1IDQxNS44OTkgMzY2LjgyMSA0ODkuMjk2IDM5Ny40MzUgNTUyLjc1NEMzOTguNjU5IDU1NS4yMDEgMzk4Ljk2NSA1NTguNzE4IDM5OC4zNTMgNTYxLjMxN0MzOTUuNzUxIDU3Mi40OCAzOTIuNjg5IDU4My40OSAzODkuNzgxIDU5NC4zNDZDMzg2LjEwNyA2MDcuOTU1IDM5My45MTQgNjE1Ljc1NCA0MDcuNTM3IDYxMi4yMzdDNDE4LjU1OCA2MDkuMzMxIDQyOS41NzkgNjA2LjEyIDQ0MC42IDYwMy42NzRDNDQzLjM1NSA2MDIuOTA5IDQ0Ny4wMjkgNjAzLjIxNSA0NDkuNjMxIDYwNC40MzhDNDkwLjUgNjIzLjM5OSA1MjkuOTkxIDYyMC44IDU2Ny43OTkgNTk2LjE4MUM1NzIuNTQ0IDU5My4xMjMgNTc2LjY3NyA1ODkuNDUzIDU4MS4xMTYgNTg1Ljc4M1oiIGZpbGw9InVybCgjcGFpbnQyX2xpbmVhcl8yMDkxXzEwMjgyKSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzIwOTFfMTAyODIiIHgxPSI1MDAiIHkxPSIwIiB4Mj0iNTAwIiB5Mj0iMTAwMCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjM1NDIyIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8yMDkxXzEwMjgyIiB4MT0iNzgyLjUiIHkxPSI3My41IiB4Mj0iMTg1LjUiIHkyPSI4OTQuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjM1NDIyIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0E1MzEwQyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Ml9saW5lYXJfMjA5MV8xMDI4MiIgeDE9Ijc1NCIgeTE9Ijg5LjUiIHgyPSIyNTYiIHkyPSIxMDE1LjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0YzNTQyMiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNBNzMyMEQiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K

165
login/login/src/lib.rs Normal file
View File

@ -0,0 +1,165 @@
use crate::hyperware::process;
use anyhow::{anyhow, Result};
use hyperware::process::standard::send_response;
use hyperware_process_lib::net::{NetAction, NetResponse};
use hyperware_process_lib::{
await_message, call_init, eth, get_blob, get_typed_state, homepage, http, hypermap, kiprintln,
set_state, Address, Capability, LazyLoadBlob, Message, NodeId, Request, Response,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
wit_bindgen::generate!({
path: "target/wit",
world: "login-sys-v0",
generate_unused_types: true,
additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
});
#[derive(Debug, Serialize, Deserialize)]
struct LoginStateV1 {
our: Address,
apps: HashMap<NodeId, String>,
}
#[derive(Debug, Serialize, Deserialize)]
enum LoginRequest {
Sign(SignRequest),
Verify { from: Address, data: SignResponse },
}
#[derive(Debug, Serialize, Deserialize)]
struct SignRequest {
pub site: String,
pub time: u64,
pub nonce: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SignResponse {
pub body: SignRequest,
pub message: Vec<u8>,
pub signature: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "version")]
enum VersionedState {
/// State fully stored in memory, persisted using serde_json.
/// Future state version will use SQLite.
V1(LoginStateV1),
}
impl VersionedState {
fn load(our: Address) -> Self {
get_typed_state(|bytes| serde_json::from_slice(bytes)).unwrap_or(Self::V1(LoginStateV1 {
our,
apps: HashMap::new(),
}))
}
fn save(&self) {
set_state(&serde_json::to_vec(&self).expect("Failed to serialize contacts state!"));
}
}
call_init!(initialize);
fn initialize(our: Address) {
let mut state = VersionedState::load(our.clone());
loop {
let msg = await_message();
match msg {
Err(_send_error) => {
// ignore send errors, local-only process
continue;
}
Ok(Message::Request {
source,
body,
capabilities,
..
}) => {
handle_request(&our, &source, &mut state, &body, capabilities).unwrap_or_default()
}
_ => continue, // ignore responses
}
}
}
fn handle_request(
our: &Address,
source: &Address,
state: &mut VersionedState,
request_bytes: &[u8],
capabilities: Vec<Capability>,
) -> Result<()> {
let req = serde_json::from_slice::<LoginRequest>(request_bytes)?;
match req {
LoginRequest::Sign(r) => handle_sign(our, r, request_bytes),
LoginRequest::Verify { from, data } => handle_verify(from, data),
}
}
// let message = [
// from.to_string().as_bytes(),
// &km.lazy_load_blob
// .as_ref()
// .unwrap_or(&lib::core::LazyLoadBlob {
// mime: None,
// bytes: vec![],
// })
// .bytes,
// ]
// .concat();
// pub fn validate_signature(from: &str, signature: &[u8], message: &[u8], pki: &OnchainPKI) -> bool {
// if let Some(peer_id) = pki.get(from) {
// let their_networking_key = signature::UnparsedPublicKey::new(
// &signature::ED25519,
// net_key_string_to_hex(&peer_id.networking_key),
// );
// their_networking_key.verify(message, signature).is_ok()
// } else {
// false
// }
// }
fn handle_sign(our: &Address, req: SignRequest, request_bytes: &[u8]) -> Result<()> {
let body = rmp_serde::to_vec(&NetAction::Sign)?;
let res = Request::to(("our", "net", "distro", "sys"))
.blob_bytes(request_bytes)
.body(body)
.send_and_await_response(10)??;
let Ok(NetResponse::Signed) = rmp_serde::from_slice::<NetResponse>(res.body()) else {
return Err(anyhow!("signature failed"));
};
let newblob = res.blob();
let message = [our.to_string().as_bytes(), request_bytes].concat();
match newblob {
None => Err(anyhow!("no blob")),
Some(b) => {
let lr = SignResponse {
body: req,
message,
signature: b.bytes().to_vec(),
};
let lrj = serde_json::to_vec(&lr)?;
Response::new().body(lrj).send()?;
Ok(())
}
}
}
fn handle_verify(from: Address, data: SignResponse) -> Result<()> {
let signature = data.signature;
let body = rmp_serde::to_vec(&NetAction::Verify { from, signature })?;
let req_bytes = rmp_serde::to_vec(&data.body)?;
let res = Request::to(("our", "net", "distro", "sys"))
.blob_bytes(req_bytes)
.body(body)
.send_and_await_response(10)??;
let resp = rmp_serde::from_slice::<NetResponse>(res.body())?;
match resp {
NetResponse::Verified(is_good) => {
Response::new().body(serde_json::to_vec(&is_good)?).send()?;
Ok(())
}
_ => Err(anyhow!("weird response")),
}
}

18
login/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "Login",
"description": "Login with Hyperware",
"image": "",
"properties": {
"package_name": "login",
"current_version": "1.0.0",
"publisher": "sys",
"mirrors": [],
"code_hashes": {
"1.0.0": ""
},
"wit_version": 1,
"dependencies": []
},
"external_url": "https://hyperware.ai",
"animation_url": ""
}

BIN
login/pkg/api.zip Normal file

Binary file not shown.

BIN
login/pkg/login.wasm Normal file

Binary file not shown.

11
login/pkg/manifest.json Normal file
View File

@ -0,0 +1,11 @@
[
{
"process_name": "login",
"process_wasm_path": "/login.wasm",
"on_exit": "Restart",
"request_networking": false,
"request_capabilities": ["net:distro:sys"],
"grant_capabilities": ["net:distro:sys"],
"public": false
}
]