Compare commits
19 Commits
user-onboa
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5933aa59c1 | ||
|
|
da96bec465 | ||
|
|
483422c0e9 | ||
|
|
3df93cc147 | ||
|
|
d686edfa6c | ||
|
|
497b0ade1f | ||
|
|
86acbcd0d6 | ||
|
|
9111c85445 | ||
|
|
1d87879787 | ||
|
|
7c777f2bdf | ||
|
|
aa8067b8d0 | ||
|
|
bd66cdeb78 | ||
|
|
8d29e50669 | ||
|
|
5a7398d242 | ||
|
|
74b984fbe6 | ||
|
|
f8e00204b4 | ||
|
|
76543df765 | ||
|
|
15d4b2a5f7 | ||
|
|
2e67f91fe0 |
@@ -1,5 +1,33 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react-native@0.9.14
|
||||
- jazz-react-native-auth-clerk@0.9.14
|
||||
- jazz-react-native-media-images@0.9.14
|
||||
|
||||
## 1.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.9.13
|
||||
- jazz-react-native-auth-clerk@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-react-native-media-images@0.9.13
|
||||
|
||||
## 1.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.9.12
|
||||
- jazz-react-native-auth-clerk@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-react-native-media-images@0.9.12
|
||||
|
||||
## 1.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.52",
|
||||
"version": "1.0.55",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { useClerk, useUser } from "@clerk/clerk-expo";
|
||||
import { JazzProvider, setupKvStore } from "jazz-react-native";
|
||||
import { useJazzClerkAuth } from "jazz-react-native-auth-clerk";
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
const AuthContext = createContext<{
|
||||
isAuthenticated: boolean;
|
||||
@@ -27,15 +21,7 @@ export function JazzAndAuth({ children }: PropsWithChildren) {
|
||||
const { isSignedIn, isLoaded: isClerkLoaded } = useUser();
|
||||
const clerk = useClerk();
|
||||
const [auth, state] = useJazzClerkAuth(clerk, kvStore);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSignedIn && isClerkLoaded && auth) {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
}, [isSignedIn, isClerkLoaded, auth]);
|
||||
const isAuthenticated = Boolean(isSignedIn && isClerkLoaded && auth);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react-native@0.9.14
|
||||
|
||||
## 1.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 1.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 1.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.49",
|
||||
"version": "1.0.52",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser@0.9.14
|
||||
- jazz-vue@0.9.14
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-vue@0.9.13
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-vue@0.9.12
|
||||
|
||||
## 0.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.36",
|
||||
"version": "0.0.39",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.135
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser-media-images@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.134
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-browser-media-images@0.9.13
|
||||
|
||||
## 0.0.133
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-browser-media-images@0.9.12
|
||||
|
||||
## 0.0.132
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.132",
|
||||
"version": "0.0.135",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
- jazz-react-auth-clerk@0.9.14
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-react-auth-clerk@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-react-auth-clerk@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.31",
|
||||
"version": "0.0.34",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.4.1",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-clerk": "workspace:0.9.11",
|
||||
"jazz-react-auth-clerk": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-svelte@0.9.14
|
||||
|
||||
## 0.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# form
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser-media-images@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-browser-media-images@0.9.13
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-browser-media-images@0.9.12
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.0.27",
|
||||
"version": "0.0.30",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser-media-images@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-browser-media-images@0.9.13
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-browser-media-images@0.9.12
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.32",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson-transport-ws@0.9.13
|
||||
- cojson@0.9.13
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson-transport-ws@0.9.12
|
||||
- cojson@0.9.12
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.98",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.9.11",
|
||||
"cojson-transport-ws": "workspace:0.9.11",
|
||||
"cojson": "workspace:0.9.13",
|
||||
"cojson-transport-ws": "workspace:0.9.13",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-inspector@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.9.13
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.9.12
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.52",
|
||||
"version": "0.0.55",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,16 +13,13 @@
|
||||
"test:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.9.11",
|
||||
"jazz-tools": "workspace:0.9.11",
|
||||
"jazz-react": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:0.9.14",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PlayerControls } from "./components/PlayerControls";
|
||||
import "./index.css";
|
||||
|
||||
import { MusicaAccount } from "@/1_schema";
|
||||
import { JazzProvider } from "jazz-react";
|
||||
import { DemoAuthBasicUI, JazzProvider, useDemoAuth } from "jazz-react";
|
||||
import { useUploadExampleData } from "./lib/useUploadExampleData";
|
||||
|
||||
/**
|
||||
@@ -54,11 +54,30 @@ function Main() {
|
||||
);
|
||||
}
|
||||
|
||||
const peer =
|
||||
(new URL(window.location.href).searchParams.get(
|
||||
"peer",
|
||||
) as `ws://${string}`) ??
|
||||
"wss://cloud.jazz.tools/?key=music-player-example-jazz@garden.co";
|
||||
function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||
const [auth, state] = useDemoAuth();
|
||||
|
||||
const peer =
|
||||
(new URL(window.location.href).searchParams.get(
|
||||
"peer",
|
||||
) as `ws://${string}`) ??
|
||||
"wss://cloud.jazz.tools/?key=music-player-example-jazz@garden.co";
|
||||
|
||||
return (
|
||||
<>
|
||||
<JazzProvider
|
||||
storage="indexedDB"
|
||||
auth={auth}
|
||||
peer={peer}
|
||||
AccountSchema={MusicaAccount}
|
||||
>
|
||||
{children}
|
||||
<JazzInspector />
|
||||
</JazzProvider>
|
||||
<DemoAuthBasicUI appName="Jazz Music Player" state={state} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
@@ -68,14 +87,8 @@ declare module "jazz-react" {
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<JazzProvider
|
||||
peer={peer}
|
||||
storage="indexedDB"
|
||||
localOnly="anonymous" // This makes the app work in local mode when the user is anonymous
|
||||
AccountSchema={MusicaAccount}
|
||||
>
|
||||
<JazzAndAuth>
|
||||
<Main />
|
||||
<JazzInspector />
|
||||
</JazzProvider>
|
||||
</JazzAndAuth>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
createInviteLink,
|
||||
useAccount,
|
||||
useCoState,
|
||||
useIsAnonymousUser,
|
||||
} from "jazz-react";
|
||||
import { createInviteLink, useAccount, useCoState } from "jazz-react";
|
||||
import { ID } from "jazz-tools";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { Playlist } from "./1_schema";
|
||||
import { createNewPlaylist, uploadMusicTracks } from "./4_actions";
|
||||
import { MediaPlayer } from "./5_useMediaPlayer";
|
||||
import { AuthButton } from "./components/AuthButton";
|
||||
import { FileUploadButton } from "./components/FileUploadButton";
|
||||
import { LogoutButton } from "./components/LogoutButton";
|
||||
import { MusicTrackRow } from "./components/MusicTrackRow";
|
||||
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
|
||||
import { SidePanel } from "./components/SidePanel";
|
||||
@@ -71,8 +66,6 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
});
|
||||
};
|
||||
|
||||
const isAnonymousUser = useIsAnonymousUser();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen text-gray-800 bg-blue-50">
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
@@ -93,12 +86,12 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
<Button onClick={handleCreatePlaylist}>New playlist</Button>
|
||||
</>
|
||||
)}
|
||||
{!isRootPlaylist && !isAnonymousUser && (
|
||||
{!isRootPlaylist && (
|
||||
<Button onClick={handlePlaylistShareClick}>
|
||||
Share playlist
|
||||
</Button>
|
||||
)}
|
||||
<AuthButton />
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="flex flex-col">
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAccount, useIsAnonymousUser } from "jazz-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AuthModal } from "./AuthModal";
|
||||
|
||||
export function AuthButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { logOut } = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isAnonymousUser = useIsAnonymousUser();
|
||||
|
||||
function handleSignOut() {
|
||||
logOut();
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
if (!isAnonymousUser) {
|
||||
return (
|
||||
<Button variant="outline" onClick={handleSignOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
className="bg-white text-black hover:bg-gray-100"
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
<AuthModal open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useAccount, usePasskeyAuth } from "jazz-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AuthModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [isSignUp, setIsSignUp] = useState(true);
|
||||
|
||||
const { me } = useAccount();
|
||||
|
||||
const [, authState] = usePasskeyAuth({
|
||||
appName: "Jazz Music Player",
|
||||
onAnonymousUserUpgrade: ({ username, isSignUp }) => {
|
||||
if (isSignUp) {
|
||||
me.profile!.name = username;
|
||||
}
|
||||
|
||||
onOpenChange(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (authState.state === "ready") {
|
||||
if (isSignUp) {
|
||||
authState.signUp(username);
|
||||
} else {
|
||||
authState.logIn();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
{isSignUp ? "Create account" : "Welcome back"}
|
||||
</DialogTitle>
|
||||
{authState.state === "ready" && (
|
||||
<DialogDescription>
|
||||
{isSignUp
|
||||
? "Sign up to enable network sync and share your playlists with others"
|
||||
: "Changes done before logging in will be lost"}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{isSignUp && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{authState.errors.length > 0 && (
|
||||
<div className="text-sm text-red-500">
|
||||
{authState.errors.map((error, i) => (
|
||||
<p key={i}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
disabled={authState.state === "loading"}
|
||||
>
|
||||
{authState.state === "loading" ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : isSignUp ? (
|
||||
"Sign up with passkey"
|
||||
) : (
|
||||
"Login with passkey"
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-center text-sm">
|
||||
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSignUp(!isSignUp)}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{isSignUp ? "Login" : "Sign up"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useIsAnonymousUser } from "jazz-react";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
export function LocalOnlyTag() {
|
||||
const isAnonymousUser = useIsAnonymousUser();
|
||||
|
||||
if (!isAnonymousUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex items-center gap-1.5 cursor-help">
|
||||
<Badge variant="default" className="h-5 text-xs font-normal">
|
||||
Local only
|
||||
</Badge>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>
|
||||
Sign up to enable network sync and share your playlists with others
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { LocalOnlyTag } from "./LocalOnlyTag";
|
||||
|
||||
export function SidePanel() {
|
||||
const { playlistId } = useParams();
|
||||
@@ -26,7 +25,7 @@ export function SidePanel() {
|
||||
|
||||
return (
|
||||
<aside className="w-64 p-6 bg-white overflow-y-auto">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="flex items-center mb-6">
|
||||
<svg
|
||||
className="w-8 h-8 mr-2"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -47,9 +46,6 @@ export function SidePanel() {
|
||||
</svg>
|
||||
<span className="text-xl font-bold text-blue-600">Music Player</span>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<LocalOnlyTag />
|
||||
</div>
|
||||
<nav>
|
||||
<h2 className="mb-2 text-sm font-semibold text-gray-600">Playlists</h2>
|
||||
<ul className="space-y-1">
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,120 +0,0 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -1,81 +1,44 @@
|
||||
import { BrowserContext, test } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
import { HomePage } from "./pages/HomePage";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function mockAuthenticator(context: BrowserContext) {
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(window.navigator, "credentials", {
|
||||
value: {
|
||||
...window.navigator.credentials,
|
||||
create: async () => ({
|
||||
type: "public-key",
|
||||
id: new Uint8Array([1, 2, 3, 4]),
|
||||
rawId: new Uint8Array([1, 2, 3, 4]),
|
||||
response: {
|
||||
clientDataJSON: new Uint8Array([1]),
|
||||
attestationObject: new Uint8Array([2]),
|
||||
},
|
||||
}),
|
||||
get: async () => ({
|
||||
type: "public-key",
|
||||
id: new Uint8Array([1, 2, 3, 4]),
|
||||
rawId: new Uint8Array([1, 2, 3, 4]),
|
||||
response: {
|
||||
authenticatorData: new Uint8Array([1]),
|
||||
clientDataJSON: new Uint8Array([2]),
|
||||
signature: new Uint8Array([3]),
|
||||
},
|
||||
}),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
test("create a new playlist and share", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
// Configure the authenticator
|
||||
test.beforeEach(async ({ context }) => {
|
||||
// Enable virtual authenticator environment
|
||||
await mockAuthenticator(context);
|
||||
});
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("S. Mario");
|
||||
await loginPage.signup();
|
||||
|
||||
test("create a new playlist and share", async ({
|
||||
page: marioPage,
|
||||
browser,
|
||||
}) => {
|
||||
await marioPage.goto("/");
|
||||
|
||||
const marioHome = new HomePage(marioPage);
|
||||
const homePage = new HomePage(page);
|
||||
|
||||
// The example song should be loaded
|
||||
await marioHome.expectMusicTrack("Example song");
|
||||
await marioHome.editTrackTitle("Example song", "Super Mario World");
|
||||
await homePage.expectMusicTrack("Example song");
|
||||
await homePage.editTrackTitle("Example song", "Super Mario World");
|
||||
|
||||
await marioHome.createPlaylist();
|
||||
await marioHome.editPlaylistTitle("Save the princess");
|
||||
await homePage.createPlaylist();
|
||||
await homePage.editPlaylistTitle("Save the princess");
|
||||
|
||||
await marioHome.navigateToPlaylist("All tracks");
|
||||
await marioHome.addTrackToPlaylist("Super Mario World", "Save the princess");
|
||||
await homePage.navigateToPlaylist("All tracks");
|
||||
await homePage.addTrackToPlaylist("Super Mario World", "Save the princess");
|
||||
|
||||
await marioHome.navigateToPlaylist("Save the princess");
|
||||
await marioHome.expectMusicTrack("Super Mario World");
|
||||
await homePage.navigateToPlaylist("Save the princess");
|
||||
await homePage.expectMusicTrack("Super Mario World");
|
||||
|
||||
await marioHome.signUp("Mario");
|
||||
|
||||
const url = await marioHome.getShareLink();
|
||||
const url = await homePage.getShareLink();
|
||||
|
||||
await sleep(4000); // Wait for the sync to complete
|
||||
|
||||
const luigiPage = await (await browser.newContext()).newPage();
|
||||
await luigiPage.goto("/");
|
||||
await homePage.logout();
|
||||
|
||||
const luigiHome = new HomePage(luigiPage);
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("Luigi");
|
||||
await loginPage.signup();
|
||||
|
||||
await luigiHome.signUp("Luigi");
|
||||
await page.goto(url);
|
||||
|
||||
await luigiPage.goto(url);
|
||||
|
||||
await luigiHome.expectMusicTrack("Super Mario World");
|
||||
await luigiHome.playMusicTrack("Super Mario World");
|
||||
await luigiHome.expectActiveTrackPlaying();
|
||||
await homePage.expectMusicTrack("Super Mario World");
|
||||
await homePage.playMusicTrack("Super Mario World");
|
||||
await homePage.expectActiveTrackPlaying();
|
||||
});
|
||||
|
||||
@@ -95,14 +95,6 @@ export class HomePage {
|
||||
.click();
|
||||
}
|
||||
|
||||
async signUp(name: string) {
|
||||
await this.page.getByRole("button", { name: "Sign up" }).click();
|
||||
await this.page.getByRole("textbox", { name: "Username" }).fill(name);
|
||||
await this.page
|
||||
.getByRole("button", { name: "Sign up with passkey" })
|
||||
.click();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.logoutButton.click();
|
||||
}
|
||||
|
||||
40
examples/music-player/tests/pages/LoginPage.ts
Normal file
40
examples/music-player/tests/pages/LoginPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly usernameInput: Locator;
|
||||
readonly signupButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.usernameInput = page.getByRole("textbox");
|
||||
this.signupButton = page.getByRole("button", {
|
||||
name: "Sign up",
|
||||
});
|
||||
}
|
||||
|
||||
async goto() {
|
||||
this.page.goto("/");
|
||||
}
|
||||
|
||||
async fillUsername(value: string) {
|
||||
await this.usernameInput.clear();
|
||||
await this.usernameInput.fill(value);
|
||||
}
|
||||
|
||||
async loginAs(value: string) {
|
||||
await this.page
|
||||
.getByRole("button", {
|
||||
name: value,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async signup() {
|
||||
await this.signupButton.click();
|
||||
}
|
||||
|
||||
async expectLoaded() {
|
||||
await expect(this.signupButton).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-onboarding
|
||||
|
||||
## 0.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser-media-images@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-browser-media-images@0.9.13
|
||||
|
||||
## 0.0.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-browser-media-images@0.9.12
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-onboarding",
|
||||
"private": true,
|
||||
"version": "0.0.33",
|
||||
"version": "0.0.36",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# organization
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.28",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.9.14
|
||||
|
||||
## 0.0.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.9.13
|
||||
|
||||
## 0.0.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.9.12
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.23",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.33",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.51",
|
||||
"version": "0.0.54",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.9.11",
|
||||
"jazz-tools": "workspace:0.9.11",
|
||||
"jazz-react": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:0.9.14",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.152
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser-media-images@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.151
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-browser-media-images@0.9.13
|
||||
|
||||
## 0.0.150
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-browser-media-images@0.9.12
|
||||
|
||||
## 0.0.149
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.149",
|
||||
"version": "0.0.152",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,9 +19,9 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.9.11",
|
||||
"jazz-react": "workspace:0.9.11",
|
||||
"jazz-tools": "workspace:0.9.11",
|
||||
"jazz-browser-media-images": "workspace:0.9.14",
|
||||
"jazz-react": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:0.9.14",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.9.11",
|
||||
"jazz-run": "workspace:0.9.14",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser-media-images@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-browser-media-images@0.9.13
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-browser-media-images@0.9.12
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.32",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser@0.9.14
|
||||
- jazz-vue@0.9.14
|
||||
|
||||
## 0.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
- jazz-vue@0.9.13
|
||||
|
||||
## 0.0.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
- jazz-vue@0.9.12
|
||||
|
||||
## 0.0.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.34",
|
||||
"version": "0.0.37",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.151
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.150
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.149
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.148
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.148",
|
||||
"version": "0.0.151",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.9.11",
|
||||
"jazz-tools": "workspace:0.9.11",
|
||||
"jazz-react": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:0.9.14",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-react@0.9.14
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.26",
|
||||
"version": "0.0.29",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson-storage@0.9.13
|
||||
- cojson@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson-storage@0.9.12
|
||||
- cojson@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.13",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.8.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson-storage@0.9.13
|
||||
- cojson@0.9.13
|
||||
|
||||
## 0.8.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson-storage@0.9.12
|
||||
- cojson@0.9.12
|
||||
|
||||
## 0.8.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-storage-rn-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.8.54",
|
||||
"version": "0.8.56",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d29e50: Restore the logger wrapper and adapt the API to pino
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson-storage@0.9.13
|
||||
- cojson@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15d4b2a: Revert the custom logger
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson-storage@0.9.12
|
||||
- cojson@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.13",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cojson": "workspace:0.9.11",
|
||||
"cojson": "workspace:0.9.13",
|
||||
"cojson-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -28,6 +28,10 @@ export type RawTransactionRow = {
|
||||
tx: string;
|
||||
};
|
||||
|
||||
export function getErrorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : "Unknown error";
|
||||
}
|
||||
|
||||
export class SQLiteClient implements DBClientInterface {
|
||||
private readonly db: DatabaseT;
|
||||
private readonly toLocalNode: OutgoingSyncQueue;
|
||||
@@ -53,7 +57,10 @@ export class SQLiteClient implements DBClientInterface {
|
||||
header: parsedHeader,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.warn(coValueId, "Invalid JSON in header", e, coValueRow?.header);
|
||||
const headerValue = coValueRow?.header ?? "";
|
||||
logger.warn("Invalid JSON in header: " + headerValue, {
|
||||
id: coValueId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -80,7 +87,7 @@ export class SQLiteClient implements DBClientInterface {
|
||||
tx: JSON.parse(transactionRow.tx) as Transaction,
|
||||
}));
|
||||
} catch (e) {
|
||||
logger.warn("Invalid JSON in transaction", e);
|
||||
logger.warn("Invalid JSON in transaction");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +49,14 @@ export class SQLiteNode {
|
||||
? v.slice(0, 20) + "..."
|
||||
: v,
|
||||
)}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
processMessages().catch((e) =>
|
||||
logger.error("Error in processMessages in sqlite", e),
|
||||
);
|
||||
};
|
||||
|
||||
processMessages().catch((e) =>
|
||||
logger.error("Error in processMessages in sqlite", e),
|
||||
);
|
||||
}
|
||||
|
||||
static async asPeer({
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d29e50: Restore the logger wrapper and adapt the API to pino
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15d4b2a: Revert the custom logger
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.13",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d29e50: Restore the logger wrapper and adapt the API to pino
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15d4b2a: Revert the custom logger
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.13",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.9.11",
|
||||
"cojson": "workspace:0.9.13",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
logger,
|
||||
} from "cojson";
|
||||
import { BatchedOutgoingMessages } from "./BatchedOutgoingMessages.js";
|
||||
import { deserializeMessages } from "./serialization.js";
|
||||
import { deserializeMessages, getErrorMessage } from "./serialization.js";
|
||||
import { AnyWebSocket } from "./types.js";
|
||||
|
||||
export const BUFFER_LIMIT = 100_000;
|
||||
@@ -164,9 +164,7 @@ export function createWebSocketPeer({
|
||||
|
||||
if (!result.ok) {
|
||||
logger.warn(
|
||||
"Error while deserializing messages",
|
||||
event.data,
|
||||
result.error,
|
||||
"Error while deserializing messages: " + getErrorMessage(result.error),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { SyncMessage, logger } from "cojson";
|
||||
import { PingMsg } from "./types.js";
|
||||
|
||||
export function getErrorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : "Unknown error";
|
||||
}
|
||||
|
||||
export function addMessageToBacklog(backlog: string, message: SyncMessage) {
|
||||
if (!backlog) {
|
||||
return JSON.stringify(message);
|
||||
@@ -24,7 +28,7 @@ export function deserializeMessages(messages: unknown) {
|
||||
| PingMsg[],
|
||||
} as const;
|
||||
} catch (e) {
|
||||
logger.error("Error while deserializing messages", e);
|
||||
logger.error("Error while deserializing messages: " + getErrorMessage(e));
|
||||
return {
|
||||
ok: false,
|
||||
error: e,
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# cojson
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d29e50: Restore the logger wrapper and adapt the API to pino
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15d4b2a: Revert the custom logger
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.13",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^1.29.0",
|
||||
"@types/jest": "^29.5.3",
|
||||
|
||||
@@ -138,7 +138,10 @@ export class PeerState {
|
||||
}
|
||||
|
||||
gracefulShutdown() {
|
||||
logger.debug("Gracefully closing", this.id);
|
||||
logger.debug("Gracefully closing", {
|
||||
peerId: this.id,
|
||||
peerRole: this.role,
|
||||
});
|
||||
this.closeQueue();
|
||||
this.peer.outgoing.close();
|
||||
this.closed = true;
|
||||
|
||||
@@ -304,7 +304,10 @@ async function loadCoValueFromPeers(
|
||||
if (coValueEntry.state.type === "loading") {
|
||||
const timeout = setTimeout(() => {
|
||||
if (coValueEntry.state.type === "loading") {
|
||||
logger.warn("Failed to load coValue from peer", peer.id);
|
||||
logger.warn("Failed to load coValue from peer", {
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
});
|
||||
coValueEntry.dispatch({
|
||||
type: "not-found-in-peer",
|
||||
peerId: peer.id,
|
||||
|
||||
@@ -60,7 +60,7 @@ export class RawAccount<
|
||||
);
|
||||
|
||||
if (agents.length !== 1) {
|
||||
logger.warn("Account has " + agents.length + " agents", this.id);
|
||||
logger.warn("Account has " + agents.length + " agents", { id: this.id });
|
||||
}
|
||||
|
||||
this._cachedCurrentAgentID = agents[0];
|
||||
|
||||
@@ -322,7 +322,7 @@ export class RawGroup<
|
||||
const secret = this.core.getReadKey(keyID);
|
||||
|
||||
if (!secret) {
|
||||
logger.error("Can't find key", keyID);
|
||||
logger.error("Can't find key " + keyID);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,9 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
||||
try {
|
||||
return JSON.parse(textDecoder.decode(plaintext));
|
||||
} catch (e) {
|
||||
logger.error("Failed to decrypt/parse sealed message", e);
|
||||
logger.error(
|
||||
"Failed to decrypt/parse sealed message: " + (e as Error)?.message,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,8 +241,9 @@ export class WasmCrypto extends CryptoProvider<Uint8Array> {
|
||||
try {
|
||||
return JSON.parse(textDecoder.decode(plaintext));
|
||||
} catch (e) {
|
||||
logger.error("Failed to decrypt/parse sealed message", e);
|
||||
return undefined;
|
||||
logger.error(
|
||||
"Failed to decrypt/parse sealed message: " + (e as Error)?.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export abstract class CryptoProvider<Blake3State = any> {
|
||||
try {
|
||||
return parseJSON(this.decryptRaw(encrypted, keySecret, nOnceMaterial));
|
||||
} catch (e) {
|
||||
logger.error("Decryption error", e);
|
||||
logger.error("Decryption error: " + (e as Error)?.message);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ export class LocalNode {
|
||||
|
||||
return node;
|
||||
} catch (e) {
|
||||
logger.error("Error withLoadedAccount", e);
|
||||
logger.error("Error withLoadedAccount: " + (e as Error)?.message);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,9 @@ export class LocalNode {
|
||||
this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
|
||||
|
||||
await entry.loadFromPeers(peers).catch((e) => {
|
||||
logger.error("Error loading from peers", id, e);
|
||||
logger.error("Error loading from peers: " + (e as Error)?.message, {
|
||||
id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -324,7 +326,9 @@ export class LocalNode {
|
||||
unsubscribe = coValue.subscribe(callback);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error subscribing to ", id, e);
|
||||
logger.error(
|
||||
"Error subscribing to " + id + ": " + (e as Error)?.message,
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
@@ -7,25 +9,25 @@ export enum LogLevel {
|
||||
}
|
||||
|
||||
export interface LogSystem {
|
||||
debug(message: string, ...args: any[]): void;
|
||||
info(message: string, ...args: any[]): void;
|
||||
warn(message: string, ...args: any[]): void;
|
||||
error(message: string, ...args: any[]): void;
|
||||
debug(message: string, attributes?: Record<string, JsonValue>): void;
|
||||
info(message: string, attributes?: Record<string, JsonValue>): void;
|
||||
warn(message: string, attributes?: Record<string, JsonValue>): void;
|
||||
error(message: string, attributes?: Record<string, JsonValue>): void;
|
||||
}
|
||||
|
||||
// Default console-based logging system
|
||||
export class ConsoleLogSystem implements LogSystem {
|
||||
debug(message: string, ...args: any[]) {
|
||||
console.debug(message, ...args);
|
||||
debug(message: string, attributes?: Record<string, JsonValue>) {
|
||||
console.debug(message, attributes);
|
||||
}
|
||||
info(message: string, ...args: any[]) {
|
||||
console.info(message, ...args);
|
||||
info(message: string, attributes?: Record<string, JsonValue>) {
|
||||
console.info(message, attributes);
|
||||
}
|
||||
warn(message: string, ...args: any[]) {
|
||||
console.warn(message, ...args);
|
||||
warn(message: string, attributes?: Record<string, JsonValue>) {
|
||||
console.warn(message, attributes);
|
||||
}
|
||||
error(message: string, ...args: any[]) {
|
||||
console.error(message, ...args);
|
||||
error(message: string, attributes?: Record<string, JsonValue>) {
|
||||
console.error(message, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,27 +51,27 @@ export class Logger {
|
||||
this.logSystem = logSystem;
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]) {
|
||||
debug(message: string, attributes?: Record<string, JsonValue>) {
|
||||
if (this.level <= LogLevel.DEBUG) {
|
||||
this.logSystem.debug(message, ...args);
|
||||
this.logSystem.debug(message, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]) {
|
||||
info(message: string, attributes?: Record<string, JsonValue>) {
|
||||
if (this.level <= LogLevel.INFO) {
|
||||
this.logSystem.info(message, ...args);
|
||||
this.logSystem.info(message, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string, ...args: any[]) {
|
||||
warn(message: string, attributes?: Record<string, JsonValue>) {
|
||||
if (this.level <= LogLevel.WARN) {
|
||||
this.logSystem.warn(message, ...args);
|
||||
this.logSystem.warn(message, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, ...args: any[]) {
|
||||
error(message: string, attributes?: Record<string, JsonValue>) {
|
||||
if (this.level <= LogLevel.ERROR) {
|
||||
this.logSystem.error(message, ...args);
|
||||
this.logSystem.error(message, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,12 +42,15 @@ export function disablePermissionErrors() {
|
||||
logPermissionErrors = false;
|
||||
}
|
||||
|
||||
function logPermissionError(...args: unknown[]) {
|
||||
function logPermissionError(
|
||||
message: string,
|
||||
attributes?: Record<string, JsonValue>,
|
||||
) {
|
||||
if (logPermissionErrors === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn("Permission error", ...args);
|
||||
logger.warn("Permission error: " + message, attributes);
|
||||
}
|
||||
|
||||
export function determineValidTransactions(
|
||||
@@ -227,17 +230,10 @@ function determineValidTransactionsForGroup(
|
||||
try {
|
||||
changes = parseJSON(tx.changes);
|
||||
} catch (e) {
|
||||
logPermissionError(
|
||||
coValue.id,
|
||||
"Invalid JSON in transaction",
|
||||
e,
|
||||
logPermissionError("Invalid JSON in transaction", {
|
||||
id: coValue.id,
|
||||
tx,
|
||||
JSON.stringify(tx.changes, (k, v) =>
|
||||
k === "changes" || k === "encryptedChanges"
|
||||
? v.slice(0, 20) + "..."
|
||||
: v,
|
||||
),
|
||||
);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,9 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
k === "changes" || k === "encryptedChanges"
|
||||
? v.slice(0, 20) + "..."
|
||||
: v,
|
||||
)}`,
|
||||
e,
|
||||
)}
|
||||
Error: ${e instanceof Error ? e.message : "Unknown error"},
|
||||
`,
|
||||
);
|
||||
}
|
||||
nMsg++;
|
||||
@@ -241,7 +242,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
} else {
|
||||
const merged = mergeChunks(coValue, newContentAsChunk);
|
||||
if (merged === "nonContigous") {
|
||||
logger.warn(
|
||||
console.warn(
|
||||
"Non-contigous new content for " + newContent.id,
|
||||
Object.entries(coValue.sessionEntries).map(([session, entries]) =>
|
||||
entries.map((entry) => ({
|
||||
@@ -316,7 +317,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
const merged = mergeChunks(result, nextChunk);
|
||||
|
||||
if (merged === "nonContigous") {
|
||||
logger.warn(
|
||||
console.warn(
|
||||
"Non-contigous chunks while loading " + id,
|
||||
result,
|
||||
nextChunk,
|
||||
@@ -375,7 +376,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
if (existingChunk) {
|
||||
const merged = mergeChunks(existingChunk, chunk);
|
||||
if (merged === "nonContigous") {
|
||||
logger.info(
|
||||
console.log(
|
||||
"Non-contigous chunks in " + chunk.id + ", " + fileName,
|
||||
existingChunk,
|
||||
chunk,
|
||||
@@ -449,7 +450,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
if (existingChunk) {
|
||||
const merged = mergeChunks(existingChunk, chunk);
|
||||
if (merged === "nonContigous") {
|
||||
logger.info(
|
||||
console.log(
|
||||
"Non-contigous chunks in " + entry.id + ", " + blockFile,
|
||||
existingChunk,
|
||||
chunk,
|
||||
|
||||
@@ -23,6 +23,10 @@ export function emptyKnownState(id: RawCoID): CoValueKnownState {
|
||||
};
|
||||
}
|
||||
|
||||
function getErrorMessage(e: unknown) {
|
||||
return e instanceof Error ? e.message : "Unknown error";
|
||||
}
|
||||
|
||||
export type SyncMessage =
|
||||
| LoadMessage
|
||||
| KnownStateMessage
|
||||
@@ -183,7 +187,7 @@ export class SyncManager {
|
||||
|
||||
if (entry.state.type !== "available") {
|
||||
entry.loadFromPeers([peer]).catch((e: unknown) => {
|
||||
logger.error("Error sending load", e);
|
||||
logger.error("Error sending load: " + getErrorMessage(e));
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -200,7 +204,7 @@ export class SyncManager {
|
||||
action: "load",
|
||||
...coValue.knownState(),
|
||||
}).catch((e: unknown) => {
|
||||
logger.error("Error sending load", e);
|
||||
logger.error("Error sending load: " + getErrorMessage(e));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -230,7 +234,7 @@ export class SyncManager {
|
||||
asDependencyOf,
|
||||
...coValue.knownState(),
|
||||
}).catch((e: unknown) => {
|
||||
logger.error("Error sending known state", e);
|
||||
logger.error("Error sending known state: " + getErrorMessage(e));
|
||||
});
|
||||
|
||||
peer.toldKnownState.add(id);
|
||||
@@ -258,7 +262,7 @@ export class SyncManager {
|
||||
let lastYield = performance.now();
|
||||
for (const [_i, piece] of newContentPieces.entries()) {
|
||||
this.trySendToPeer(peer, piece).catch((e: unknown) => {
|
||||
logger.error("Error sending content piece", e);
|
||||
logger.error("Error sending content piece: " + getErrorMessage(e));
|
||||
});
|
||||
|
||||
if (performance.now() - lastYield > 10) {
|
||||
@@ -331,7 +335,10 @@ export class SyncManager {
|
||||
return;
|
||||
}
|
||||
if (msg === "PingTimeout") {
|
||||
logger.error("Ping timeout from peer", peer.id);
|
||||
logger.error("Ping timeout from peer", {
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -354,13 +361,22 @@ export class SyncManager {
|
||||
processMessages()
|
||||
.then(() => {
|
||||
if (peer.crashOnClose) {
|
||||
logger.warn("Unexepcted close from peer", peer.id);
|
||||
logger.error("Unexepcted close from peer", {
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
});
|
||||
this.local.crashed = new Error("Unexpected close from peer");
|
||||
throw new Error("Unexpected close from peer");
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error processing messages from peer", peer.id, e);
|
||||
logger.error(
|
||||
"Error processing messages from peer: " + getErrorMessage(e),
|
||||
{
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
},
|
||||
);
|
||||
if (peer.crashOnClose) {
|
||||
this.local.crashed = e;
|
||||
throw new Error(e);
|
||||
@@ -596,17 +612,11 @@ export class SyncManager {
|
||||
// );
|
||||
|
||||
if (result.isErr()) {
|
||||
logger.error(
|
||||
"Failed to add transactions from",
|
||||
peer.id,
|
||||
result.error,
|
||||
msg.id,
|
||||
newTransactions.length + " new transactions",
|
||||
"after: " + newContentForSession.after,
|
||||
"our last known tx idx initially: " + ourKnownTxIdx,
|
||||
"our last known tx idx now: " +
|
||||
coValue.sessionLogs.get(sessionID)?.transactions.length,
|
||||
);
|
||||
logger.error("Failed to add transactions: " + result.error.type, {
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
id: msg.id,
|
||||
});
|
||||
peer.erroredCoValues.set(msg.id, result.error);
|
||||
continue;
|
||||
}
|
||||
@@ -627,7 +637,13 @@ export class SyncManager {
|
||||
isCorrection: true,
|
||||
...coValue.knownState(),
|
||||
}).catch((e) => {
|
||||
logger.error("Error sending known state correction", e);
|
||||
logger.error(
|
||||
"Error sending known state correction: " + getErrorMessage(e),
|
||||
{
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
@@ -641,7 +657,10 @@ export class SyncManager {
|
||||
action: "known",
|
||||
...coValue.knownState(),
|
||||
}).catch((e: unknown) => {
|
||||
logger.error("Error sending known state", e);
|
||||
logger.error("Error sending known state: " + getErrorMessage(e), {
|
||||
peerId: peer.id,
|
||||
peerRole: peer.role,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,14 @@ describe("Logger", () => {
|
||||
|
||||
expect(mockLogSystem.debug).not.toHaveBeenCalled();
|
||||
expect(mockLogSystem.info).not.toHaveBeenCalled();
|
||||
expect(mockLogSystem.warn).toHaveBeenCalledWith("Warning message");
|
||||
expect(mockLogSystem.error).toHaveBeenCalledWith("Error message");
|
||||
expect(mockLogSystem.warn).toHaveBeenCalledWith(
|
||||
"Warning message",
|
||||
undefined,
|
||||
);
|
||||
expect(mockLogSystem.error).toHaveBeenCalledWith(
|
||||
"Error message",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("should pass additional arguments to log system", () => {
|
||||
@@ -33,14 +39,12 @@ describe("Logger", () => {
|
||||
};
|
||||
|
||||
const logger = new Logger(LogLevel.DEBUG, mockLogSystem);
|
||||
const additionalArgs = [{ foo: "bar" }, 42, "extra"];
|
||||
|
||||
logger.debug("Debug message", ...additionalArgs);
|
||||
logger.debug("Debug message", { foo: "bar" });
|
||||
|
||||
expect(mockLogSystem.debug).toHaveBeenCalledWith(
|
||||
"Debug message",
|
||||
...additionalArgs,
|
||||
);
|
||||
expect(mockLogSystem.debug).toHaveBeenCalledWith("Debug message", {
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +64,7 @@ describe("Logger", () => {
|
||||
|
||||
logger.setLevel(LogLevel.WARN);
|
||||
logger.warn("Warning 2"); // Should log
|
||||
expect(mockLogSystem.warn).toHaveBeenCalledWith("Warning 2");
|
||||
expect(mockLogSystem.warn).toHaveBeenCalledWith("Warning 2", undefined);
|
||||
});
|
||||
|
||||
test("should allow changing log system at runtime", () => {
|
||||
@@ -81,12 +85,12 @@ describe("Logger", () => {
|
||||
const logger = new Logger(LogLevel.INFO, mockLogSystem1);
|
||||
|
||||
logger.info("Message 1");
|
||||
expect(mockLogSystem1.info).toHaveBeenCalledWith("Message 1");
|
||||
expect(mockLogSystem1.info).toHaveBeenCalledWith("Message 1", undefined);
|
||||
expect(mockLogSystem2.info).not.toHaveBeenCalled();
|
||||
|
||||
logger.setLogSystem(mockLogSystem2);
|
||||
logger.info("Message 2");
|
||||
expect(mockLogSystem2.info).toHaveBeenCalledWith("Message 2");
|
||||
expect(mockLogSystem2.info).toHaveBeenCalledWith("Message 2", undefined);
|
||||
expect(mockLogSystem1.info).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -103,17 +107,17 @@ describe("Logger", () => {
|
||||
const logger = new Logger();
|
||||
logger.setLevel(LogLevel.DEBUG);
|
||||
const testMessage = "Test message";
|
||||
const testArgs = [{ data: "test" }, 123];
|
||||
const testArgs = { data: "test" };
|
||||
|
||||
logger.debug(testMessage, ...testArgs);
|
||||
logger.info(testMessage, ...testArgs);
|
||||
logger.warn(testMessage, ...testArgs);
|
||||
logger.error(testMessage, ...testArgs);
|
||||
logger.debug(testMessage, testArgs);
|
||||
logger.info(testMessage, testArgs);
|
||||
logger.warn(testMessage, testArgs);
|
||||
logger.error(testMessage, testArgs);
|
||||
|
||||
expect(consoleSpy.debug).toHaveBeenCalledWith(testMessage, ...testArgs);
|
||||
expect(consoleSpy.info).toHaveBeenCalledWith(testMessage, ...testArgs);
|
||||
expect(consoleSpy.warn).toHaveBeenCalledWith(testMessage, ...testArgs);
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(testMessage, ...testArgs);
|
||||
expect(consoleSpy.debug).toHaveBeenCalledWith(testMessage, testArgs);
|
||||
expect(consoleSpy.info).toHaveBeenCalledWith(testMessage, testArgs);
|
||||
expect(consoleSpy.warn).toHaveBeenCalledWith(testMessage, testArgs);
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(testMessage, testArgs);
|
||||
|
||||
// Cleanup
|
||||
Object.values(consoleSpy).forEach((spy) => spy.mockRestore());
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.9.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser@0.9.14
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson@0.9.13
|
||||
- jazz-browser@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson@0.9.12
|
||||
- jazz-browser@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-clerk",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.14",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.9.11",
|
||||
"jazz-browser": "workspace:0.9.11",
|
||||
"jazz-tools": "workspace:0.9.11"
|
||||
"cojson": "workspace:0.9.13",
|
||||
"jazz-browser": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:0.9.14"
|
||||
},
|
||||
"scripts": {
|
||||
"format-and-lint": "biome check .",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AgentSecret, CryptoProvider } from "cojson";
|
||||
import { AuthSecretStorage } from "jazz-browser";
|
||||
import { AgentSecret } from "cojson";
|
||||
import { Account, AuthMethod, AuthResult, Credentials, ID } from "jazz-tools";
|
||||
|
||||
const localStorageKey = "jazz-clerk-auth";
|
||||
|
||||
export type MinimalClerkClient = {
|
||||
user:
|
||||
| {
|
||||
@@ -20,11 +21,15 @@ export type MinimalClerkClient = {
|
||||
signOut: () => Promise<void>;
|
||||
};
|
||||
|
||||
type ClerkCredentials = {
|
||||
jazzAccountID: ID<Account>;
|
||||
jazzAccountSecret: AgentSecret;
|
||||
jazzAccountSeed?: number[];
|
||||
};
|
||||
function saveCredentialsToLocalStorage(credentials: Credentials) {
|
||||
localStorage.setItem(
|
||||
localStorageKey,
|
||||
JSON.stringify({
|
||||
accountID: credentials.accountID,
|
||||
secret: credentials.secret,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export class BrowserClerkAuth implements AuthMethod {
|
||||
constructor(
|
||||
@@ -32,28 +37,23 @@ export class BrowserClerkAuth implements AuthMethod {
|
||||
private readonly clerkClient: MinimalClerkClient,
|
||||
) {}
|
||||
|
||||
async start(crypto: CryptoProvider): Promise<AuthResult> {
|
||||
AuthSecretStorage.migrate();
|
||||
|
||||
async start(): Promise<AuthResult> {
|
||||
// Check local storage for credentials
|
||||
const credentials = AuthSecretStorage.get();
|
||||
const isAnonymous = AuthSecretStorage.isAnonymous();
|
||||
const locallyStoredCredentials = localStorage.getItem(localStorageKey);
|
||||
|
||||
if (credentials && !isAnonymous) {
|
||||
if (locallyStoredCredentials) {
|
||||
try {
|
||||
const credentials = JSON.parse(locallyStoredCredentials) as Credentials;
|
||||
return {
|
||||
type: "existing",
|
||||
credentials: {
|
||||
accountID: credentials.accountID,
|
||||
secret: credentials.accountSecret,
|
||||
},
|
||||
credentials,
|
||||
saveCredentials: async () => {}, // No need to save credentials when recovering from local storage
|
||||
onSuccess: () => {},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
localStorage.removeItem(localStorageKey);
|
||||
void this.clerkClient.signOut();
|
||||
},
|
||||
};
|
||||
@@ -63,65 +63,22 @@ export class BrowserClerkAuth implements AuthMethod {
|
||||
}
|
||||
|
||||
if (this.clerkClient.user) {
|
||||
const username =
|
||||
this.clerkClient.user.fullName ||
|
||||
this.clerkClient.user.username ||
|
||||
this.clerkClient.user.id;
|
||||
// Check clerk user metadata for credentials
|
||||
const clerkCredentials = this.clerkClient.user
|
||||
.unsafeMetadata as ClerkCredentials;
|
||||
if (clerkCredentials.jazzAccountID) {
|
||||
if (!clerkCredentials.jazzAccountSecret) {
|
||||
this.driver.onError("No secret for existing user");
|
||||
const storedCredentials = this.clerkClient.user.unsafeMetadata;
|
||||
if (storedCredentials.jazzAccountID) {
|
||||
if (!storedCredentials.jazzAccountSecret) {
|
||||
throw new Error("No secret for existing user");
|
||||
}
|
||||
return {
|
||||
type: "existing",
|
||||
credentials: {
|
||||
accountID: clerkCredentials.jazzAccountID as ID<Account>,
|
||||
secret: clerkCredentials.jazzAccountSecret as AgentSecret,
|
||||
accountID: storedCredentials.jazzAccountID as ID<Account>,
|
||||
secret: storedCredentials.jazzAccountSecret as AgentSecret,
|
||||
},
|
||||
saveCredentials: async ({ accountID, secret }: Credentials) => {
|
||||
AuthSecretStorage.set({
|
||||
saveCredentialsToLocalStorage({
|
||||
accountID,
|
||||
accountSecret: secret,
|
||||
secretSeed: clerkCredentials.jazzAccountSeed
|
||||
? Uint8Array.from(clerkCredentials.jazzAccountSeed)
|
||||
: undefined,
|
||||
provider: "clerk",
|
||||
});
|
||||
},
|
||||
onSuccess: () => {},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
void this.clerkClient.signOut();
|
||||
},
|
||||
};
|
||||
} else if (credentials && isAnonymous) {
|
||||
return {
|
||||
type: "existing",
|
||||
username,
|
||||
credentials: {
|
||||
accountID: credentials.accountID,
|
||||
secret: credentials.accountSecret,
|
||||
},
|
||||
saveCredentials: async ({ accountID, secret }: Credentials) => {
|
||||
AuthSecretStorage.set({
|
||||
accountID,
|
||||
accountSecret: secret,
|
||||
secretSeed: credentials.secretSeed,
|
||||
provider: "clerk",
|
||||
});
|
||||
await this.clerkClient.user?.update({
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: accountID,
|
||||
jazzAccountSecret: secret,
|
||||
jazzAccountSeed: credentials.secretSeed
|
||||
? Array.from(credentials.secretSeed)
|
||||
: undefined,
|
||||
} satisfies ClerkCredentials,
|
||||
secret,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {},
|
||||
@@ -133,27 +90,25 @@ export class BrowserClerkAuth implements AuthMethod {
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const secretSeed = crypto.newRandomSecretSeed();
|
||||
// No credentials found, so we need to create new credentials
|
||||
return {
|
||||
type: "new",
|
||||
creationProps: {
|
||||
name: username,
|
||||
name:
|
||||
this.clerkClient.user.fullName ||
|
||||
this.clerkClient.user.username ||
|
||||
this.clerkClient.user.id,
|
||||
},
|
||||
initialSecret: crypto.agentSecretFromSecretSeed(secretSeed),
|
||||
saveCredentials: async ({ accountID, secret }: Credentials) => {
|
||||
AuthSecretStorage.set({
|
||||
saveCredentialsToLocalStorage({
|
||||
accountID,
|
||||
secretSeed,
|
||||
accountSecret: secret,
|
||||
provider: "clerk",
|
||||
secret,
|
||||
});
|
||||
await this.clerkClient.user?.update({
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: accountID,
|
||||
jazzAccountSecret: secret,
|
||||
jazzAccountSeed: Array.from(secretSeed),
|
||||
} satisfies ClerkCredentials,
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess: () => {},
|
||||
|
||||
@@ -1,212 +1,119 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { AgentSecret } from "cojson";
|
||||
import { AuthSecretStorage } from "jazz-browser";
|
||||
import { Account } from "jazz-tools";
|
||||
import { ID } from "jazz-tools";
|
||||
import { Account, ID } from "jazz-tools";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { BrowserClerkAuth } from "../index";
|
||||
|
||||
beforeEach(() => {
|
||||
AuthSecretStorage.clear();
|
||||
});
|
||||
import { BrowserClerkAuth, MinimalClerkClient } from "../index.js";
|
||||
|
||||
describe("BrowserClerkAuth", () => {
|
||||
function createDriver() {
|
||||
return {
|
||||
onError: vi.fn(),
|
||||
} satisfies BrowserClerkAuth.Driver;
|
||||
}
|
||||
let mockLocalStorage: { [key: string]: string };
|
||||
let mockClerkClient: MinimalClerkClient;
|
||||
let mockDriver: BrowserClerkAuth.Driver;
|
||||
|
||||
function createMockClerkClient(user?: any) {
|
||||
return {
|
||||
user,
|
||||
beforeEach(() => {
|
||||
// Mock localStorage
|
||||
mockLocalStorage = {};
|
||||
global.localStorage = {
|
||||
getItem: vi.fn((key: string) => mockLocalStorage[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
mockLocalStorage[key] = value;
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete mockLocalStorage[key];
|
||||
}),
|
||||
clear: vi.fn(),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock Clerk client
|
||||
mockClerkClient = {
|
||||
user: {
|
||||
unsafeMetadata: {},
|
||||
fullName: "Test User",
|
||||
username: "testuser",
|
||||
id: "test-id",
|
||||
update: vi.fn(),
|
||||
},
|
||||
signOut: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should handle existing non-anonymous user from storage", async () => {
|
||||
const driver = createDriver();
|
||||
const mockClerkClient = createMockClerkClient();
|
||||
const auth = new BrowserClerkAuth(driver, mockClerkClient);
|
||||
// Mock driver
|
||||
mockDriver = {
|
||||
onError: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Set up existing user in storage
|
||||
AuthSecretStorage.set({
|
||||
accountID: "test123" as ID<Account>,
|
||||
secretSeed: new Uint8Array([1]),
|
||||
accountSecret: "fromAuthStorage" as AgentSecret,
|
||||
provider: "clerk",
|
||||
describe("clerk credentials in localStorage", () => {
|
||||
it("should get credentials from localStorage when clerk user is not signed in", async () => {
|
||||
mockLocalStorage["jazz-clerk-auth"] = JSON.stringify({
|
||||
accountID: "test-account-id",
|
||||
accountSecret: "test-secret",
|
||||
});
|
||||
|
||||
const mockCrypto = {
|
||||
newRandomSecretSeed: () => new Uint8Array([2]),
|
||||
agentSecretFromSecretSeed: () => "xxxxx" as AgentSecret,
|
||||
};
|
||||
|
||||
const result = await auth.start(mockCrypto as any);
|
||||
const auth = new BrowserClerkAuth(mockDriver, {
|
||||
...mockClerkClient,
|
||||
user: null,
|
||||
});
|
||||
|
||||
const result = await auth.start();
|
||||
expect(result.type).toBe("existing");
|
||||
|
||||
if (result.type !== "existing") {
|
||||
throw new Error("Expected existing user login");
|
||||
}
|
||||
|
||||
expect(result.credentials).toEqual({
|
||||
accountID: "test123",
|
||||
secret: "fromAuthStorage",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle existing clerk user with credentials", async () => {
|
||||
const driver = createDriver();
|
||||
const mockUser = {
|
||||
id: "clerk123",
|
||||
fullName: "Test User",
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: "test123",
|
||||
jazzAccountSecret: "clerkSecret",
|
||||
jazzAccountSeed: [1, 2, 3],
|
||||
},
|
||||
update: vi.fn(),
|
||||
};
|
||||
const mockClerkClient = createMockClerkClient(mockUser);
|
||||
const auth = new BrowserClerkAuth(driver, mockClerkClient);
|
||||
|
||||
const mockCrypto = {
|
||||
newRandomSecretSeed: () => new Uint8Array([2]),
|
||||
agentSecretFromSecretSeed: () => "xxxxx" as AgentSecret,
|
||||
};
|
||||
|
||||
const result = await auth.start(mockCrypto as any);
|
||||
|
||||
expect(result.type).toBe("existing");
|
||||
|
||||
if (result.type !== "existing") {
|
||||
throw new Error("Expected existing user login");
|
||||
}
|
||||
|
||||
expect(result.credentials).toEqual({
|
||||
accountID: "test123",
|
||||
secret: "clerkSecret",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle anonymous user upgrade", async () => {
|
||||
const driver = createDriver();
|
||||
const mockUser = {
|
||||
id: "clerk123",
|
||||
fullName: "Test User",
|
||||
unsafeMetadata: {},
|
||||
update: vi.fn(),
|
||||
};
|
||||
const mockClerkClient = createMockClerkClient(mockUser);
|
||||
const auth = new BrowserClerkAuth(driver, mockClerkClient);
|
||||
|
||||
// Set up anonymous user in storage
|
||||
AuthSecretStorage.set({
|
||||
accountID: "anon123" as ID<Account>,
|
||||
secretSeed: new Uint8Array([1]),
|
||||
accountSecret: "anonSecret" as AgentSecret,
|
||||
provider: "anonymous",
|
||||
});
|
||||
|
||||
const result = await auth.start({} as any);
|
||||
|
||||
expect(result.type).toBe("existing");
|
||||
|
||||
if (result.type !== "existing") {
|
||||
throw new Error("Expected existing user login");
|
||||
}
|
||||
|
||||
expect(result.username).toBe("Test User");
|
||||
expect(result.credentials).toEqual({
|
||||
accountID: "anon123",
|
||||
secret: "anonSecret",
|
||||
});
|
||||
|
||||
// Test saving credentials updates both storage and clerk metadata
|
||||
await result.saveCredentials?.({
|
||||
accountID: "anon123" as ID<Account>,
|
||||
secret: "newSecret" as AgentSecret,
|
||||
});
|
||||
|
||||
expect(mockUser.update).toHaveBeenCalledWith({
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: "anon123",
|
||||
jazzAccountSecret: "newSecret",
|
||||
jazzAccountSeed: expect.any(Array),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle new user creation", async () => {
|
||||
const driver = createDriver();
|
||||
const mockUser = {
|
||||
id: "clerk123",
|
||||
fullName: "Test User",
|
||||
unsafeMetadata: {},
|
||||
update: vi.fn(),
|
||||
};
|
||||
const mockClerkClient = createMockClerkClient(mockUser);
|
||||
const auth = new BrowserClerkAuth(driver, mockClerkClient);
|
||||
|
||||
const mockCrypto = {
|
||||
newRandomSecretSeed: () => new Uint8Array([1, 2, 3]),
|
||||
agentSecretFromSecretSeed: () => "newSecret" as AgentSecret,
|
||||
};
|
||||
|
||||
const result = await auth.start(mockCrypto as any);
|
||||
|
||||
describe("clerk credentials not in localStorage", () => {
|
||||
it("should return new credentials when clerk user signs up", async () => {
|
||||
const auth = new BrowserClerkAuth(mockDriver, mockClerkClient);
|
||||
const result = await auth.start();
|
||||
expect(result.type).toBe("new");
|
||||
});
|
||||
|
||||
if (result.type !== "new") {
|
||||
throw new Error("Expected new user login");
|
||||
}
|
||||
|
||||
expect(result.creationProps).toEqual({
|
||||
name: "Test User",
|
||||
});
|
||||
expect(result.initialSecret).toBe("newSecret");
|
||||
|
||||
await result.saveCredentials({
|
||||
accountID: "new123" as ID<Account>,
|
||||
secret: "newSecret" as AgentSecret,
|
||||
});
|
||||
|
||||
expect(mockUser.update).toHaveBeenCalledWith({
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: "new123",
|
||||
jazzAccountSecret: "newSecret",
|
||||
jazzAccountSeed: [1, 2, 3],
|
||||
it("should return existing credentials when clerk user is signed in", async () => {
|
||||
mockClerkClient = {
|
||||
user: {
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: "test-account-id",
|
||||
jazzAccountSecret: "test-secret",
|
||||
},
|
||||
fullName: "Test User",
|
||||
username: "testuser",
|
||||
id: "test-id",
|
||||
update: vi.fn(),
|
||||
},
|
||||
});
|
||||
signOut: vi.fn(),
|
||||
};
|
||||
|
||||
const auth = new BrowserClerkAuth(mockDriver, mockClerkClient);
|
||||
const result = await auth.start();
|
||||
expect(result.type).toBe("existing");
|
||||
});
|
||||
|
||||
it("should throw error when not signed in", async () => {
|
||||
const driver = createDriver();
|
||||
const mockClerkClient = createMockClerkClient(undefined);
|
||||
const auth = new BrowserClerkAuth(driver, mockClerkClient);
|
||||
const auth = new BrowserClerkAuth(mockDriver, {
|
||||
...mockClerkClient,
|
||||
user: null,
|
||||
});
|
||||
|
||||
await expect(auth.start({} as any)).rejects.toThrow("Not signed in");
|
||||
await expect(auth.start()).rejects.toThrow("Not signed in");
|
||||
});
|
||||
|
||||
it("should throw error when clerk user has ID but no secret", async () => {
|
||||
const driver = createDriver();
|
||||
const mockUser = {
|
||||
id: "clerk123",
|
||||
fullName: "Test User",
|
||||
unsafeMetadata: {
|
||||
jazzAccountID: "test123",
|
||||
},
|
||||
update: vi.fn(),
|
||||
};
|
||||
const mockClerkClient = createMockClerkClient(mockUser);
|
||||
const auth = new BrowserClerkAuth(driver, mockClerkClient);
|
||||
it("should save credentials to localStorage", async () => {
|
||||
const auth = new BrowserClerkAuth(mockDriver, mockClerkClient);
|
||||
const result = await auth.start();
|
||||
if (result.saveCredentials) {
|
||||
await result.saveCredentials({
|
||||
accountID: "test-account-id" as ID<Account>,
|
||||
secret: "test-secret" as AgentSecret,
|
||||
});
|
||||
}
|
||||
|
||||
await expect(auth.start({} as any)).rejects.toThrow(
|
||||
"No secret for existing user",
|
||||
);
|
||||
expect(mockLocalStorage["jazz-clerk-auth"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("should call clerk signOut when logging out", async () => {
|
||||
const auth = new BrowserClerkAuth(mockDriver, mockClerkClient);
|
||||
const result = await auth.start();
|
||||
result.logOut();
|
||||
|
||||
expect(mockClerkClient.signOut).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.9.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
- jazz-browser@0.9.14
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.14",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
@@ -8,8 +8,8 @@
|
||||
"dependencies": {
|
||||
"@types/image-blob-reduce": "^4.1.1",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "workspace:0.9.11",
|
||||
"jazz-tools": "workspace:0.9.11",
|
||||
"jazz-browser": "workspace:0.9.14",
|
||||
"jazz-tools": "workspace:0.9.14",
|
||||
"pica": "^9.0.1",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
|
||||
@@ -44,7 +44,6 @@ vi.mock("image-blob-reduce", () => {
|
||||
|
||||
describe("createImage", () => {
|
||||
it("should create an image definition with correct dimensions", async () => {
|
||||
vi.useFakeTimers();
|
||||
// Create a test blob that simulates a 400x300 image
|
||||
const blob = new Blob(["fake-image-data"], { type: "image/jpeg" });
|
||||
Object.defineProperty(blob, "size", { value: 1024 * 50 }); // 50KB
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.9.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3df93cc]
|
||||
- jazz-tools@0.9.14
|
||||
|
||||
## 0.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8d29e50]
|
||||
- cojson-transport-ws@0.9.13
|
||||
- cojson@0.9.13
|
||||
- cojson-storage-indexeddb@0.9.13
|
||||
- jazz-tools@0.9.13
|
||||
|
||||
## 0.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [15d4b2a]
|
||||
- cojson-transport-ws@0.9.12
|
||||
- cojson@0.9.12
|
||||
- cojson-storage-indexeddb@0.9.12
|
||||
- jazz-tools@0.9.12
|
||||
|
||||
## 0.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.14",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^1.3.0",
|
||||
"cojson": "workspace:*",
|
||||
"cojson-storage-indexeddb": "workspace:*",
|
||||
"cojson-transport-ws": "workspace:*",
|
||||
"jazz-tools": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"cojson": "workspace:0.9.13",
|
||||
"cojson-storage-indexeddb": "workspace:0.9.13",
|
||||
"cojson-transport-ws": "workspace:0.9.13",
|
||||
"jazz-tools": "workspace:0.9.14",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { AgentSecret } from "cojson";
|
||||
import { Account, ID } from "jazz-tools";
|
||||
|
||||
const STORAGE_KEY = "jazz-logged-in-secret";
|
||||
|
||||
export type AuthCredentials = {
|
||||
accountID: ID<Account>;
|
||||
secretSeed?: Uint8Array;
|
||||
accountSecret: AgentSecret;
|
||||
provider?: "anonymous" | "clerk" | "demo" | "passkey" | "passphrase" | string;
|
||||
};
|
||||
|
||||
export type AuthSetPayload = {
|
||||
accountID: ID<Account>;
|
||||
secretSeed?: Uint8Array;
|
||||
accountSecret: AgentSecret;
|
||||
provider: "anonymous" | "clerk" | "demo" | "passkey" | "passphrase" | string;
|
||||
};
|
||||
|
||||
export const AuthSecretStorage = {
|
||||
migrate() {
|
||||
if (!localStorage[STORAGE_KEY]) {
|
||||
const demoAuthSecret = localStorage["demo-auth-logged-in-secret"];
|
||||
if (demoAuthSecret) {
|
||||
localStorage[STORAGE_KEY] = demoAuthSecret;
|
||||
delete localStorage["demo-auth-logged-in-secret"];
|
||||
}
|
||||
|
||||
const clerkAuthSecret = localStorage["jazz-clerk-auth"];
|
||||
if (clerkAuthSecret) {
|
||||
localStorage[STORAGE_KEY] = clerkAuthSecret;
|
||||
delete localStorage["jazz-clerk-auth"];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get(): AuthCredentials | null {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (!parsed.accountID || !parsed.accountSecret) {
|
||||
throw new Error("Invalid auth secret storage data");
|
||||
}
|
||||
|
||||
return {
|
||||
accountID: parsed.accountID,
|
||||
secretSeed: parsed.secretSeed
|
||||
? new Uint8Array(parsed.secretSeed)
|
||||
: undefined,
|
||||
accountSecret: parsed.accountSecret,
|
||||
provider: parsed.provider,
|
||||
};
|
||||
},
|
||||
|
||||
set(payload: AuthSetPayload) {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
accountID: payload.accountID,
|
||||
secretSeed: payload.secretSeed
|
||||
? Array.from(payload.secretSeed)
|
||||
: undefined,
|
||||
accountSecret: payload.accountSecret,
|
||||
provider: payload.provider,
|
||||
}),
|
||||
);
|
||||
this.emitUpdate();
|
||||
},
|
||||
|
||||
isAnonymous() {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (!data) return false;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
return parsed.provider === "anonymous";
|
||||
},
|
||||
|
||||
onUpdate(handler: () => void) {
|
||||
window.addEventListener("jazz-auth-update", handler);
|
||||
return () => window.removeEventListener("jazz-auth-update", handler);
|
||||
},
|
||||
|
||||
emitUpdate() {
|
||||
window.dispatchEvent(new Event("jazz-auth-update"));
|
||||
},
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
this.emitUpdate();
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import { AgentSecret, CryptoProvider } from "cojson";
|
||||
import { AgentSecret } from "cojson";
|
||||
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
|
||||
import { AuthSecretStorage } from "./AuthSecretStorage.js";
|
||||
|
||||
type StorageData = {
|
||||
accountID: ID<Account>;
|
||||
accountSecret: AgentSecret;
|
||||
};
|
||||
|
||||
const localStorageKey = "jazz-logged-in-secret";
|
||||
|
||||
/**
|
||||
* `BrowserDemoAuth` provides a `JazzAuth` object for demo authentication.
|
||||
*
|
||||
@@ -52,39 +53,44 @@ export class BrowserDemoAuth implements AuthMethod {
|
||||
/**
|
||||
* @returns A `JazzAuth` object
|
||||
*/
|
||||
async start(crypto: CryptoProvider) {
|
||||
AuthSecretStorage.migrate();
|
||||
async start() {
|
||||
// migrate old localStorage key to new one
|
||||
if (localStorage["demo-auth-logged-in-secret"]) {
|
||||
if (!localStorage[localStorageKey]) {
|
||||
localStorage[localStorageKey] =
|
||||
localStorage["demo-auth-logged-in-secret"];
|
||||
}
|
||||
delete localStorage["demo-auth-logged-in-secret"];
|
||||
}
|
||||
|
||||
const credentials = AuthSecretStorage.get();
|
||||
if (localStorage[localStorageKey]) {
|
||||
const localStorageData = JSON.parse(
|
||||
localStorage[localStorageKey],
|
||||
) as StorageData;
|
||||
|
||||
if (credentials) {
|
||||
const accountID = credentials.accountID;
|
||||
const secret = credentials.accountSecret;
|
||||
const accountID = localStorageData.accountID as ID<Account>;
|
||||
const secret = localStorageData.accountSecret;
|
||||
|
||||
return {
|
||||
type: "existing",
|
||||
credentials: { accountID, secret },
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut, isSignUp: false });
|
||||
this.driver.onSignedIn({ logOut });
|
||||
},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
} satisfies AuthResult;
|
||||
} else {
|
||||
return new Promise<AuthResult>((resolve) => {
|
||||
this.driver.onReady({
|
||||
signUp: async (username) => {
|
||||
const secretSeed = crypto.newRandomSecretSeed();
|
||||
const accountSecret = crypto.agentSecretFromSecretSeed(secretSeed);
|
||||
|
||||
resolve({
|
||||
type: "new",
|
||||
creationProps: { name: username },
|
||||
initialSecret: accountSecret,
|
||||
saveCredentials: async (credentials: {
|
||||
accountID: ID<Account>;
|
||||
secret: AgentSecret;
|
||||
@@ -94,13 +100,7 @@ export class BrowserDemoAuth implements AuthMethod {
|
||||
accountSecret: credentials.secret,
|
||||
} satisfies StorageData);
|
||||
|
||||
AuthSecretStorage.set({
|
||||
accountID: credentials.accountID,
|
||||
secretSeed,
|
||||
accountSecret,
|
||||
provider: "demo",
|
||||
});
|
||||
|
||||
localStorage[localStorageKey] = storageData;
|
||||
localStorage["demo-auth-existing-users-" + username] =
|
||||
storageData;
|
||||
|
||||
@@ -111,13 +111,13 @@ export class BrowserDemoAuth implements AuthMethod {
|
||||
: username;
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut, isSignUp: true });
|
||||
this.driver.onSignedIn({ logOut });
|
||||
},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -128,11 +128,7 @@ export class BrowserDemoAuth implements AuthMethod {
|
||||
localStorage["demo-auth-existing-users-" + existingUser],
|
||||
) as StorageData;
|
||||
|
||||
AuthSecretStorage.set({
|
||||
accountID: storageData.accountID,
|
||||
accountSecret: storageData.accountSecret,
|
||||
provider: "demo",
|
||||
});
|
||||
localStorage[localStorageKey] = JSON.stringify(storageData);
|
||||
|
||||
resolve({
|
||||
type: "existing",
|
||||
@@ -141,13 +137,13 @@ export class BrowserDemoAuth implements AuthMethod {
|
||||
secret: storageData.accountSecret,
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut, isSignUp: false });
|
||||
this.driver.onSignedIn({ logOut });
|
||||
},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -166,14 +162,11 @@ export namespace BrowserDemoAuth {
|
||||
existingUsers: string[];
|
||||
logInAs: (existingUser: string) => Promise<void>;
|
||||
}) => void;
|
||||
onSignedIn: (next: {
|
||||
logOut: () => void;
|
||||
isSignUp: boolean;
|
||||
}) => void;
|
||||
onSignedIn: (next: { logOut: () => void }) => void;
|
||||
onError: (error: string | Error) => void;
|
||||
}
|
||||
}
|
||||
|
||||
function logOut() {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
}
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
import { AgentSecret, CryptoProvider } from "cojson";
|
||||
import { AgentSecret } from "cojson";
|
||||
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
|
||||
import { AuthSecretStorage } from "./AuthSecretStorage.js";
|
||||
|
||||
type StorageData = {
|
||||
accountID: ID<Account>;
|
||||
accountSecret: AgentSecret;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "jazz-logged-in-secret";
|
||||
|
||||
/**
|
||||
* `BrowserAnonymousAuth` provides a `JazzAuth` object for demo authentication.
|
||||
* `BrowserOnboardingAuth` provides a `JazzAuth` object for demo authentication.
|
||||
*
|
||||
* Demo authentication is useful for quickly testing your app, as it allows you to create new accounts and log in as existing ones. The authentication persists across page reloads, with the credentials stored in `localStorage`.
|
||||
*
|
||||
* ```
|
||||
* import { BrowserAnonymousAuth } from "jazz-browser";
|
||||
* import { BrowserOnboardingAuth } from "jazz-browser";
|
||||
*
|
||||
* const auth = new BrowserAnonymousAuth(driver);
|
||||
* const auth = new BrowserOnboardingAuth(driver);
|
||||
* ```
|
||||
*
|
||||
* @category Auth Providers
|
||||
*/
|
||||
export class BrowserAnonymousAuth implements AuthMethod {
|
||||
export class BrowserOnboardingAuth implements AuthMethod {
|
||||
constructor(
|
||||
public defaultUserName: string,
|
||||
public driver: BrowserAnonymousAuth.Driver,
|
||||
public driver: BrowserOnboardingAuth.Driver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns A `JazzAuth` object
|
||||
*/
|
||||
async start(crypto: CryptoProvider) {
|
||||
AuthSecretStorage.migrate();
|
||||
|
||||
const existingUser = AuthSecretStorage.get();
|
||||
async start() {
|
||||
const existingUser = localStorage[STORAGE_KEY];
|
||||
|
||||
if (existingUser) {
|
||||
const accountID = existingUser.accountID;
|
||||
const secret = existingUser.accountSecret;
|
||||
const existingUserData = JSON.parse(existingUser) as StorageData;
|
||||
|
||||
const accountID = existingUserData.accountID as ID<Account>;
|
||||
const secret = existingUserData.accountSecret;
|
||||
|
||||
return {
|
||||
type: "existing",
|
||||
@@ -45,22 +51,19 @@ export class BrowserAnonymousAuth implements AuthMethod {
|
||||
logOut,
|
||||
} satisfies AuthResult;
|
||||
} else {
|
||||
const secretSeed = crypto.newRandomSecretSeed();
|
||||
|
||||
return {
|
||||
type: "new",
|
||||
creationProps: { name: this.defaultUserName, anonymous: true },
|
||||
initialSecret: crypto.agentSecretFromSecretSeed(secretSeed),
|
||||
saveCredentials: async (credentials: {
|
||||
accountID: ID<Account>;
|
||||
secret: AgentSecret;
|
||||
}) => {
|
||||
AuthSecretStorage.set({
|
||||
const storageData = JSON.stringify({
|
||||
accountID: credentials.accountID,
|
||||
secretSeed,
|
||||
accountSecret: credentials.secret,
|
||||
provider: "anonymous",
|
||||
});
|
||||
} satisfies StorageData);
|
||||
|
||||
localStorage[STORAGE_KEY] = storageData;
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
@@ -76,7 +79,7 @@ export class BrowserAnonymousAuth implements AuthMethod {
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace BrowserAnonymousAuth {
|
||||
export namespace BrowserOnboardingAuth {
|
||||
export interface Driver {
|
||||
onSignedIn: (next: { logOut: () => void }) => void;
|
||||
onError: (error: string | Error) => void;
|
||||
@@ -84,5 +87,5 @@ export namespace BrowserAnonymousAuth {
|
||||
}
|
||||
|
||||
function logOut() {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[STORAGE_KEY];
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
import { CryptoProvider, RawAccountID, cojsonInternals } from "cojson";
|
||||
import {
|
||||
AgentSecret,
|
||||
CryptoProvider,
|
||||
RawAccountID,
|
||||
cojsonInternals,
|
||||
} from "cojson";
|
||||
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
|
||||
import { AuthSecretStorage } from "./AuthSecretStorage.js";
|
||||
|
||||
type LocalStorageData = {
|
||||
accountID: ID<Account>;
|
||||
accountSecret: AgentSecret;
|
||||
};
|
||||
|
||||
const localStorageKey = "jazz-logged-in-secret";
|
||||
|
||||
/**
|
||||
* `BrowserPasskeyAuth` provides a `JazzAuth` object for passkey authentication.
|
||||
@@ -21,18 +32,25 @@ export class BrowserPasskeyAuth implements AuthMethod {
|
||||
public appHostname: string = window.location.hostname,
|
||||
) {}
|
||||
|
||||
accountLoaded() {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
}
|
||||
|
||||
onError(error: string | Error) {
|
||||
this.driver.onError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A `JazzAuth` object
|
||||
*/
|
||||
async start(crypto: CryptoProvider): Promise<AuthResult> {
|
||||
AuthSecretStorage.migrate();
|
||||
if (localStorage[localStorageKey]) {
|
||||
const localStorageData = JSON.parse(
|
||||
localStorage[localStorageKey],
|
||||
) as LocalStorageData;
|
||||
|
||||
const credentials = AuthSecretStorage.get();
|
||||
const isAnonymous = AuthSecretStorage.isAnonymous();
|
||||
|
||||
if (credentials && !isAnonymous) {
|
||||
const accountID = credentials.accountID;
|
||||
const secret = credentials.accountSecret;
|
||||
const accountID = localStorageData.accountID as ID<Account>;
|
||||
const secret = localStorageData.accountSecret;
|
||||
|
||||
return {
|
||||
type: "existing",
|
||||
@@ -44,96 +62,85 @@ export class BrowserPasskeyAuth implements AuthMethod {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
} satisfies AuthResult;
|
||||
} else {
|
||||
return new Promise<AuthResult>((resolve) => {
|
||||
this.driver.onReady({
|
||||
signUp: async (username) => {
|
||||
if (credentials && isAnonymous && credentials.secretSeed) {
|
||||
const secretSeed = credentials.secretSeed;
|
||||
const secretSeed = crypto.newRandomSecretSeed();
|
||||
|
||||
resolve({
|
||||
type: "existing",
|
||||
username,
|
||||
credentials: {
|
||||
accountID: credentials.accountID,
|
||||
secret: credentials.accountSecret,
|
||||
},
|
||||
saveCredentials: async ({ accountID, secret }) => {
|
||||
await this.createPasskeyCredentials({
|
||||
accountID,
|
||||
secretSeed,
|
||||
username,
|
||||
});
|
||||
resolve({
|
||||
type: "new",
|
||||
creationProps: { name: username },
|
||||
initialSecret: crypto.agentSecretFromSecretSeed(secretSeed),
|
||||
saveCredentials: async ({ accountID, secret }) => {
|
||||
const webAuthNCredentialPayload = new Uint8Array(
|
||||
cojsonInternals.secretSeedLength +
|
||||
cojsonInternals.shortHashLength,
|
||||
);
|
||||
|
||||
AuthSecretStorage.set({
|
||||
accountID,
|
||||
secretSeed,
|
||||
accountSecret: secret,
|
||||
provider: "passkey",
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
const secretSeed = crypto.newRandomSecretSeed();
|
||||
webAuthNCredentialPayload.set(secretSeed);
|
||||
webAuthNCredentialPayload.set(
|
||||
cojsonInternals.rawCoIDtoBytes(
|
||||
accountID as unknown as RawAccountID,
|
||||
),
|
||||
cojsonInternals.secretSeedLength,
|
||||
);
|
||||
|
||||
resolve({
|
||||
type: "new",
|
||||
creationProps: { name: username },
|
||||
initialSecret: crypto.agentSecretFromSecretSeed(secretSeed),
|
||||
saveCredentials: async ({ accountID, secret }) => {
|
||||
await this.createPasskeyCredentials({
|
||||
accountID,
|
||||
secretSeed,
|
||||
username,
|
||||
});
|
||||
await navigator.credentials.create({
|
||||
publicKey: {
|
||||
challenge: Uint8Array.from([0, 1, 2]),
|
||||
rp: {
|
||||
name: this.appName,
|
||||
id: this.appHostname,
|
||||
},
|
||||
user: {
|
||||
id: webAuthNCredentialPayload,
|
||||
name: username + ` (${new Date().toLocaleString()})`,
|
||||
displayName: username,
|
||||
},
|
||||
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "platform",
|
||||
requireResidentKey: true,
|
||||
residentKey: "required",
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: "direct",
|
||||
},
|
||||
});
|
||||
|
||||
AuthSecretStorage.set({
|
||||
accountID,
|
||||
secretSeed,
|
||||
accountSecret: secret,
|
||||
provider: "passkey",
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut,
|
||||
});
|
||||
}
|
||||
localStorage[localStorageKey] = JSON.stringify({
|
||||
accountID,
|
||||
accountSecret: secret,
|
||||
} satisfies LocalStorageData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
},
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
});
|
||||
},
|
||||
logIn: async () => {
|
||||
const webAuthNCredential = await this.getPasskeyCredentials().catch(
|
||||
() => {
|
||||
this.driver.onError(
|
||||
"Error while accessing the passkey credentials",
|
||||
);
|
||||
return "rejected" as const;
|
||||
const webAuthNCredential = (await navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: Uint8Array.from([0, 1, 2]),
|
||||
rpId: this.appHostname,
|
||||
allowCredentials: [],
|
||||
timeout: 60000,
|
||||
},
|
||||
);
|
||||
|
||||
if (webAuthNCredential === "rejected") {
|
||||
return;
|
||||
}
|
||||
|
||||
})) as unknown as {
|
||||
response: { userHandle: ArrayBuffer };
|
||||
};
|
||||
if (!webAuthNCredential) {
|
||||
this.driver.onError(
|
||||
"Error while accessing the passkey credentials",
|
||||
);
|
||||
return;
|
||||
throw new Error("Couldn't log in");
|
||||
}
|
||||
|
||||
const webAuthNCredentialPayload = new Uint8Array(
|
||||
@@ -158,12 +165,10 @@ export class BrowserPasskeyAuth implements AuthMethod {
|
||||
type: "existing",
|
||||
credentials: { accountID, secret },
|
||||
saveCredentials: async ({ accountID, secret }) => {
|
||||
AuthSecretStorage.set({
|
||||
localStorage[localStorageKey] = JSON.stringify({
|
||||
accountID,
|
||||
accountSecret: secret,
|
||||
secretSeed: accountSecretSeed,
|
||||
provider: "passkey",
|
||||
});
|
||||
} satisfies LocalStorageData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
@@ -171,79 +176,15 @@ export class BrowserPasskeyAuth implements AuthMethod {
|
||||
onError: (error: string | Error) => {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut,
|
||||
logOut: () => {
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async createPasskeyCredentials({
|
||||
accountID,
|
||||
secretSeed,
|
||||
username,
|
||||
}: {
|
||||
accountID: ID<Account>;
|
||||
secretSeed: Uint8Array;
|
||||
username: string;
|
||||
}) {
|
||||
const webAuthNCredentialPayload = new Uint8Array(
|
||||
cojsonInternals.secretSeedLength + cojsonInternals.shortHashLength,
|
||||
);
|
||||
|
||||
webAuthNCredentialPayload.set(secretSeed);
|
||||
webAuthNCredentialPayload.set(
|
||||
cojsonInternals.rawCoIDtoBytes(accountID as unknown as RawAccountID),
|
||||
cojsonInternals.secretSeedLength,
|
||||
);
|
||||
|
||||
try {
|
||||
await navigator.credentials.create({
|
||||
publicKey: {
|
||||
challenge: Uint8Array.from([0, 1, 2]),
|
||||
rp: {
|
||||
name: this.appName,
|
||||
id: this.appHostname,
|
||||
},
|
||||
user: {
|
||||
id: webAuthNCredentialPayload,
|
||||
name: username + ` (${new Date().toLocaleString()})`,
|
||||
displayName: username,
|
||||
},
|
||||
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "platform",
|
||||
requireResidentKey: true,
|
||||
residentKey: "required",
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: "direct",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
||||
throw new Error("Passkey creation not allowed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async getPasskeyCredentials() {
|
||||
const value = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: Uint8Array.from([0, 1, 2]),
|
||||
rpId: this.appHostname,
|
||||
allowCredentials: [],
|
||||
timeout: 60000,
|
||||
},
|
||||
});
|
||||
|
||||
return value as
|
||||
| (Credential & { response: { userHandle: ArrayBuffer } })
|
||||
| null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -260,5 +201,5 @@ export namespace BrowserPasskeyAuth {
|
||||
}
|
||||
|
||||
function logOut() {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import * as bip39 from "@scure/bip39";
|
||||
import { CryptoProvider, cojsonInternals } from "cojson";
|
||||
import { AgentSecret, CryptoProvider, cojsonInternals } from "cojson";
|
||||
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
|
||||
import { AuthSecretStorage } from "./AuthSecretStorage.js";
|
||||
|
||||
type LocalStorageData = {
|
||||
accountID: ID<Account>;
|
||||
accountSecret: AgentSecret;
|
||||
};
|
||||
|
||||
const localStorageKey = "jazz-logged-in-secret";
|
||||
|
||||
/**
|
||||
* `BrowserPassphraseAuth` provides a `JazzAuth` object for passphrase authentication.
|
||||
@@ -9,7 +15,7 @@ import { AuthSecretStorage } from "./AuthSecretStorage.js";
|
||||
* ```ts
|
||||
* import { BrowserPassphraseAuth } from "jazz-browser";
|
||||
*
|
||||
* const auth = new BrowserPassphraseAuth(driver, wordlist);
|
||||
* const auth = new BrowserPassphraseAuth(driver, wordlist, appName);
|
||||
* ```
|
||||
*
|
||||
* @category Auth Providers
|
||||
@@ -18,20 +24,22 @@ export class BrowserPassphraseAuth implements AuthMethod {
|
||||
constructor(
|
||||
public driver: BrowserPassphraseAuth.Driver,
|
||||
public wordlist: string[],
|
||||
public appName: string,
|
||||
// TODO: is this a safe default?
|
||||
public appHostname: string = window.location.hostname,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns A `JazzAuth` object
|
||||
*/
|
||||
async start(crypto: CryptoProvider): Promise<AuthResult> {
|
||||
AuthSecretStorage.migrate();
|
||||
if (localStorage[localStorageKey]) {
|
||||
const localStorageData = JSON.parse(
|
||||
localStorage[localStorageKey],
|
||||
) as LocalStorageData;
|
||||
|
||||
const credentials = AuthSecretStorage.get();
|
||||
const isAnonymous = AuthSecretStorage.isAnonymous();
|
||||
|
||||
if (credentials && !isAnonymous) {
|
||||
const accountID = credentials.accountID;
|
||||
const secret = credentials.accountSecret;
|
||||
const accountID = localStorageData.accountID as ID<Account>;
|
||||
const secret = localStorageData.accountSecret;
|
||||
|
||||
return {
|
||||
type: "existing",
|
||||
@@ -43,19 +51,13 @@ export class BrowserPassphraseAuth implements AuthMethod {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
} satisfies AuthResult;
|
||||
} else {
|
||||
return new Promise<AuthResult>((resolve) => {
|
||||
this.driver.onReady({
|
||||
signUp: async (username, passphrase) => {
|
||||
if (credentials && isAnonymous) {
|
||||
console.warn(
|
||||
"Anonymous user upgrade is currently not supported on passphrase auth",
|
||||
);
|
||||
}
|
||||
|
||||
const secretSeed = bip39.mnemonicToEntropy(
|
||||
passphrase,
|
||||
this.wordlist,
|
||||
@@ -71,12 +73,10 @@ export class BrowserPassphraseAuth implements AuthMethod {
|
||||
creationProps: { name: username },
|
||||
initialSecret: accountSecret,
|
||||
saveCredentials: async (credentials) => {
|
||||
AuthSecretStorage.set({
|
||||
localStorage[localStorageKey] = JSON.stringify({
|
||||
accountID: credentials.accountID,
|
||||
secretSeed,
|
||||
accountSecret,
|
||||
provider: "passphrase",
|
||||
});
|
||||
accountSecret: credentials.secret,
|
||||
} satisfies LocalStorageData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
@@ -85,7 +85,7 @@ export class BrowserPassphraseAuth implements AuthMethod {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -112,13 +112,11 @@ export class BrowserPassphraseAuth implements AuthMethod {
|
||||
resolve({
|
||||
type: "existing",
|
||||
credentials: { accountID, secret: accountSecret },
|
||||
saveCredentials: async ({ accountID }) => {
|
||||
AuthSecretStorage.set({
|
||||
saveCredentials: async ({ accountID, secret }) => {
|
||||
localStorage[localStorageKey] = JSON.stringify({
|
||||
accountID,
|
||||
secretSeed,
|
||||
accountSecret,
|
||||
provider: "passphrase",
|
||||
});
|
||||
accountSecret: secret,
|
||||
} satisfies LocalStorageData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.driver.onSignedIn({ logOut });
|
||||
@@ -127,7 +125,7 @@ export class BrowserPassphraseAuth implements AuthMethod {
|
||||
this.driver.onError(error);
|
||||
},
|
||||
logOut: () => {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -150,5 +148,5 @@ export namespace BrowserPassphraseAuth {
|
||||
}
|
||||
|
||||
function logOut() {
|
||||
AuthSecretStorage.clear();
|
||||
delete localStorage[localStorageKey];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Peer } from "cojson";
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import type { Peer } from "jazz-tools";
|
||||
|
||||
export function createWebSocketPeerWithReconnection(
|
||||
peer: string,
|
||||
reconnectionTimeout: number | undefined,
|
||||
addPeer: (peer: Peer) => void,
|
||||
removePeer: (peer: Peer) => void,
|
||||
) {
|
||||
let currentPeer: Peer | undefined = undefined;
|
||||
const firstWsPeer = createWebSocketPeer({
|
||||
websocket: new WebSocket(peer),
|
||||
id: peer,
|
||||
role: "server",
|
||||
onClose: reconnectWebSocket,
|
||||
});
|
||||
|
||||
let shouldTryToReconnect = true;
|
||||
let currentReconnectionTimeout = reconnectionTimeout || 500;
|
||||
@@ -22,49 +26,35 @@ export function createWebSocketPeerWithReconnection(
|
||||
async function reconnectWebSocket() {
|
||||
if (!shouldTryToReconnect) return;
|
||||
|
||||
if (currentPeer) {
|
||||
removePeer(currentPeer);
|
||||
console.log(
|
||||
"Websocket disconnected, trying to reconnect in " +
|
||||
currentReconnectionTimeout +
|
||||
"ms",
|
||||
);
|
||||
currentReconnectionTimeout = Math.min(
|
||||
currentReconnectionTimeout * 2,
|
||||
30000,
|
||||
);
|
||||
|
||||
console.log(
|
||||
"Websocket disconnected, trying to reconnect in " +
|
||||
currentReconnectionTimeout +
|
||||
"ms",
|
||||
);
|
||||
currentReconnectionTimeout = Math.min(
|
||||
currentReconnectionTimeout * 2,
|
||||
30000,
|
||||
);
|
||||
|
||||
await waitForOnline(currentReconnectionTimeout);
|
||||
}
|
||||
await waitForOnline(currentReconnectionTimeout);
|
||||
|
||||
if (!shouldTryToReconnect) return;
|
||||
|
||||
currentPeer = createWebSocketPeer({
|
||||
websocket: new WebSocket(peer),
|
||||
id: peer,
|
||||
role: "server",
|
||||
onClose: reconnectWebSocket,
|
||||
});
|
||||
|
||||
addPeer(currentPeer);
|
||||
addPeer(
|
||||
createWebSocketPeer({
|
||||
websocket: new WebSocket(peer),
|
||||
id: peer,
|
||||
role: "server",
|
||||
onClose: reconnectWebSocket,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
enable: () => {
|
||||
shouldTryToReconnect = true;
|
||||
|
||||
if (!currentPeer) {
|
||||
reconnectWebSocket();
|
||||
}
|
||||
},
|
||||
disable: () => {
|
||||
peer: firstWsPeer,
|
||||
done: () => {
|
||||
shouldTryToReconnect = false;
|
||||
window.removeEventListener("online", onOnline);
|
||||
if (currentPeer) {
|
||||
removePeer(currentPeer);
|
||||
currentPeer = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,12 +18,10 @@ import {
|
||||
import { OPFSFilesystem } from "./OPFSFilesystem.js";
|
||||
import { createWebSocketPeerWithReconnection } from "./createWebSocketPeerWithReconnection.js";
|
||||
import { StorageConfig, getStorageOptions } from "./storageOptions.js";
|
||||
export { AuthSecretStorage } from "./auth/AuthSecretStorage.js";
|
||||
export { BrowserDemoAuth } from "./auth/DemoAuth.js";
|
||||
export { BrowserPasskeyAuth } from "./auth/PasskeyAuth.js";
|
||||
export { BrowserPassphraseAuth } from "./auth/PassphraseAuth.js";
|
||||
export { BrowserAnonymousAuth } from "./auth/AnonymousAuth.js";
|
||||
import { BrowserAnonymousAuth } from "./auth/AnonymousAuth.js";
|
||||
export { BrowserOnboardingAuth } from "./auth/OnboardingAuth.js";
|
||||
import { setupInspector } from "./utils/export-account-inspector.js";
|
||||
|
||||
setupInspector();
|
||||
@@ -31,7 +29,6 @@ setupInspector();
|
||||
/** @category Context Creation */
|
||||
export type BrowserContext<Acc extends Account> = {
|
||||
me: Acc;
|
||||
toggleNetwork: (enabled: boolean) => void;
|
||||
logOut: () => void;
|
||||
// TODO: Symbol.dispose?
|
||||
done: () => void;
|
||||
@@ -39,21 +36,15 @@ export type BrowserContext<Acc extends Account> = {
|
||||
|
||||
export type BrowserGuestContext = {
|
||||
guest: AnonymousJazzAgent;
|
||||
toggleNetwork: (enabled: boolean) => void;
|
||||
logOut: () => void;
|
||||
done: () => void;
|
||||
};
|
||||
|
||||
export type BrowserContextOptions<Acc extends Account> = {
|
||||
auth?: AuthMethod;
|
||||
AccountSchema?: CoValueClass<Acc> & {
|
||||
auth: AuthMethod;
|
||||
AccountSchema: CoValueClass<Acc> & {
|
||||
fromNode: (typeof Account)["fromNode"];
|
||||
};
|
||||
guest: false;
|
||||
} & BaseBrowserContextOptions;
|
||||
|
||||
export type BrowserGuestContextOptions = {
|
||||
guest: true;
|
||||
} & BaseBrowserContextOptions;
|
||||
|
||||
export type BaseBrowserContextOptions = {
|
||||
@@ -61,25 +52,34 @@ export type BaseBrowserContextOptions = {
|
||||
reconnectionTimeout?: number;
|
||||
storage?: StorageConfig;
|
||||
crypto?: CryptoProvider;
|
||||
localOnly?: boolean;
|
||||
};
|
||||
|
||||
function getAnonymousUserAuth() {
|
||||
const auth = new BrowserAnonymousAuth("Anonymous user", {
|
||||
onSignedIn: () => {},
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
/** @category Context Creation */
|
||||
export async function createJazzBrowserContext<Acc extends Account>(
|
||||
options: BrowserContextOptions<Acc> | BrowserGuestContextOptions,
|
||||
options: BrowserContextOptions<Acc>,
|
||||
): Promise<BrowserContext<Acc>>;
|
||||
export async function createJazzBrowserContext(
|
||||
options: BaseBrowserContextOptions,
|
||||
): Promise<BrowserGuestContext>;
|
||||
export async function createJazzBrowserContext<Acc extends Account>(
|
||||
options: BrowserContextOptions<Acc> | BaseBrowserContextOptions,
|
||||
): Promise<BrowserContext<Acc> | BrowserGuestContext>;
|
||||
export async function createJazzBrowserContext<Acc extends Account>(
|
||||
options: BrowserContextOptions<Acc> | BaseBrowserContextOptions,
|
||||
): Promise<BrowserContext<Acc> | BrowserGuestContext> {
|
||||
const crypto = options.crypto || (await WasmCrypto.create());
|
||||
let node: LocalNode | undefined = undefined;
|
||||
|
||||
const wsPeer = createWebSocketPeerWithReconnection(
|
||||
options.peer,
|
||||
options.reconnectionTimeout,
|
||||
(peer) => {
|
||||
if (node) {
|
||||
node.syncManager.addPeer(peer);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const { useSingleTabOPFS, useIndexedDB } = getStorageOptions(options.storage);
|
||||
|
||||
const peersToLoadFrom: Peer[] = [];
|
||||
@@ -97,37 +97,13 @@ export async function createJazzBrowserContext<Acc extends Account>(
|
||||
peersToLoadFrom.push(await IDBStorage.asPeer());
|
||||
}
|
||||
|
||||
const wsPeer = createWebSocketPeerWithReconnection(
|
||||
options.peer,
|
||||
options.reconnectionTimeout,
|
||||
(peer) => {
|
||||
if (node) {
|
||||
node.syncManager.addPeer(peer);
|
||||
} else {
|
||||
peersToLoadFrom.push(peer);
|
||||
}
|
||||
},
|
||||
(peer) => {
|
||||
peersToLoadFrom.splice(peersToLoadFrom.indexOf(peer), 1);
|
||||
},
|
||||
);
|
||||
|
||||
function toggleNetwork(enabled: boolean) {
|
||||
if (enabled) {
|
||||
wsPeer.enable();
|
||||
} else {
|
||||
wsPeer.disable();
|
||||
}
|
||||
}
|
||||
|
||||
toggleNetwork(!options.localOnly);
|
||||
peersToLoadFrom.push(wsPeer.peer);
|
||||
|
||||
const context =
|
||||
options.guest !== true
|
||||
"auth" in options
|
||||
? await createJazzContext({
|
||||
AccountSchema:
|
||||
"AccountSchema" in options ? options.AccountSchema : undefined,
|
||||
auth: options.auth ?? getAnonymousUserAuth(),
|
||||
AccountSchema: options.AccountSchema,
|
||||
auth: options.auth,
|
||||
crypto,
|
||||
peersToLoadFrom,
|
||||
sessionProvider: provideBrowserLockSession,
|
||||
@@ -143,9 +119,8 @@ export async function createJazzBrowserContext<Acc extends Account>(
|
||||
return "account" in context
|
||||
? {
|
||||
me: context.account,
|
||||
toggleNetwork,
|
||||
done: () => {
|
||||
wsPeer.disable();
|
||||
wsPeer.done();
|
||||
context.done();
|
||||
},
|
||||
logOut: () => {
|
||||
@@ -154,9 +129,8 @@ export async function createJazzBrowserContext<Acc extends Account>(
|
||||
}
|
||||
: {
|
||||
guest: context.agent,
|
||||
toggleNetwork,
|
||||
done: () => {
|
||||
wsPeer.disable();
|
||||
wsPeer.done();
|
||||
context.done();
|
||||
},
|
||||
logOut: () => {
|
||||
@@ -196,9 +170,9 @@ export function provideBrowserLockSession(
|
||||
if (!lock) return "noLock";
|
||||
|
||||
const sessionID =
|
||||
localStorage.getItem(accountID + "_" + idx) ||
|
||||
localStorage[accountID + "_" + idx] ||
|
||||
crypto.newRandomSessionID(accountID as RawAccountID | AgentID);
|
||||
localStorage.setItem(accountID + "_" + idx, sessionID);
|
||||
localStorage[accountID + "_" + idx] = sessionID;
|
||||
|
||||
// console.debug(
|
||||
// "Got lock",
|
||||
@@ -206,7 +180,7 @@ export function provideBrowserLockSession(
|
||||
// sessionID
|
||||
// );
|
||||
|
||||
resolveSession(sessionID as SessionID);
|
||||
resolveSession(sessionID);
|
||||
|
||||
await donePromise;
|
||||
console.log("Done with lock", accountID + "_" + idx, sessionID);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user