feature/implement wss routes (#1196)

* feat(database): create schema for hway and motr

* fix(gateway): correct naming inconsistencies in handlers

* build: update schema file to be compatible with postgresql syntax

* fix: update schema to be compatible with PostgreSQL syntax

* chore: update query_hway.sql to follow sqlc syntax

* ```text
refactor: update query_hway.sql for PostgreSQL and sqlc
```

* feat: add vaults table to store encrypted data

* refactor: Update vaults table schema for sqlc compatibility

* chore(deps): Upgrade dependencies and add pgx/v5

* refactor(Makefile): move sqlc generate to internal/models

* docs(foundations): remove outdated pages

* chore(build): add Taskfile for build tasks

* refactor(embed): move embed files to internal package

* docs: add documentation for Cosmos SDK ORM
This commit is contained in:
Prad Nukala
2024-12-18 20:53:45 +00:00
committed by GitHub
parent fc001216a8
commit 6072f6ecfa
111 changed files with 4919 additions and 8584 deletions
+4 -7
View File
@@ -6,7 +6,7 @@ import (
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
config "github.com/onsonr/sonr/internal/config/hway"
"github.com/onsonr/sonr/internal/database"
"github.com/onsonr/sonr/internal/models/drivers/hwayorm"
"github.com/onsonr/sonr/pkg/common"
"github.com/onsonr/sonr/pkg/gateway/middleware"
"github.com/onsonr/sonr/pkg/gateway/routes"
@@ -15,11 +15,8 @@ import (
type Gateway = *echo.Echo
// New returns a new Gateway instance
func New(env config.Hway, ipc common.IPFS) (Gateway, error) {
db, err := database.NewDB(env)
if err != nil {
return nil, err
}
func New(env config.Hway, ipc common.IPFS, dbq *hwayorm.Queries) (Gateway, error) {
e := echo.New()
// Override default behaviors
e.IPExtractor = echo.ExtractIPDirect()
@@ -29,7 +26,7 @@ func New(env config.Hway, ipc common.IPFS) (Gateway, error) {
e.Use(echoprometheus.NewMiddleware("hway"))
e.Use(echomiddleware.Logger())
e.Use(echomiddleware.Recover())
e.Use(middleware.UseGateway(env, ipc, db))
e.Use(middleware.UseGateway(env, ipc, dbq))
routes.Register(e)
return e, nil
}
+7 -4
View File
@@ -3,13 +3,16 @@ package handlers
import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/gateway/middleware"
"github.com/onsonr/sonr/internal/nebula/input"
"github.com/onsonr/sonr/pkg/gateway/middleware"
)
// ValidateProfileHandle finds the chosen handle and verifies it is unique
func ValidateProfileHandle(c echo.Context) error {
// CheckProfileHandle finds the chosen handle and verifies it is unique
func CheckProfileHandle(c echo.Context) error {
handle := c.FormValue("handle")
if handle == "" {
return middleware.Render(c, input.HandleError(handle, "Please enter a valid handle"))
}
//
// if ok {
// return middleware.Render(c, input.HandleError(handle))
@@ -19,7 +22,7 @@ func ValidateProfileHandle(c echo.Context) error {
}
// ValidateProfileHandle finds the chosen handle and verifies it is unique
func ValidateIsHumanSum(c echo.Context) error {
func CheckIsHumanSum(c echo.Context) error {
// data := context.GetCreateProfileData(c)
// value := c.FormValue("is_human")
// intValue, err := strconv.Atoi(value)
+14 -2
View File
@@ -5,12 +5,24 @@ import (
"github.com/onsonr/sonr/pkg/gateway/middleware"
)
func RenderIndex(c echo.Context) error {
func HandleIndex(c echo.Context) error {
id := middleware.GetSessionID(c)
if id == "" {
return startNewSession(c)
}
return middleware.RenderInitial(c)
}
func startNewSession(c echo.Context) error {
// Initialize the session
err := middleware.NewSession(c)
if err != nil {
return middleware.RenderError(c, err)
}
// Render the initial view
return middleware.RenderInitial(c)
}
func continueExistingSession(c echo.Context, id string) error {
// Do some auth checks here
return middleware.RenderInitial(c)
}
+3 -4
View File
@@ -3,21 +3,20 @@ package handlers
import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/gateway/middleware"
"github.com/onsonr/sonr/pkg/gateway/types"
"github.com/onsonr/sonr/pkg/gateway/views"
)
func RenderProfileCreate(c echo.Context) error {
// numF, numL := middleware.GetHumanVerificationNumbers(c)
params := types.CreateProfileParams{
params := middleware.CreateProfileParams{
FirstNumber: int(middleware.CurrentBlock(c)),
LastNumber: int(middleware.CurrentBlock(c)),
}
return middleware.Render(c, views.RegisterProfileView(params))
return middleware.Render(c, views.RegisterProfileView(params.FirstNumber, params.LastNumber))
}
func RenderPasskeyCreate(c echo.Context) error {
return middleware.Render(c, views.RegisterPasskeyView(types.CreatePasskeyParams{}))
return middleware.Render(c, views.RegisterPasskeyView("", "", "", "", ""))
}
func RenderVaultLoading(c echo.Context) error {
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/gateway/types"
"github.com/onsonr/sonr/pkg/gateway/middleware"
)
@@ -16,7 +15,7 @@ func SubmitProfileHandle(c echo.Context) error {
// SubmitPublicKeyCredential submits a public key credential
func SubmitPublicKeyCredential(c echo.Context) error {
credentialJSON := c.FormValue("credential")
cred := &types.CredentialDescriptor{}
cred := &middleware.CredentialDescriptor{}
// Unmarshal the credential JSON
if err := json.Unmarshal([]byte(credentialJSON), cred); err != nil {
return middleware.RenderError(c, err)
+45 -6
View File
@@ -4,11 +4,10 @@ import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/internal/database/repository"
"github.com/onsonr/sonr/pkg/gateway/types"
"github.com/onsonr/sonr/internal/models/drivers/hwayorm"
)
func ListCredentials(c echo.Context, handle string) ([]*types.CredentialDescriptor, error) {
func ListCredentials(c echo.Context, handle string) ([]*CredentialDescriptor, error) {
cc, ok := c.(*GatewayContext)
if !ok {
return nil, echo.NewHTTPError(http.StatusInternalServerError, "Credentials Context not found")
@@ -17,10 +16,10 @@ func ListCredentials(c echo.Context, handle string) ([]*types.CredentialDescript
if err != nil {
return nil, err
}
return types.CredentialArrayToDescriptors(creds), nil
return CredentialArrayToDescriptors(creds), nil
}
func SubmitCredential(c echo.Context, cred *types.CredentialDescriptor) error {
func SubmitCredential(c echo.Context, cred *CredentialDescriptor) error {
origin := GetOrigin(c)
handle := GetHandle(c)
md := cred.ToModel(handle, origin)
@@ -30,7 +29,7 @@ func SubmitCredential(c echo.Context, cred *types.CredentialDescriptor) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Credentials Context not found")
}
_, err := cc.dbq.InsertCredential(bgCtx(), repository.InsertCredentialParams{
_, err := cc.dbq.InsertCredential(bgCtx(), hwayorm.InsertCredentialParams{
Handle: handle,
CredentialID: md.CredentialID,
Origin: origin,
@@ -42,3 +41,43 @@ func SubmitCredential(c echo.Context, cred *types.CredentialDescriptor) error {
}
return nil
}
// Define the credential structure matching our frontend data
type CredentialDescriptor struct {
ID string `json:"id"`
RawID string `json:"rawId"`
Type string `json:"type"`
AuthenticatorAttachment string `json:"authenticatorAttachment"`
Transports string `json:"transports"`
ClientExtensionResults map[string]string `json:"clientExtensionResults"`
Response struct {
AttestationObject string `json:"attestationObject"`
ClientDataJSON string `json:"clientDataJSON"`
} `json:"response"`
}
func (c *CredentialDescriptor) ToModel(handle, origin string) *hwayorm.Credential {
return &hwayorm.Credential{
Handle: handle,
Origin: origin,
CredentialID: c.ID,
Type: c.Type,
Transports: c.Transports,
AuthenticatorAttachment: c.AuthenticatorAttachment,
}
}
func CredentialArrayToDescriptors(credentials []hwayorm.Credential) []*CredentialDescriptor {
var descriptors []*CredentialDescriptor
for _, cred := range credentials {
cd := &CredentialDescriptor{
ID: cred.CredentialID,
RawID: cred.CredentialID,
Type: cred.Type,
AuthenticatorAttachment: cred.AuthenticatorAttachment,
Transports: cred.Transports,
}
descriptors = append(descriptors, cd)
}
return descriptors
}
+5 -7
View File
@@ -1,13 +1,11 @@
package middleware
import (
"database/sql"
"github.com/labstack/echo/v4"
"github.com/medama-io/go-useragent"
"github.com/onsonr/sonr/crypto/mpc"
"github.com/onsonr/sonr/internal/config/hway"
"github.com/onsonr/sonr/internal/database/repository"
"github.com/onsonr/sonr/internal/models/drivers/hwayorm"
"github.com/onsonr/sonr/pkg/common"
)
@@ -15,21 +13,21 @@ type GatewayContext struct {
echo.Context
agent useragent.UserAgent
id string
dbq *repository.Queries
dbq *hwayorm.Queries
ipfsClient common.IPFS
tokenStore common.IPFSTokenStore
stagedEnclaves map[string]mpc.Enclave
grpcAddr string
}
func UseGateway(env hway.Hway, ipc common.IPFS, db *sql.DB) echo.MiddlewareFunc {
func UseGateway(env hway.Hway, ipc common.IPFS, db *hwayorm.Queries) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ua := useragent.NewParser()
ctx := &GatewayContext{
agent: ua.Parse(c.Request().UserAgent()),
agent: ua.Parse(c.Request().UserAgent()),
Context: c,
dbq: repository.New(db),
dbq: db,
ipfsClient: ipc,
grpcAddr: env.GetSonrGrpcUrl(),
tokenStore: common.NewUCANStore(ipc),
+26 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/internal/context"
"github.com/onsonr/sonr/internal/database/repository"
repository "github.com/onsonr/sonr/internal/models/drivers/hwayorm"
)
func CheckHandleUnique(c echo.Context, handle string) bool {
@@ -98,3 +98,28 @@ func DeleteProfile(c echo.Context) error {
}
return nil
}
// ╭───────────────────────────────────────────────────────────╮
// │ Create Profile (/register/profile) │
// ╰───────────────────────────────────────────────────────────╯
// DefaultCreateProfileParams returns a default CreateProfileParams
func DefaultCreateProfileParams() CreateProfileParams {
return CreateProfileParams{
TurnstileSiteKey: "",
FirstNumber: 0,
LastNumber: 0,
}
}
// CreateProfileParams represents the parameters for creating a profile
type CreateProfileParams struct {
TurnstileSiteKey string
FirstNumber int
LastNumber int
}
// Sum returns the sum of the first and last number
func (d CreateProfileParams) Sum() int {
return d.FirstNumber + d.LastNumber
}
+57 -7
View File
@@ -3,9 +3,12 @@ package middleware
import (
gocontext "context"
"github.com/go-webauthn/webauthn/protocol"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/internal/context"
"github.com/onsonr/sonr/internal/database"
"github.com/medama-io/go-useragent"
ctx "github.com/onsonr/sonr/internal/context"
"github.com/onsonr/sonr/internal/models/drivers/hwayorm"
"github.com/segmentio/ksuid"
)
func NewSession(c echo.Context) error {
@@ -13,13 +16,13 @@ func NewSession(c echo.Context) error {
if !ok {
return nil
}
baseSessionCreateParams := database.BaseSessionCreateParams(cc)
baseSessionCreateParams := BaseSessionCreateParams(cc)
cc.id = baseSessionCreateParams.ID
if _, err := cc.dbq.CreateSession(bgCtx(), baseSessionCreateParams); err != nil {
return err
}
// Set Cookie
if err := context.WriteCookie(c, context.SessionID, cc.id); err != nil {
if err := ctx.WriteCookie(c, ctx.SessionID, cc.id); err != nil {
return err
}
return nil
@@ -46,10 +49,10 @@ func GetSessionID(c echo.Context) string {
}
// check from cookie
if cc.id == "" {
if ok := context.CookieExists(c, context.SessionID); !ok {
if ok := ctx.CookieExists(c, ctx.SessionID); !ok {
return ""
}
cc.id = context.ReadCookieUnsafe(c, context.SessionID)
cc.id = ctx.ReadCookieUnsafe(c, ctx.SessionID)
}
return cc.id
}
@@ -68,7 +71,7 @@ func GetSessionChallenge(c echo.Context) string {
func GetHandle(c echo.Context) string {
// First check for the cookie
handle := context.ReadCookieUnsafe(c, context.UserHandle)
handle := ctx.ReadCookieUnsafe(c, ctx.UserHandle)
if handle != "" {
return handle
}
@@ -107,3 +110,50 @@ func bgCtx() gocontext.Context {
ctx := gocontext.Background()
return ctx
}
func BaseSessionCreateParams(e echo.Context) hwayorm.CreateSessionParams {
// f := rand.Intn(5) + 1
// l := rand.Intn(4) + 1
challenge, _ := protocol.CreateChallenge()
id := getOrCreateSessionID(e)
ua := useragent.NewParser()
s := ua.Parse(e.Request().UserAgent())
return hwayorm.CreateSessionParams{
ID: id,
BrowserName: s.GetBrowser(),
BrowserVersion: s.GetMajorVersion(),
ClientIpaddr: e.RealIP(),
Platform: s.GetOS(),
IsMobile: s.IsMobile(),
IsTablet: s.IsTablet(),
IsDesktop: s.IsDesktop(),
IsBot: s.IsBot(),
IsTv: s.IsTV(),
// IsHumanFirst: int64(f),
// IsHumanLast: int64(l),
Challenge: challenge.String(),
}
}
func getOrCreateSessionID(c echo.Context) string {
if ok := ctx.CookieExists(c, ctx.SessionID); !ok {
sessionID := ksuid.New().String()
ctx.WriteCookie(c, ctx.SessionID, sessionID)
return sessionID
}
sessionID, err := ctx.ReadCookie(c, ctx.SessionID)
if err != nil {
sessionID = ksuid.New().String()
ctx.WriteCookie(c, ctx.SessionID, sessionID)
}
return sessionID
}
func boolToInt64(b bool) int64 {
if b {
return 1
}
return 0
}
+30 -7
View File
@@ -6,11 +6,10 @@ import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/crypto/mpc"
"github.com/onsonr/sonr/internal/context"
"github.com/onsonr/sonr/pkg/gateway/types"
"lukechampine.com/blake3"
)
func Spawn(c echo.Context) (types.CreatePasskeyParams, error) {
func Spawn(c echo.Context) (CreatePasskeyParams, error) {
cc := c.(*GatewayContext)
block := fmt.Sprintf("%d", CurrentBlock(c))
handle := GetHandle(c)
@@ -19,15 +18,15 @@ func Spawn(c echo.Context) (types.CreatePasskeyParams, error) {
sid := GetSessionID(c)
nonce, err := calcNonce(sid)
if err != nil {
return types.DefaultCreatePasskeyParams(), err
return defaultCreatePasskeyParams(), err
}
encl, err := mpc.GenEnclave(nonce)
if err != nil {
return types.DefaultCreatePasskeyParams(), err
return defaultCreatePasskeyParams(), err
}
cc.stagedEnclaves[sid] = encl
context.WriteCookie(c, context.SonrAddress, encl.Address())
return types.CreatePasskeyParams{
return CreatePasskeyParams{
Address: encl.Address(),
Handle: handle,
Name: origin,
@@ -36,8 +35,8 @@ func Spawn(c echo.Context) (types.CreatePasskeyParams, error) {
}, nil
}
func Claim() (types.CreatePasskeyParams, error) {
return types.CreatePasskeyParams{}, nil
func Claim() (CreatePasskeyParams, error) {
return CreatePasskeyParams{}, nil
}
// Uses blake3 to hash the sessionID to generate a nonce of length 12 bytes
@@ -55,3 +54,27 @@ func calcNonce(sessionID string) ([]byte, error) {
}
return nonce, nil
}
// ╭───────────────────────────────────────────────────────────╮
// │ Create Passkey (/register/passkey) │
// ╰───────────────────────────────────────────────────────────╯
// defaultCreatePasskeyParams returns a default CreatePasskeyParams
func defaultCreatePasskeyParams() CreatePasskeyParams {
return CreatePasskeyParams{
Address: "",
Handle: "",
Name: "",
Challenge: "",
CreationBlock: "",
}
}
// CreatePasskeyParams represents the parameters for creating a passkey
type CreatePasskeyParams struct {
Address string
Handle string
Name string
Challenge string
CreationBlock string
}
+3 -3
View File
@@ -7,14 +7,14 @@ import (
func Register(e *echo.Echo) error {
// Register View Handlers
e.GET("/", handlers.RenderIndex)
e.GET("/", handlers.HandleIndex)
e.GET("/register", handlers.RenderProfileCreate)
e.POST("/register/passkey", handlers.RenderPasskeyCreate)
e.POST("/register/finish", handlers.RenderVaultLoading)
// Register Validation Handlers
e.POST("/register/profile/handle", handlers.ValidateProfileHandle)
e.POST("/register/profile/is_human", handlers.ValidateIsHumanSum)
e.POST("/register/profile/handle", handlers.CheckProfileHandle)
e.POST("/register/profile/is_human", handlers.CheckIsHumanSum)
e.POST("/submit/profile/handle", handlers.SubmitProfileHandle)
e.POST("/submit/credential", handlers.SubmitPublicKeyCredential)
return nil
-64
View File
@@ -1,64 +0,0 @@
package embed
import (
"encoding/json"
"github.com/ipfs/boxo/files"
config "github.com/onsonr/sonr/internal/config/motr"
"github.com/onsonr/sonr/internal/models"
)
const SchemaVersion = 1
const (
AppManifestFileName = "app.webmanifest"
DWNConfigFileName = "dwn.json"
IndexHTMLFileName = "index.html"
MainJSFileName = "main.js"
ServiceWorkerFileName = "sw.js"
)
// spawnVaultDirectory creates a new directory with the default files
func NewVaultFS(cfg *config.Config) (files.Directory, error) {
manifestBz, err := models.NewWebManifest()
if err != nil {
return nil, err
}
cnfBz, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
return files.NewMapDirectory(map[string]files.Node{
AppManifestFileName: files.NewBytesFile(manifestBz),
DWNConfigFileName: files.NewBytesFile(cnfBz),
IndexHTMLFileName: files.NewBytesFile(IndexHTML),
MainJSFileName: files.NewBytesFile(MainJS),
ServiceWorkerFileName: files.NewBytesFile(WorkerJS),
}), nil
}
// NewVaultConfig returns the default vault config
func NewVaultConfig(addr string, ucanCID string) *config.Config {
return &config.Config{
MotrToken: ucanCID,
MotrAddress: addr,
IpfsGatewayUrl: "http://localhost:80",
SonrApiUrl: "http://localhost:1317",
SonrRpcUrl: "http://localhost:26657",
SonrChainId: "sonr-testnet-1",
VaultSchema: DefaultSchema(),
}
}
// DefaultSchema returns the default schema
func DefaultSchema() *config.Schema {
return &config.Schema{
Version: SchemaVersion,
Account: getSchema(&models.Account{}),
Asset: getSchema(&models.Asset{}),
Chain: getSchema(&models.Chain{}),
Credential: getSchema(&models.Credential{}),
Grant: getSchema(&models.Grant{}),
Keyshare: getSchema(&models.Keyshare{}),
Profile: getSchema(&models.Profile{}),
}
}
-138
View File
@@ -1,138 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sonr DWN</title>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- WASM Support -->
<script src="https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js"></script>
<!-- Main JS -->
<script src="main.js"></script>
<!-- Tailwind (assuming you're using it based on your classes) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Add manifest for PWA support -->
<link
rel="manifest"
href="/app.webmanifest"
crossorigin="use-credentials"
/>
<!-- Offline detection styles -->
<style>
.offline-indicator {
display: none;
}
body.offline .offline-indicator {
display: block;
background: #f44336;
color: white;
text-align: center;
padding: 0.5rem;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
</style>
</head>
<body
class="flex items-center justify-center h-full bg-zinc-50 lg:p-24 md:16 p-4"
>
<!-- Offline indicator -->
<div class="offline-indicator">
You are currently offline. Some features may be limited.
</div>
<!-- Loading indicator -->
<div
id="loading-indicator"
class="fixed top-0 left-0 w-full h-1 bg-blue-200 transition-all duration-300"
style="display: none"
>
<div class="h-full bg-blue-600 w-0 transition-all duration-300"></div>
</div>
<main
class="flex-row items-center justify-center mx-auto w-fit max-w-screen-sm gap-y-3"
>
<div
id="content"
hx-get="/#"
hx-trigger="load"
hx-swap="outerHTML"
hx-indicator="#loading-indicator"
>
Loading...
</div>
</main>
<!-- WASM Ready Indicator (hidden) -->
<div
id="wasm-status"
class="hidden fixed bottom-4 right-4 p-2 rounded-md bg-green-500 text-white"
hx-swap-oob="true"
>
WASM Ready
</div>
<script>
// Initialize service worker
if ("serviceWorker" in navigator) {
window.addEventListener("load", async function () {
try {
const registration =
await navigator.serviceWorker.register("/sw.js");
console.log(
"Service Worker registered with scope:",
registration.scope,
);
} catch (error) {
console.error("Service Worker registration failed:", error);
}
});
}
// HTMX loading indicator
htmx.on("htmx:beforeRequest", function (evt) {
document.getElementById("loading-indicator").style.display = "block";
});
htmx.on("htmx:afterRequest", function (evt) {
document.getElementById("loading-indicator").style.display = "none";
});
// WASM ready event handler
document.addEventListener("wasm-ready", function () {
const status = document.getElementById("wasm-status");
status.classList.remove("hidden");
setTimeout(() => {
status.classList.add("hidden");
}, 3000);
});
// Offline status handler
window.addEventListener("offline", function () {
document.body.classList.add("offline");
});
window.addEventListener("online", function () {
document.body.classList.remove("offline");
});
// Initial offline check
if (!navigator.onLine) {
document.body.classList.add("offline");
}
</script>
</body>
</html>
-152
View File
@@ -1,152 +0,0 @@
// MessageChannel for WASM communication
let wasmChannel;
let wasmPort;
async function initWasmChannel() {
wasmChannel = new MessageChannel();
wasmPort = wasmChannel.port1;
// Setup message handling from WASM
wasmPort.onmessage = (event) => {
const { type, data } = event.data;
switch (type) {
case 'WASM_READY':
console.log('WASM is ready');
document.dispatchEvent(new CustomEvent('wasm-ready'));
break;
case 'RESPONSE':
handleWasmResponse(data);
break;
case 'SYNC_COMPLETE':
handleSyncComplete(data);
break;
}
};
}
// Initialize WebAssembly and Service Worker
async function init() {
try {
// Register service worker
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('./sw.js');
console.log('ServiceWorker registered');
// Wait for the service worker to be ready
await navigator.serviceWorker.ready;
// Initialize MessageChannel
await initWasmChannel();
// Send the MessageChannel port to the service worker
navigator.serviceWorker.controller.postMessage({
type: 'PORT_INITIALIZATION',
port: wasmChannel.port2
}, [wasmChannel.port2]);
// Register for periodic sync if available
if ('periodicSync' in registration) {
try {
await registration.periodicSync.register('wasm-sync', {
minInterval: 24 * 60 * 60 * 1000 // 24 hours
});
} catch (error) {
console.log('Periodic sync could not be registered:', error);
}
}
}
// Initialize HTMX with custom config
htmx.config.withCredentials = true;
htmx.config.wsReconnectDelay = 'full-jitter';
// Override HTMX's internal request handling
htmx.config.beforeRequest = function (config) {
// Add request ID for tracking
const requestId = 'req_' + Date.now();
config.headers['X-Wasm-Request-ID'] = requestId;
// If offline, handle through service worker
if (!navigator.onLine) {
return false; // Let service worker handle it
}
return true;
};
// Handle HTMX after request
htmx.config.afterRequest = function (config) {
// Additional processing after request if needed
};
// Handle HTMX errors
htmx.config.errorHandler = function (error) {
console.error('HTMX Error:', error);
};
} catch (error) {
console.error('Initialization failed:', error);
}
}
function handleWasmResponse(data) {
const { requestId, response } = data;
// Process the WASM response
// This might update the UI or trigger HTMX swaps
const targetElement = document.querySelector(`[data-request-id="${requestId}"]`);
if (targetElement) {
htmx.process(targetElement);
}
}
function handleSyncComplete(data) {
const { url } = data;
// Handle successful sync
// Maybe refresh the relevant part of the UI
htmx.trigger('body', 'sync:complete', { url });
}
// Handle offline status changes
window.addEventListener('online', () => {
document.body.classList.remove('offline');
// Trigger sync when back online
if (wasmPort) {
wasmPort.postMessage({ type: 'SYNC_REQUEST' });
}
});
window.addEventListener('offline', () => {
document.body.classList.add('offline');
});
// Custom event handlers for HTMX
document.addEventListener('htmx:beforeRequest', (event) => {
const { elt, xhr } = event.detail;
// Add request tracking
const requestId = xhr.headers['X-Wasm-Request-ID'];
elt.setAttribute('data-request-id', requestId);
});
document.addEventListener('htmx:afterRequest', (event) => {
const { elt, successful } = event.detail;
if (successful) {
elt.removeAttribute('data-request-id');
}
});
// Initialize everything when the page loads
document.addEventListener('DOMContentLoaded', init);
// Export functions that might be needed by WASM
window.wasmBridge = {
triggerUIUpdate: function (selector, content) {
const target = document.querySelector(selector);
if (target) {
htmx.process(htmx.parse(content).forEach(node => target.appendChild(node)));
}
},
showNotification: function (message, type = 'info') {
// Implement notification system
console.log(`${type}: ${message}`);
}
};
-258
View File
@@ -1,258 +0,0 @@
// Cache names for different types of resources
const CACHE_NAMES = {
wasm: 'wasm-cache-v1',
static: 'static-cache-v1',
dynamic: 'dynamic-cache-v1'
};
// Import required scripts
importScripts(
"https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js",
"https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v1.1.0/sw.js",
);
// Initialize WASM HTTP listener
const wasmInstance = registerWasmHTTPListener("https://cdn.sonr.id/wasm/app.wasm");
// MessageChannel port for WASM communication
let wasmPort;
// Request queue for offline operations
let requestQueue = new Map();
// Setup message channel handler
self.addEventListener('message', async (event) => {
if (event.data.type === 'PORT_INITIALIZATION') {
wasmPort = event.data.port;
setupWasmCommunication();
}
});
function setupWasmCommunication() {
wasmPort.onmessage = async (event) => {
const { type, data } = event.data;
switch (type) {
case 'WASM_REQUEST':
handleWasmRequest(data);
break;
case 'SYNC_REQUEST':
processSyncQueue();
break;
}
};
// Notify that WASM is ready
wasmPort.postMessage({ type: 'WASM_READY' });
}
// Enhanced install event
self.addEventListener("install", (event) => {
event.waitUntil(
Promise.all([
skipWaiting(),
// Cache WASM binary and essential resources
caches.open(CACHE_NAMES.wasm).then(cache =>
cache.addAll([
'https://cdn.sonr.id/wasm/app.wasm',
'https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js'
])
)
])
);
});
// Enhanced activate event
self.addEventListener("activate", (event) => {
event.waitUntil(
Promise.all([
clients.claim(),
// Clean up old caches
caches.keys().then(keys =>
Promise.all(
keys.map(key => {
if (!Object.values(CACHE_NAMES).includes(key)) {
return caches.delete(key);
}
})
)
)
])
);
});
// Intercept fetch events
self.addEventListener('fetch', (event) => {
const request = event.request;
// Handle API requests differently from static resources
if (request.url.includes('/api/')) {
event.respondWith(handleApiRequest(request));
} else {
event.respondWith(handleStaticRequest(request));
}
});
async function handleApiRequest(request) {
try {
// Try to make the request
const response = await fetch(request.clone());
// If successful, pass through WASM handler
if (response.ok) {
return await processWasmResponse(request, response);
}
// If offline or failed, queue the request
await queueRequest(request);
// Return cached response if available
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline response
return new Response(
JSON.stringify({ error: 'Currently offline' }),
{
status: 503,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
await queueRequest(request);
return new Response(
JSON.stringify({ error: 'Request failed' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
}
async function handleStaticRequest(request) {
// Check cache first
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(request);
// Cache successful responses
if (response.ok) {
const cache = await caches.open(CACHE_NAMES.static);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return caches.match('/offline.html');
}
throw error;
}
}
async function processWasmResponse(request, response) {
// Clone the response before processing
const responseClone = response.clone();
try {
// Process through WASM
const processedResponse = await wasmInstance.processResponse(responseClone);
// Notify client through message channel
if (wasmPort) {
wasmPort.postMessage({
type: 'RESPONSE',
requestId: request.headers.get('X-Wasm-Request-ID'),
response: processedResponse
});
}
return processedResponse;
} catch (error) {
console.error('WASM processing error:', error);
return response;
}
}
async function queueRequest(request) {
const serializedRequest = await serializeRequest(request);
requestQueue.set(request.url, serializedRequest);
// Register for background sync
try {
await self.registration.sync.register('wasm-sync');
} catch (error) {
console.error('Sync registration failed:', error);
}
}
async function serializeRequest(request) {
const headers = {};
for (const [key, value] of request.headers.entries()) {
headers[key] = value;
}
return {
url: request.url,
method: request.method,
headers,
body: await request.text(),
timestamp: Date.now()
};
}
// Handle background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'wasm-sync') {
event.waitUntil(processSyncQueue());
}
});
async function processSyncQueue() {
const requests = Array.from(requestQueue.values());
for (const serializedRequest of requests) {
try {
const response = await fetch(new Request(serializedRequest.url, {
method: serializedRequest.method,
headers: serializedRequest.headers,
body: serializedRequest.body
}));
if (response.ok) {
requestQueue.delete(serializedRequest.url);
// Notify client of successful sync
if (wasmPort) {
wasmPort.postMessage({
type: 'SYNC_COMPLETE',
url: serializedRequest.url
});
}
}
} catch (error) {
console.error('Sync failed for request:', error);
}
}
}
// Handle payment requests
self.addEventListener("canmakepayment", function (e) {
e.respondWith(Promise.resolve(true));
});
// Handle periodic sync if available
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'wasm-sync') {
event.waitUntil(processSyncQueue());
}
});
-47
View File
@@ -1,47 +0,0 @@
package embed
import (
_ "embed"
"reflect"
"strings"
)
//go:embed index.html
var IndexHTML []byte
//go:embed main.js
var MainJS []byte
//go:embed sw.js
var WorkerJS []byte
func getSchema(structType interface{}) string {
t := reflect.TypeOf(structType)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return ""
}
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldName := toCamelCase(field.Name)
fields = append(fields, fieldName)
}
// Add "++" at the beginning, separated by a comma
return "++, " + strings.Join(fields, ", ")
}
func toCamelCase(s string) string {
if s == "" {
return s
}
if len(s) == 1 {
return strings.ToLower(s)
}
return strings.ToLower(s[:1]) + s[1:]
}
-50
View File
@@ -1,50 +0,0 @@
package types
// ╭───────────────────────────────────────────────────────────╮
// │ Create Passkey (/register/passkey) │
// ╰───────────────────────────────────────────────────────────╯
// DefaultCreatePasskeyParams returns a default CreatePasskeyParams
func DefaultCreatePasskeyParams() CreatePasskeyParams {
return CreatePasskeyParams{
Address: "",
Handle: "",
Name: "",
Challenge: "",
CreationBlock: "",
}
}
// CreatePasskeyParams represents the parameters for creating a passkey
type CreatePasskeyParams struct {
Address string
Handle string
Name string
Challenge string
CreationBlock string
}
// ╭───────────────────────────────────────────────────────────╮
// │ Create Profile (/register/profile) │
// ╰───────────────────────────────────────────────────────────╯
// DefaultCreateProfileParams returns a default CreateProfileParams
func DefaultCreateProfileParams() CreateProfileParams {
return CreateProfileParams{
TurnstileSiteKey: "",
FirstNumber: 0,
LastNumber: 0,
}
}
// CreateProfileParams represents the parameters for creating a profile
type CreateProfileParams struct {
TurnstileSiteKey string
FirstNumber int
LastNumber int
}
// Sum returns the sum of the first and last number
func (d CreateProfileParams) Sum() int {
return d.FirstNumber + d.LastNumber
}
-43
View File
@@ -1,43 +0,0 @@
package types
import "github.com/onsonr/sonr/internal/database/repository"
// Define the credential structure matching our frontend data
type CredentialDescriptor struct {
ID string `json:"id"`
RawID string `json:"rawId"`
Type string `json:"type"`
AuthenticatorAttachment string `json:"authenticatorAttachment"`
Transports string `json:"transports"`
ClientExtensionResults map[string]string `json:"clientExtensionResults"`
Response struct {
AttestationObject string `json:"attestationObject"`
ClientDataJSON string `json:"clientDataJSON"`
} `json:"response"`
}
func (c *CredentialDescriptor) ToModel(handle, origin string) *repository.Credential {
return &repository.Credential{
Handle: handle,
Origin: origin,
CredentialID: c.ID,
Type: c.Type,
Transports: c.Transports,
AuthenticatorAttachment: c.AuthenticatorAttachment,
}
}
func CredentialArrayToDescriptors(credentials []repository.Credential) []*CredentialDescriptor {
var descriptors []*CredentialDescriptor
for _, cred := range credentials {
cd := &CredentialDescriptor{
ID: cred.CredentialID,
RawID: cred.CredentialID,
Type: cred.Type,
AuthenticatorAttachment: cred.AuthenticatorAttachment,
Transports: cred.Transports,
}
descriptors = append(descriptors, cd)
}
return descriptors
}
+5 -6
View File
@@ -1,7 +1,6 @@
package views
import (
"github.com/onsonr/sonr/pkg/gateway/types"
"github.com/onsonr/sonr/internal/nebula/card"
"github.com/onsonr/sonr/internal/nebula/form"
"github.com/onsonr/sonr/internal/nebula/hero"
@@ -9,7 +8,7 @@ import (
"github.com/onsonr/sonr/internal/nebula/layout"
)
templ RegisterProfileView(data types.CreateProfileParams) {
templ RegisterProfileView(firstNumber int, lastNumber int) {
@layout.View("New Profile | Sonr.ID") {
@layout.Container() {
@hero.TitleDesc("Basic Info", "Tell us a little about yourself.")
@@ -23,7 +22,7 @@ templ RegisterProfileView(data types.CreateProfileParams) {
}
@input.Handle()
@input.Name()
@input.HumanSlider(data.FirstNumber, data.LastNumber)
@input.HumanSlider(firstNumber, lastNumber)
@form.Footer() {
@form.CancelButton()
@form.SubmitButton("Next")
@@ -34,7 +33,7 @@ templ RegisterProfileView(data types.CreateProfileParams) {
}
}
templ RegisterPasskeyView(data types.CreatePasskeyParams) {
templ RegisterPasskeyView(address string, handle string, name string, challenge string, creationBlock string) {
@layout.View("Register | Sonr.ID") {
@layout.Container() {
@hero.TitleDesc("Link a PassKey", "This will be used to login to your vault.")
@@ -42,11 +41,11 @@ templ RegisterPasskeyView(data types.CreatePasskeyParams) {
<input type="hidden" name="credential" id="credential-data" required/>
@form.Body() {
@form.Header() {
@card.SonrProfile(data.Address, data.Name, data.Handle, data.CreationBlock)
@card.SonrProfile(address, name, handle, creationBlock)
}
@input.CoinSelect()
@form.Footer() {
@input.Passkey(data.Address, data.Handle, data.Challenge)
@input.Passkey(address, handle, challenge)
@form.CancelButton()
}
}
+5 -6
View File
@@ -14,10 +14,9 @@ import (
"github.com/onsonr/sonr/internal/nebula/hero"
"github.com/onsonr/sonr/internal/nebula/input"
"github.com/onsonr/sonr/internal/nebula/layout"
"github.com/onsonr/sonr/pkg/gateway/types"
)
func RegisterProfileView(data types.CreateProfileParams) templ.Component {
func RegisterProfileView(firstNumber int, lastNumber int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -136,7 +135,7 @@ func RegisterProfileView(data types.CreateProfileParams) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = input.HumanSlider(data.FirstNumber, data.LastNumber).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = input.HumanSlider(firstNumber, lastNumber).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -202,7 +201,7 @@ func RegisterProfileView(data types.CreateProfileParams) templ.Component {
})
}
func RegisterPasskeyView(data types.CreatePasskeyParams) templ.Component {
func RegisterPasskeyView(address string, handle string, name string, challenge string, creationBlock string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -295,7 +294,7 @@ func RegisterPasskeyView(data types.CreatePasskeyParams) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = card.SonrProfile(data.Address, data.Name, data.Handle, data.CreationBlock).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = card.SonrProfile(address, name, handle, creationBlock).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -329,7 +328,7 @@ func RegisterPasskeyView(data types.CreatePasskeyParams) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = input.Passkey(data.Address, data.Handle, data.Challenge).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = input.Passkey(address, handle, challenge).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}