Compare commits

..

25 Commits

Author SHA1 Message Date
Anselm
3757d12dc4 Fix loading of accounts 2023-12-20 21:48:01 +00:00
Anselm
fc5b670c73 Publish 2023-12-20 11:33:39 +00:00
Anselm
c8adcc4c47 Fix unused var 2023-12-20 11:32:40 +00:00
Anselm
41a755fe41 publish 2023-12-20 11:17:25 +00:00
Anselm
8def1bb29e IndexedDB & timer perf improvements 2023-12-20 11:04:45 +00:00
Anselm
d379b04e33 Publish 2023-12-18 15:15:27 +00:00
Anselm
17a30e054e Add passphrase based auth + example 2023-12-18 15:13:37 +00:00
Anselm
7d8f4b4c00 Publish jazz-browser-auth0 2023-12-05 19:12:16 +00:00
Anselm
e2a3896bf0 v0.0.0 2023-12-05 19:11:00 +00:00
Anselm
446de8e0ff Fix for not receiving user_metadata 2023-12-05 19:10:15 +00:00
Anselm
5ae6c95878 Use authorization params everywhere 2023-12-05 17:07:18 +00:00
Anselm
7cde349a50 Actually deploy auth0 example 2023-12-05 16:45:41 +00:00
Anselm
61e640f574 Fix auth0 example dependencies 2023-12-05 16:43:18 +00:00
Anselm
ed122d9d8e Publish 2023-12-05 16:41:12 +00:00
Anselm
34817f4536 Auth0 integration and example 2023-12-05 16:38:37 +00:00
Anselm
0998a0eabf Correct homepage path 2023-11-24 17:09:34 +00:00
Anselm
a96108478b Homepage updates 2023-11-24 17:04:03 +00:00
Anselm
a4cf4c40d4 publish 2023-11-08 11:42:36 +00:00
Anselm Eickhoff
934fe4d29b Merge pull request #127 from KyleAMathews/main
fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts
2023-11-07 17:18:02 +00:00
Anselm Eickhoff
408012f2e5 remove redundant peer add 2023-11-07 17:17:41 +00:00
Kyle Mathews
d0078b830e fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts 2023-11-03 14:58:43 -07:00
Anselm Eickhoff
e52948b2b7 Merge pull request #125 from gardencmp/jazz-nodejs
jazz-nodejs MVP
2023-11-02 12:11:56 +00:00
Anselm
53bb1b230b Fix interpolation 2023-11-02 11:53:19 +00:00
Anselm
54e83aeaaa use NOMAD_ADDR secret 2023-11-02 11:43:32 +00:00
Anselm
aa3129cab5 Provide localNode to AccountMigrations 2023-10-27 14:48:36 +01:00
105 changed files with 3837 additions and 699 deletions

View File

@@ -11,8 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop"]
steps:
- uses: actions/checkout@v3
@@ -54,40 +53,39 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
# build-homepage:
# runs-on: ubuntu-latest
build-homepage:
runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
steps:
- uses: actions/checkout@v3
with:
submodules: true
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: gardencmp
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: gardencmp
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Docker Build & Push
# uses: docker/build-push-action@v4
# with:
# context: ./homepage/homepage-jazz
# push: true
# tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
# cache-from: type=gha
# cache-to: type=gha,mode=max
- name: Docker Build & Push
uses: docker/build-push-action@v4
with:
context: ./homepage
push: true
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-examples:
runs-on: ubuntu-latest
needs: build-examples
strategy:
matrix:
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop"]
steps:
- uses: actions/checkout@v3
@@ -115,38 +113,38 @@ jobs:
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./examples/${{ matrix.example }}
# deploy-homepage:
# runs-on: ubuntu-latest
# needs: build-homepage
deploy-homepage:
runs-on: ubuntu-latest
needs: build-homepage
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
# - uses: gacts/install-nomad@v1
# - name: Tailscale
# uses: tailscale/github-action@v1
# with:
# authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: gacts/install-nomad@v1
- name: Tailscale
uses: tailscale/github-action@v1
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
# - name: Deploy on Nomad
# run: |
# if [ "${{github.ref_name}}" == "main" ]; then
# export BRANCH_SUFFIX="";
# export BRANCH_SUBDOMAIN="";
# else
# export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
# export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
# fi
- name: Deploy on Nomad
run: |
if [ "${{github.ref_name}}" == "main" ]; then
export BRANCH_SUFFIX="";
export BRANCH_SUBDOMAIN="";
else
export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
fi
# export DOCKER_USER=gardencmp;
# export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
# export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
export DOCKER_USER=gardencmp;
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
# envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
# cat job-instance.nomad;
# NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
# working-directory: ./homepage/homepage-jazz
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./homepage

View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
examples/chat-passphrase/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,17 @@
# jazz-example-chat
## 0.0.47
### Patch Changes
- Implement passphrase based auth
- Updated dependencies
- jazz-react-auth-passphrase@0.5.1
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

View File

@@ -0,0 +1,64 @@
# Jazz Todo List Example
Live version: https://example-todo.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
```
(This ensures that you have the example app without git history or our multi-package monorepo)
Install dependencies:
```bash
npm install
```
Start the dev server:
```bash
npm run dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
## Walkthrough
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
3. Creating a new todo project: [`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx)
4. Reactively rendering a todo project as a table, adding and editing tasks: [`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx)
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/jazz-logo.png" />
<link rel="stylesheet" href="/src/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Chat Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
job "chat$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "chat$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,48 @@
{
"name": "jazz-example-chat-passphrase",
"private": true,
"version": "0.0.47",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"hash-slash": "^0.1.3",
"jazz-react": "^0.5.0",
"jazz-react-auth-passphrase": "^0.5.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,45 @@
import { WithJazz, useJazz } from 'jazz-react';
import { PassphraseAuth, PassphraseAuthBasicUI } from 'jazz-react-auth-passphrase';
import ReactDOM from 'react-dom/client';
import { HashRoute } from 'hash-slash';
import { ChatWindow } from './chatWindow.tsx';
import { Chat } from './dataModel.ts';
import {wordlist} from '@scure/bip39/wordlists/english';
ReactDOM.createRoot(document.getElementById('root')!).render(
<WithJazz
auth={PassphraseAuth({
appName: 'Jazz Chat Example',
wordlist,
Component: PassphraseAuthBasicUI
})}
apiKey="api_z9d034j3t34ht034ir"
>
<App />
</WithJazz>,
);
function App() {
return <div className='flex flex-col items-center justify-between w-screen h-screen p-2 dark:bg-black dark:text-white'>
<button onClick={useJazz().logOut} className='rounded mb-5 px-2 py-1 bg-stone-200 dark:bg-stone-800 dark:text-white self-end'>
Log Out
</button>
{HashRoute({
'/': <Home />,
'/chat/:id': (id) => <ChatWindow chatId={id as Chat['id']} />,
}, { reportToParentFrame: true })}
</div>
}
function Home() {
const { me } = useJazz();
return <button className='rounded py-2 px-4 bg-stone-200 dark:bg-stone-800 dark:text-white my-auto'
onClick={() => {
const group = me.createGroup().addMember('everyone', 'writer');
const chat = group.createList<Chat>();
location.hash = '/chat/' + chat.id;
}}>
Create New Chat
</button>
}

View File

@@ -0,0 +1,43 @@
import { useAutoSub } from 'jazz-react';
import { Chat, Message } from './dataModel.ts';
export function ChatWindow(props: { chatId: Chat['id'] }) {
const chat = useAutoSub(props.chatId);
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
{
chat.map((msg, i) => (
<ChatBubble key={msg?.id}
text={msg?.text}
by={chat.meta.edits[i].by?.profile?.name}
byMe={chat.meta.edits[i].by?.isMe}
at={chat.meta.edits[i].at} />
))
}
<ChatInput onSubmit={(text) => {
const msg = chat.meta.group.createMap<Message>({ text });
chat.append(msg.id);
}}/>
</div> : <div>Loading...</div>;
}
function ChatBubble(props: { text?: string, by?: string, at?: Date, byMe?: boolean }) {
return <div className={`${props.byMe ? 'items-end' : 'items-start'} flex flex-col`}>
<div className='rounded-xl bg-stone-100 dark:bg-stone-700 dark:text-white py-2 px-4 mt-2 min-w-[5rem]'>
{ props.text }
</div>
<div className='text-xs text-neutral-500 ml-2'>
{ props.by } { props.at?.getHours() }:{ props.at?.getMinutes() }
</div>
</div>;
}
function ChatInput(props: { onSubmit: (text: string) => void }) {
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white dark:border-stone-700'
placeholder='Type a message and press Enter'
onKeyDown={({ key, currentTarget: input }) => {
if (key !== 'Enter' || !input.value) return;
props.onSubmit(input.value);
input.value = '';
}}/>
}

View File

@@ -0,0 +1,4 @@
import { CoMap, CoList } from 'cojson';
export type Chat = CoList<Message['id']>;
export type Message = CoMap<{ text: string }>;

View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
margin: 0;
padding: 0;
}
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

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

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

24
examples/counter-js-auth0/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Counter Example (Auth0)</title>
</head>
<body>
<div id="root">
<div id="count">...initializing</div>
<button id="increment">Increment</button>
<button id="login">Login</button>
</div>
<script type="module" src="/index.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
import * as auth0 from "@auth0/auth0-spa-js";
import { Account, CoID, CoList, CoMap, Profile } from "cojson";
import {
ResolvedAccount,
autoSub,
autoSubResolution,
createBrowserNode,
} from "jazz-browser";
import { BrowserAuth0 } from "jazz-browser-auth0";
const auth0options = {
domain: "dev-12uyj8w4t4yjzkwa.us.auth0.com",
clientId: "TcYtq9an3PDyInQJvD1k8PtqupYG4PnA",
};
const authorizationParams = {
redirect_uri: window.location.origin,
audience: `https://${auth0options.domain}/api/v2/`,
scope: "read:current_user update:current_user_metadata",
};
window.onload = async () => {
const auth0Client = await auth0.createAuth0Client(auth0options);
const query = window.location.search;
if (query.includes("code=") && query.includes("state=")) {
await auth0Client.handleRedirectCallback();
window.history.replaceState({}, document.title, "/");
} else {
document
.getElementById("login")
?.addEventListener("click", async () => {
await auth0Client.loginWithRedirect({
authorizationParams,
});
});
}
if (!(await auth0Client.isAuthenticated())) {
return;
}
let accessToken: string | undefined;
try {
accessToken = await auth0Client.getTokenSilently({
authorizationParams,
});
} catch (e) {
alert(
"Failed to get access token silently, creating popup - this should only happen on localhost. Otherwise, check that allowed callback URLs are set correctly."
);
accessToken = await auth0Client.getTokenWithPopup({
authorizationParams,
});
}
if (!accessToken) {
throw new Error("No access token");
}
const { node, done } = await createBrowserNode({
auth: new BrowserAuth0(
{
domain: auth0options.domain,
clientID: auth0options.clientId,
accessToken,
},
auth0options.domain,
"jazz-browser-auth0-example"
),
migration: (account) => {
if (!account.get("root")) {
account.set(
"root",
account.createMap({
countIncrements: account.createList([]).id,
}).id
);
}
},
});
autoSub<Account<Profile, CoMap<{ countIncrements: CoID<CoList<number>> }>>>(
"me",
node,
(me) => {
if (me?.root?.countIncrements) {
document.getElementById("count")!.innerText =
me.root.countIncrements.reduce((sum, inc) => sum + inc, 0) +
"";
}
}
);
const increments = await autoSubResolution(
"me",
(
me: ResolvedAccount<
Account<
Profile,
CoMap<{ countIncrements: CoID<CoList<number>> }>
>
>
) => {
return me?.root?.countIncrements;
},
node
);
document.getElementById("increment")!.addEventListener("click", () => {
increments.append(1);
});
};

View File

@@ -0,0 +1,56 @@
job "counter-js-auth0$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "counter-js-auth0$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,22 @@
{
"name": "counter-js-auth0",
"private": true,
"version": "0.0.46",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@auth0/auth0-spa-js": "^2.1.2",
"jazz-browser": "^0.6.0",
"jazz-browser-auth0": "^0.6.2"
},
"devDependencies": {
"@vitejs/plugin-react-swc": "^3.3.2",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -49,7 +49,7 @@ export default function RootLayout({
mainLogo={<JazzLogo className="w-24" />}
items={[
{ title: "Toolkit", href: "/" },
{ title: "Global Mesh", href: "/mesh" },
{ title: "Mesh / Pricing", href: "/mesh" },
{
title: "Docs & Guides",
href: "https://github.com/gardencmp/jazz/blob/main/DOCS.md",

248
homepage/app/mesh/page.mdx Normal file
View File

@@ -0,0 +1,248 @@
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { fmtPrice, pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Jazz Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz Mesh
<Slogan>Serverless sync & storage for Jazz apps.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
## The first Collaboration Delivery Network
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
<Grid>
<GridCard>
### Mesh Free
<span className="text-2xl">$0</span>
- Unlimited projects
- For individual developers
- Low-latency sync
- Egress/mo: 5 million ops <span className="text-xs">or 50GB blobs</span>
- Storage: 2.5 million ops <span className="text-xs">or 25GB blobs</span>
</GridCard>
<GridCard>
### Mesh Starter <ComingSoonBadge/>
<span className="text-2xl">$9</span>/developer/mo
- Unlimited projects
- Up to 3 developers
- Low-latency sync
- Egress/mo: 50 million ops <span className="text-xs">or 500GB blobs</span>
- Storage: 25 million ops <span className="text-xs">or 250GB blobs</span>
<div className="text-xs">
- Extra egress: {fmtPrice(10 * pricePer1MtxSyncedOut)} per 10 million ops or 100GB blobs
- Extra storage: {fmtPrice(10 * pricePer1MtxStored)} per 10 million ops or 100GB blobs
</div>
</GridCard>
<GridCard>
### Mesh Pro <ComingSoonBadge/>
<span className="text-2xl">$19</span>/developer/mo
- Unlimited projects
- Up to 50 developers, SSO/SAML
- Ultra-low-latency sync
- Egress/mo: 100 million ops <span className="text-xs">or 1TB blobs</span>
- Storage: 50 million ops <span className="text-xs">or 500GB blobs</span>
<div className="text-xs">
- Extra egress: {fmtPrice(10 * pricePer1MtxSyncedOut)} per 10 million ops or 100GB blobs
- Extra storage: {fmtPrice(10 * pricePer1MtxStored)} per 10 million ops or 100GB blobs
</div>
</GridCard>
{/*<GridCard>
### Mesh Enterprise <ComingSoonBadge/>
<span className="text-2xl">Custom</span>
- Custom SLA
- Custom cloud deployment
- Dedicated support
- Audit logs
</GridCard>*/}
</Grid>
An operation represents an **individual user action**, or **10KB of data** for blobs/streams.
<Grid>
<GridItem className="col-start-1">
#### Egress:
<div className="text-sm">
- Operations sent out from Jazz Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each op should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Operations stored:
<div className="text-sm">
- Operations that are continuously persisted.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of ops generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- **Session A: 4 users co-oping 10 pages of text, typing them out as individual character inserts:**
- 3,000 inserts/page &times; 10 pages = 30,000 ops -> 30k ops stored
- 30,000 ops, each synced out to 3 other users -> 90k ops egress (one-time)
- **You could have ~50 such sessions/mo within Mesh Free** and you can keep storing ~80 such texts.
- **You could have ~500 such sessions/mo within Mesh Starter included usage** and you can keep storing ~800 such texts.
- **You could have ~1000 such sessions/mo within Mesh Pro included usage** and you can keep storing ~1600 such texts.
- Each further such session would cost you about {fmtPrice(90000 * pricePerTxSyncedOut)} and {fmtPrice(30000 * pricePerTxStored)} per month to keep storing the text.
- **Session B: 3 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 5 hours**
- 3 users &times; 10 FPS &times; 10s/min &times; 60min/h &times; 5h = 90k ops -> 90k ops stored
- 90k ops, each synced out to 2 other users -> 180k ops egress (one-time)
- **You could have ~20 such sessions/mo within Mesh Free** and you can keep storing 20 such canvases.
- **You could have ~250 such sessions/mo within Mesh Starter included usage** and you can keep storing 250 such canvases.
- **You could have ~500 such sessions/mo within Mesh Pro included usage** and you can keep storing 500 such canvases.
- Each further such session would cost you about {fmtPrice(180000 * pricePerTxSyncedOut)} and {fmtPrice(90000 * pricePerTxStored)} per month to keep storing the canvas.
- **Session C: A livestreamer streaming video (1GB total) to 25 viewers (combined live & on-demand)**
- 1GB = 100,000 ops (10KB each) -> 100k ops stored
- 100,000 ops, each synced out to 25 viewers -> 2.5M ops egress (one-time)
- **You could have ~2 such livestreams/mo within Mesh Free** and you can keep storing 25 such videos.
- **You could have ~20 such livestreams/mo within Mesh Starter included usage** and you can keep storing 250 such videos.
- **You could have ~40 such livestreams/mo within Mesh Pro included usage** and you can keep storing 500 such videos.
- Each further such livestream would cost you about {fmtPrice(2500000 * pricePerTxSyncedOut)} and {fmtPrice(100000 * pricePerTxStored)} per month to keep storing the video.
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan>You can rely on Jazz Mesh. But you don't have to.</Slogan>
<p>Because Jazz is open-source, you can optionally run your own sync nodes &mdash; in a variety of setups.</p>
<Grid>
<GridCard>
#### Jazz Mesh + Data Backup Node.
<p className="no-prose text-base">Connect your users to Jazz Mesh for all its benefits, but also run and connect your own data backup node (just in case.)</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Every op is additionally synced to your backup node and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Jazz Mesh + DIY Mesh.
<p className="no-prose text-base">Connect your users to Jazz Mesh, or your own nodes as a lower-performance fallback. The two networks stay in constant sync.</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Every op is additionally synced to your DIY mesh and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">Build your own network of sync and storage nodes.
Handle networking, security and backups yourself.</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -34,7 +34,7 @@ Jazz is an open-source toolkit for building apps with **sync** & **secure collab
<h2 className="md:mt-24">Hard things are easy now</h2>
Jazz replaces APIs, DBs and message queues with **a single new abstraction: CoJSON**.
Jazz replaces APIs, databases and message queues with **a single new abstraction: collaborative data**.
This means you get **built-in capabilities** that took best-in-class apps years to build:
@@ -71,20 +71,7 @@ This means you get **built-in capabilities** that took best-in-class apps years
</Grid>
</div>
## CoJSON
<Slogan small>The collaborative core.</Slogan>
Jazz is built around **CoJSON,** a new abstraction for **sync** & **secure collaborative data.** And while it does all the heavy lifting...
- **multi-device co-editing**
- **user identities & accounts**
- **permissions** & **roles**
- **sync** & **caching**
- **persistence**
...its API couldn't be simpler: CoJSON makes collaboration and secure access control feel like **inherent properties of your data**.
### Collaborative Values
## Collaborative Values
<Slogan small>Your new building blocks.</Slogan>
@@ -99,7 +86,7 @@ CoValues also **keep their full edit history,** including author metadata and po
<div className="text-sm">
- Collaborative key-value map
- Possible values:
- Immutable JSON & IDs of other CoValues
- Immutable JSON & other CoValues
</div>
</GridCard>
<GridCard>
@@ -107,7 +94,7 @@ CoValues also **keep their full edit history,** including author metadata and po
<div className="text-sm">
- Collaborative ordered list
- Possible items:
- Immutable JSON & IDs of other CoValues
- Immutable JSON & other CoValues
</div>
</GridCard>
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
@@ -139,7 +126,7 @@ A shocking amount of UI is text editing. CoJSON offers correct, versatile primit
### `CoStream`
<div className="text-sm">
- Collection of independent per-user items streams:
- Immutable JSON & IDs of other CoValues
- Immutable JSON & other CoValues
- Great for presence, reactions, polls, replies etc.
</div>
</GridCard>
@@ -185,7 +172,7 @@ A simple API to define access control from anywhere, verifiably enforced by encr
## The Jazz Toolkit
<Slogan small>Idiomatic bindings for CoJSON, with batteries included.</Slogan>
<Slogan small>A high-level toolkit for building apps around CoValues.</Slogan>
Supported environments:
<div className="text-sm">
@@ -264,15 +251,15 @@ Supported environments:
</GridCard>
</Grid>
## Global Mesh
## Jazz Mesh
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
To give you sync & secure collaborative data instantly on a global scale, we're running Global Mesh. It works with any CoJSON-based app, requires no setup and has straightforward, scale-to-zero pricing.
To give you sync and secure collaborative data instantly on a global scale, we're running Jazz Mesh. It works with any Jazz-based app, requires no setup and has straightforward, scale-to-zero pricing.
Global Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
Jazz Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
<Link href="/mesh" target="_blank">Learn more about Global Mesh</Link>
<Link href="/mesh" target="_blank">Learn more about Jazz Mesh</Link>
## Get Started

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,19 @@
import { ComingSoonBadge, Grid, GridCard, GridItem } from "./forMdx";
export const pricePer1MtxSyncedOut = 1;
export const pricePer1MtxStored = 2;
export const pricePer1MtxSyncedOut = 0.1;
export const pricePer1MtxStored = 0.2;
export const pricePerTxSyncedOut = pricePer1MtxSyncedOut / 1_000_000;
export const pricePerTxStored = pricePer1MtxStored / 1_000_000;
export function fmtPrice(raw: number) {
return raw.toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumSignificantDigits: 2,
});
}
export function Pricing() {
const worstCaseBytesPerTx = 200_000;

View File

@@ -1,202 +0,0 @@
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Global Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz Global Mesh
<Slogan>Serverless sync & storage for Jazz apps.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
## The first Collaboration Delivery Network
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
### Free Tier
<span className="text-lg font-medium bg-emerald-200 dark:bg-emerald-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</span>
<p className="text-sm">Later, any usage under $1/mo will be free.</p>
<Grid>
<GridItem className="md:col-span-2">
### Unlimited <ComingSoonBadge/>
<div className="lg:text-2xl border rounded-lg px-1 py-3 text-center">${pricePer1MtxSyncedOut} <small>per 1M TXs synced out</small> + ${pricePer1MtxStored}<small>/mo per 1M TXs stored</small></div>
<p><small>$6/mo minimum usage</small></p>
A TX (transaction) represents an **individual user action**, or **up to 100KB of binary data**.
</GridItem>
<GridItem className="col-start-1">
#### Transactions synced out:
<div className="text-sm">
- Transactions sent out from Global Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each transaction should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Transactions stored:
<div className="text-sm">
- Transactions that are continuously persisted.
- Counted per second.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of transactions generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- 4 users co-editing 10 pages of text, typing them out as individual character inserts:
- 3,000 inserts/page &times; 10 pages = 30,000 transactions
- 30,000 transactions stored = ${30000 * pricePerTxStored} / mo
- 3 &times; 30,000 transactions synced out = ${3 * 30000 * pricePerTxSyncedOut} one-time
- 4 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 2h/day for a month
- 4 users &times; 10 FPS &times; 10s/min &times; 60min/h * 2h/day &times; 30days = 1.44M transactions
- 1.44M transactions stored = ${1440000 * pricePerTxStored} / mo = ${1440000 * pricePerTxStored / 4} / mo / user
- 3 &times; 1.44M transactions synced out = ${(3 * 1440000 * pricePerTxSyncedOut).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time = ${(3 * 1440000 * pricePerTxSyncedOut / 4).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time / user
- A livestreamer streaming video (1GB total) to 100 viewers (combined live & on-demand)
- 1GB = 10,000 transactions (100KB each)
- 10,000 transactions stored = ${10000 * pricePerTxStored} / mo (= ${10000 * 1000 * pricePerTxStored} per 1TB stored)
- 100 &times; 10,000 transaction synced out = ${100 * 10000 * pricePerTxSyncedOut} one-time (= ${10000 * 1000 * pricePerTxSyncedOut} per 1TB egress)
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan>You can rely on Global Mesh. But you don't have to.</Slogan>
<p>Because Jazz is open-source, you can optionally run your own sync nodes &mdash; in a variety of setups.</p>
<Grid>
<GridCard>
#### Global Mesh + Data Backup Node.
<p className="no-prose text-base">Connect your users to Global Mesh for all its benefits, but also run and connect your own data backup node (just in case.)</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Every transaction is additionally synced to your backup node and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Global Mesh + DIY Mesh.
<p className="no-prose text-base">Connect your users to Global Mesh, or your own nodes as a lower-performance fallback. The two networks stay in constant sync.</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Every transaction is additionally synced to your DIY mesh and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">Build your own network of sync and storage nodes.
Handle networking, security and backups yourself.</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -22,14 +22,14 @@ await rm("./codeSamples", { recursive: true, force: true });
(
await Promise.all(
(
await readdir(path.join("../../", dir))
await readdir(path.join("../", dir))
).map(async (f) =>
(f.endsWith(".ts") && f !== "vite-env.d.ts") ||
f.endsWith(".tsx")
? [
f,
await readFile(
path.join("../../", dir, f),
path.join("../", dir, f),
"utf8"
),
]

View File

@@ -18,5 +18,6 @@
"updated": "lerna updated --include-merged-tags",
"publish-all": "yarn run gen-docs && lerna publish --include-merged-tags",
"gen-docs": "ts-node generateDocs.ts"
}
},
"version": "0.0.0"
}

View File

@@ -1,5 +1,19 @@
# cojson-storage-indexeddb
## 0.6.2
### Patch Changes
- Fix TypeScript lint
## 0.6.1
### Patch Changes
- IndexedDB & timer perf improvements
- Updated dependencies
- cojson@0.6.4
## 0.6.0
### Minor Changes

View File

@@ -1,11 +1,11 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.6.0",
"version": "0.6.2",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.6.0",
"cojson": "^0.6.4",
"typescript": "^5.1.6"
},
"devDependencies": {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,228 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */
const isFunction = (func: any) => typeof func === 'function';
const isObject = (supposedObject: any) =>
typeof supposedObject === 'object' &&
supposedObject !== null &&
!Array.isArray(supposedObject);
const isThenable = (obj: any) => isObject(obj) && isFunction(obj.then);
const identity = (val: any) => val;
export { isObject, isThenable, identity, isFunction };
enum States {
PENDING = 'PENDING',
RESOLVED = 'RESOLVED',
REJECTED = 'REJECTED',
}
interface Handler<T, U> {
onSuccess: HandlerOnSuccess<T, U>;
onFail: HandlerOnFail<U>;
}
type HandlerOnSuccess<T, U = any> = (value: T) => U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) => U | Thenable<U>;
type Finally<U> = () => U | Thenable<U>;
interface Thenable<T> {
then<U>(
onSuccess?: HandlerOnSuccess<T, U>,
onFail?: HandlerOnFail<U>,
): Thenable<U>;
then<U>(
onSuccess?: HandlerOnSuccess<T, U>,
onFail?: (reason: any) => void,
): Thenable<U>;
}
type Resolve<R> = (value?: R | Thenable<R>) => void;
type Reject = (value?: any) => void;
export class SyncPromise<T> {
private state: States = States.PENDING;
private handlers: Handler<T, any>[] = [];
private value: T | any;
public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) {
try {
callback(this.resolve as Resolve<T>, this.reject);
} catch (e) {
this.reject(e);
}
}
private resolve = (value: T) => {
return this.setResult(value, States.RESOLVED);
};
private reject = (reason: any) => {
return this.setResult(reason, States.REJECTED);
};
private setResult = (value: T | any, state: States) => {
const set = () => {
if (this.state !== States.PENDING) {
return null;
}
if (isThenable(value)) {
return (value as Thenable<T>).then(this.resolve, this.reject);
}
this.value = value;
this.state = state;
return this.executeHandlers();
};
set();
};
private executeHandlers = () => {
if (this.state === States.PENDING) {
return null;
}
this.handlers.forEach((handler) => {
if (this.state === States.REJECTED) {
return handler.onFail(this.value);
}
return handler.onSuccess(this.value);
});
this.handlers = [];
};
private attachHandler = (handler: Handler<T, any>) => {
this.handlers = [...this.handlers, handler];
this.executeHandlers();
};
public then<U>(
onSuccess: HandlerOnSuccess<T, U>,
onFail?: HandlerOnFail<U>,
) {
return new SyncPromise<U>((resolve, reject) => {
return this.attachHandler({
onSuccess: (result) => {
try {
return resolve(onSuccess(result));
} catch (e) {
return reject(e);
}
},
onFail: (reason) => {
if (!onFail) {
return reject(reason);
}
try {
return resolve(onFail(reason));
} catch (e) {
return reject(e);
}
},
});
});
}
public catch<U>(onFail: HandlerOnFail<U>) {
return this.then<U>(identity, onFail);
}
// methods
public toString() {
return `[object SyncPromise]`;
}
public finally<U>(cb: Finally<U>) {
return new SyncPromise<U>((resolve, reject) => {
let val: U | any;
let isRejected: boolean;
return this.then(
(value) => {
isRejected = false;
val = value;
return cb();
},
(reason) => {
isRejected = true;
val = reason;
return cb();
},
).then(() => {
if (isRejected) {
return reject(val);
}
return resolve(val);
});
});
}
public spread<U>(handler: (...args: any[]) => U) {
return this.then<U>((collection) => {
if (Array.isArray(collection)) {
return handler(...collection);
}
return handler(collection);
});
}
// static
public static resolve<U = any>(value?: U | Thenable<U>) {
return new SyncPromise<U>((resolve) => {
return resolve(value);
});
}
public static reject<U>(reason?: any) {
return new SyncPromise<U>((_resolve, reject) => {
return reject(reason);
});
}
public static all<U = any>(collection: (U | Thenable<U>)[]) {
return new SyncPromise<U[]>((resolve, reject) => {
if (!Array.isArray(collection)) {
return reject(new TypeError('An array must be provided.'));
}
if (collection.length === 0) {
return resolve([]);
}
let counter = collection.length;
const resolvedCollection: U[] = [];
const tryResolve = (value: U, index: number) => {
counter -= 1;
resolvedCollection[index] = value;
if (counter !== 0) {
return null;
}
return resolve(resolvedCollection);
};
return collection.forEach((item, index) => {
return SyncPromise.resolve(item)
.then((value) => {
return tryResolve(value, index);
})
.catch(reject);
});
});
}
}

View File

@@ -1,5 +1,29 @@
# cojson
## 0.6.4
### Patch Changes
- IndexedDB & timer perf improvements
## 0.6.3
### Patch Changes
- Implement passphrase based auth
## 0.6.2
### Patch Changes
- Add peersToLoadFrom for node creation as well
## 0.6.1
### Patch Changes
- Provide localNode to AccountMigrations
## 0.6.0
### Minor Changes

View File

@@ -5,7 +5,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.6.0",
"version": "0.6.4",
"devDependencies": {
"@noble/curves": "^1.2.0",
"@types/jest": "^29.5.3",

View File

@@ -15,6 +15,7 @@ import {
import { AgentID } from "../ids.js";
import { CoMap } from "./coMap.js";
import { Group, InviteSecret } from "./group.js";
import { LocalNode } from "../index.js";
export function accountHeaderForInitialAgentSecret(
agentSecret: AgentSecret
@@ -169,5 +170,6 @@ export type AccountMigration<
Meta extends AccountMeta = AccountMeta
> = (
account: ControlledAccount<P, R, Meta>,
profile: P
profile: P,
localNode: LocalNode
) => void | Promise<void>;

View File

@@ -2,6 +2,7 @@ import {
CoValueCore,
newRandomSessionID,
MAX_RECOMMENDED_TX_SIZE,
idforHeader,
} from "./coValueCore.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { LocalNode } from "./localNode.js";
@@ -36,7 +37,7 @@ import { Group, EVERYONE } from "./coValues/group.js";
import type { Everyone } from "./coValues/group.js";
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
import { parseJSON } from "./jsonStringify.js";
import { Account, Profile } from "./coValues/account.js";
import { Account, Profile, accountHeaderForInitialAgentSecret } from "./coValues/account.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
@@ -80,6 +81,8 @@ export const cojsonInternals = {
parseJSON,
accountOrAgentIDfromSessionID,
isAccountID,
accountHeaderForInitialAgentSecret,
idforHeader
};
export {

View File

@@ -74,10 +74,12 @@ export class LocalNode {
Meta extends AccountMeta = AccountMeta
>({
name,
peersToLoadFrom,
migration,
initialAgentSecret = newRandomAgentSecret(),
}: {
name: string;
peersToLoadFrom?: Peer[];
migration?: AccountMigration<P, R, Meta>;
initialAgentSecret?: AgentSecret;
}): Promise<{
@@ -107,8 +109,14 @@ export class LocalNode {
"After creating account"
);
if (peersToLoadFrom) {
for (const peer of peersToLoadFrom) {
nodeWithAccount.syncManager.addPeer(peer);
}
}
if (migration) {
await migration(accountOnNodeWithAccount, profile as P);
await migration(accountOnNodeWithAccount, profile as P, nodeWithAccount);
}
nodeWithAccount.account = new ControlledAccount(
@@ -160,12 +168,12 @@ export class LocalNode {
newRandomSessionID(accountID)
);
const accountPromise = loadingNode.load(accountID);
for (const peer of peersToLoadFrom) {
loadingNode.syncManager.addPeer(peer);
}
const accountPromise = loadingNode.load(accountID);
const account = await accountPromise;
if (account === "unavailable") {
@@ -196,7 +204,8 @@ export class LocalNode {
if (migration) {
await migration(
controlledAccount as ControlledAccount<P, R, Meta>,
profile as P
profile as P,
node
);
node.account = new ControlledAccount(
controlledAccount.core,

View File

@@ -160,10 +160,8 @@ export function newStreamPair<T>(
// make sure write resolves before corresponding read, but make sure writes are still in order
await lastWritePromise;
lastWritePromise = new Promise((resolve) => {
setTimeout(() => {
enqueue(chunk);
resolve();
});
enqueue(chunk);
resolve();
});
}
},

View File

@@ -195,7 +195,7 @@ export class SyncManager {
return await this.handleKnownState(msg, peer);
}
case "content":
await new Promise<void>((resolve) => setTimeout(resolve, 0));
// await new Promise<void>((resolve) => setTimeout(resolve, 0));
return await this.handleNewContent(msg, peer);
case "done":
return await this.handleUnsubscribe(msg);
@@ -355,9 +355,9 @@ export class SyncManager {
e
);
});
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});
// await new Promise<void>((resolve) => {
// setTimeout(resolve, 0);
// });
} catch (e) {
console.error(
new Date(),

View File

@@ -288,9 +288,16 @@ export function autoSub(
if (!effectiveId) return () => {};
// const ctxId = Math.random().toString(16).slice(2);
// let updateN = 0;
const context = new AutoSubContext(node, () => {
const rootResolved = context.values[effectiveId]?.lastLoaded;
// const n = updateN;
// updateN++;
// console.time("AutoSubContext.onUpdate " + n + " " + ctxId);
callback(rootResolved);
// console.timeEnd("AutoSubContext.onUpdate " + n + " " + ctxId);
});
context.autoSub(effectiveId, [], "");

View File

@@ -0,0 +1,21 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:require-extensions/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "require-extensions"],
parserOptions: {
project: "./tsconfig.json",
},
root: true,
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-floating-promises": "error",
},
};

View File

@@ -0,0 +1,171 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
.DS_Store

View File

@@ -0,0 +1,2 @@
coverage
node_modules

View File

@@ -0,0 +1,33 @@
# jazz-browser-auth-local
## 0.5.1
### Patch Changes
- Implement passphrase based auth
## 0.5.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- jazz-browser@0.6.0
## 0.4.17
### Patch Changes
- Allow account migrations to be async
- Updated dependencies
- jazz-browser@0.5.1
## 0.4.16
### Patch Changes
- Updated dependencies
- jazz-browser@0.5.0

View File

@@ -0,0 +1,18 @@
{
"name": "jazz-browser-auth-passphrase",
"version": "0.5.1",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"@scure/bip39": "^1.2.2",
"jazz-browser": "^0.6.0",
"typescript": "^5.1.6"
},
"scripts": {
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
}

View File

@@ -0,0 +1,177 @@
import {
AccountID,
AccountMigration,
AgentSecret,
cojsonInternals,
LocalNode,
Peer,
} from "cojson";
import { agentSecretFromSecretSeed } from "cojson/src/crypto";
import { AuthProvider, SessionProvider } from "jazz-browser";
type SessionStorageData = {
accountID: AccountID;
accountSecret: AgentSecret;
};
const sessionStorageKey = "jazz-logged-in-secret";
export interface BrowserPassphraseAuthDriver {
onReady: (next: {
signUp: (username: string, passphrase: string) => Promise<void>;
logIn: (passphrase: string) => Promise<void>;
}) => void;
onSignedIn: (next: { logOut: () => void }) => void;
}
export class BrowserPassphraseAuth implements AuthProvider {
driver: BrowserPassphraseAuthDriver;
appName: string;
appHostname: string;
wordlist: string[];
constructor(
driver: BrowserPassphraseAuthDriver,
wordlist: string[],
appName: string,
// TODO: is this a safe default?
appHostname: string = window.location.hostname
) {
this.driver = driver;
this.wordlist = wordlist;
this.appName = appName;
this.appHostname = appHostname;
}
async createNode(
getSessionFor: SessionProvider,
initialPeers: Peer[],
migration?: AccountMigration
): Promise<LocalNode> {
if (sessionStorage[sessionStorageKey]) {
const sessionStorageData = JSON.parse(
sessionStorage[sessionStorageKey]
) as SessionStorageData;
const sessionID = await getSessionFor(sessionStorageData.accountID);
const node = await LocalNode.withLoadedAccount({
accountID: sessionStorageData.accountID,
accountSecret: sessionStorageData.accountSecret,
sessionID,
peersToLoadFrom: initialPeers,
migration,
});
this.driver.onSignedIn({ logOut });
return Promise.resolve(node);
} else {
const node = await new Promise<LocalNode>(
(doneSigningUpOrLoggingIn) => {
this.driver.onReady({
signUp: async (username, passphrase) => {
const node = await signUp(
username,
passphrase,
this.wordlist,
getSessionFor,
this.appName,
this.appHostname,
migration
);
for (const peer of initialPeers) {
node.syncManager.addPeer(peer);
}
doneSigningUpOrLoggingIn(node);
this.driver.onSignedIn({ logOut });
},
logIn: async (passphrase: string) => {
const node = await logIn(
passphrase,
this.wordlist,
getSessionFor,
this.appHostname,
initialPeers,
migration
);
doneSigningUpOrLoggingIn(node);
this.driver.onSignedIn({ logOut });
},
});
}
);
return node;
}
}
}
import * as bip39 from '@scure/bip39';
async function signUp(
username: string,
passphrase: string,
wordlist: string[],
getSessionFor: SessionProvider,
appName: string,
appHostname: string,
migration?: AccountMigration
): Promise<LocalNode> {
const secretSeed = bip39.mnemonicToEntropy(passphrase, wordlist);
const { node, accountID, accountSecret } =
await LocalNode.withNewlyCreatedAccount({
name: username,
initialAgentSecret: agentSecretFromSecretSeed(secretSeed),
migration,
});
sessionStorage[sessionStorageKey] = JSON.stringify({
accountID,
accountSecret,
} satisfies SessionStorageData);
node.currentSessionID = await getSessionFor(accountID);
return node;
}
async function logIn(
passphrase: string,
wordlist: string[],
getSessionFor: SessionProvider,
appHostname: string,
initialPeers: Peer[],
migration?: AccountMigration
): Promise<LocalNode> {
const accountSecretSeed = bip39.mnemonicToEntropy(passphrase, wordlist);
const accountSecret = agentSecretFromSecretSeed(accountSecretSeed);
if (!accountSecret) {
throw new Error("Invalid credential");
}
const accountID = cojsonInternals.idforHeader(cojsonInternals.accountHeaderForInitialAgentSecret(accountSecret)) as AccountID;
sessionStorage[sessionStorageKey] = JSON.stringify({
accountID,
accountSecret,
} satisfies SessionStorageData);
const node = await LocalNode.withLoadedAccount({
accountID,
accountSecret,
sessionID: await getSessionFor(accountID),
peersToLoadFrom: initialPeers,
migration,
});
return node;
}
function logOut() {
delete sessionStorage[sessionStorageKey];
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"module": "esnext",
"target": "ES2020",
"moduleResolution": "bundler",
"moduleDetection": "force",
"strict": true,
"jsx": "react",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
},
"include": ["./src/**/*"],
}

View File

@@ -0,0 +1,83 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@noble/ciphers@^0.1.3":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
"@noble/curves@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
dependencies:
"@noble/hashes" "1.3.1"
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@scure/base@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/react@^18.2.19":
version "18.2.19"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.3"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
cojson@^0.0.14:
version "0.0.14"
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
dependencies:
"@noble/ciphers" "^0.1.3"
"@noble/curves" "^1.1.0"
"@noble/hashes" "^1.3.1"
"@scure/base" "^1.1.1"
fast-json-stable-stringify "^2.1.0"
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
fast-json-stable-stringify@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
version "1.0.3"
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
jazz-react@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/jazz-react/-/jazz-react-0.0.6.tgz#53b0245720b10ec31ac8deba45fd8a052b313b06"
integrity sha512-JlYTKUVPpuK3T7cLfk2YwHh3yH+2BPVSuWIQui35U52/gce+HmTMGolqFYGghWMVQtwclaZ0IoEbtuycPiADOQ==
dependencies:
cojson "^0.0.14"
typescript "^5.1.6"
typescript@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==

View File

@@ -0,0 +1,21 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:require-extensions/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "require-extensions"],
parserOptions: {
project: "./tsconfig.json",
},
root: true,
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-floating-promises": "error",
},
};

171
packages/jazz-browser-auth0/.gitignore vendored Normal file
View File

@@ -0,0 +1,171 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
.DS_Store

View File

@@ -0,0 +1,2 @@
coverage
node_modules

View File

@@ -0,0 +1,39 @@
# jazz-browser-auth-local
## 0.6.2
### Patch Changes
- Fix bug where user_metadata wasn't received
## 0.6.1
### Patch Changes
- Initial implementation
## 0.5.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- jazz-browser@0.6.0
## 0.4.17
### Patch Changes
- Allow account migrations to be async
- Updated dependencies
- jazz-browser@0.5.1
## 0.4.16
### Patch Changes
- Updated dependencies
- jazz-browser@0.5.0

View File

@@ -0,0 +1,21 @@
{
"name": "jazz-browser-auth0",
"version": "0.6.2",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"auth0-js": "^9.23.3",
"jazz-browser": "^0.6.0",
"typescript": "^5.1.6"
},
"scripts": {
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13",
"devDependencies": {
"@types/auth0-js": "^9.21.5"
}
}

View File

@@ -0,0 +1,123 @@
import {
AccountID,
AccountMigration,
AgentSecret,
LocalNode,
Peer,
} from "cojson";
import { AuthProvider, SessionProvider } from "jazz-browser";
import { Auth0UserProfile, Authentication, Management } from "auth0-js";
type CredentialData = {
accountID: AccountID;
accountSecret: AgentSecret;
};
export class BrowserAuth0 implements AuthProvider {
clientID: string;
auth0domain: string;
accessToken: string;
appName: string;
appHostname: string;
/** Create or use Jazz account linked to an Auth0 account. The access token must have been retreived from a WebAuthn instance created with the following options:
*
* - `audience: https://{yourDomain}/api/v2/`
* - `scope: "read:current_user update:current_user_metadata"`
*/
constructor(
auth0Options: {
clientID: string;
domain: string;
accessToken: string;
},
appName: string,
// TODO: is this a safe default?
appHostname: string = window.location.hostname
) {
this.clientID = auth0Options.clientID;
this.auth0domain = auth0Options.domain;
this.accessToken = auth0Options.accessToken;
this.appName = appName;
this.appHostname = appHostname;
}
async createNode(
getSessionFor: SessionProvider,
initialPeers: Peer[],
migration?: AccountMigration
): Promise<LocalNode> {
const auth0client = new Authentication({
clientID: this.clientID,
domain: this.auth0domain,
});
const user = await new Promise<Auth0UserProfile>((resolve, reject) => {
auth0client.userInfo(this.accessToken, (err, user) => {
if (err) reject(err);
else resolve(user);
});
});
const management = new Management({
token: this.accessToken,
domain: this.auth0domain,
});
const metadata = await new Promise<Record<string, string>>(
(resolve, reject) => {
management.getUser(user.sub, (err, profile) => {
if (err) reject(err);
else resolve(profile.user_metadata);
});
}
);
const existingCredentialString = metadata?.["jazz_credential"];
if (existingCredentialString) {
const existingCredential = JSON.parse(
existingCredentialString
) as CredentialData;
return LocalNode.withLoadedAccount({
accountID: existingCredential.accountID,
accountSecret: existingCredential.accountSecret,
sessionID: await getSessionFor(existingCredential.accountID),
peersToLoadFrom: initialPeers,
migration,
});
} else {
const username =
user.nickname || user.name || user.email || user.sub;
const { node, accountID, accountSecret } =
await LocalNode.withNewlyCreatedAccount({
name: username,
migration,
peersToLoadFrom: initialPeers,
});
await new Promise<void>((resolve, reject) =>
management.patchUserMetadata(
user.sub,
{
jazz_credential: JSON.stringify({
accountID,
accountSecret,
} as CredentialData),
},
(err) => {
if (err) reject(err);
else resolve();
}
)
);
return node;
}
}
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"module": "esnext",
"target": "ES2020",
"moduleResolution": "bundler",
"moduleDetection": "force",
"strict": true,
"jsx": "react",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
},
"include": ["./src/**/*"],
}

View File

@@ -0,0 +1,83 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@noble/ciphers@^0.1.3":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
"@noble/curves@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
dependencies:
"@noble/hashes" "1.3.1"
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@scure/base@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/react@^18.2.19":
version "18.2.19"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.3"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
cojson@^0.0.14:
version "0.0.14"
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
dependencies:
"@noble/ciphers" "^0.1.3"
"@noble/curves" "^1.1.0"
"@noble/hashes" "^1.3.1"
"@scure/base" "^1.1.1"
fast-json-stable-stringify "^2.1.0"
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
fast-json-stable-stringify@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
version "1.0.3"
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
jazz-react@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/jazz-react/-/jazz-react-0.0.6.tgz#53b0245720b10ec31ac8deba45fd8a052b313b06"
integrity sha512-JlYTKUVPpuK3T7cLfk2YwHh3yH+2BPVSuWIQui35U52/gce+HmTMGolqFYGghWMVQtwclaZ0IoEbtuycPiADOQ==
dependencies:
cojson "^0.0.14"
typescript "^5.1.6"
typescript@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==

View File

@@ -1,5 +1,14 @@
# jazz-browser
## 0.6.1
### Patch Changes
- IndexedDB & timer perf improvements
- Updated dependencies
- cojson@0.6.4
- cojson-storage-indexeddb@0.6.1
## 0.6.0
### Minor Changes

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-browser",
"version": "0.6.0",
"version": "0.6.1",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.6.0",
"cojson-storage-indexeddb": "^0.6.0",
"cojson": "^0.6.4",
"cojson-storage-indexeddb": "^0.6.1",
"jazz-autosub": "^0.6.0",
"typescript": "^5.1.6"
},

View File

@@ -1,5 +1,27 @@
# jazz-autosub
## 0.6.3
### Patch Changes
- IndexedDB & timer perf improvements
- Updated dependencies
- cojson@0.6.4
## 0.6.2
### Patch Changes
- Add peersToLoadFrom for node creation as well
- Updated dependencies
- cojson@0.6.2
## 0.6.1
### Patch Changes
- Fix wrong import from cojson
## 0.6.0
### Minor Changes

View File

@@ -5,9 +5,9 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.6.0",
"version": "0.6.3",
"dependencies": {
"cojson": "^0.6.0",
"cojson": "^0.6.4",
"cojson-transport-nodejs-ws": "^0.5.1",
"jazz-autosub": "^0.6.0"
},

View File

@@ -17,8 +17,8 @@ import {
Profile,
SessionID,
cojsonReady,
cojsonInternals
} from "cojson";
import { newRandomSessionID } from "cojson/src/coValueCore";
import { readFile, writeFile } from "node:fs/promises";
if (!("crypto" in globalThis)) {
@@ -72,7 +72,7 @@ export async function createOrResumeWorker<
// TODO: locked sessions similar to browser
const sessionID =
process.env.JAZZ_WORKER_SESSION ||
newRandomSessionID(existingCredentials.accountID);
cojsonInternals.newRandomSessionID(existingCredentials.accountID);
console.log("Loading worker", existingCredentials.accountID);
@@ -94,13 +94,12 @@ export async function createOrResumeWorker<
} else {
const newWorker = await LocalNode.withNewlyCreatedAccount({
name: workerName,
peersToLoadFrom: [wsPeer],
migration,
});
localNode = newWorker.node;
localNode.syncManager.addPeer(wsPeer);
await credentialStorage.save(
workerName,
newWorker.accountID,

View File

@@ -0,0 +1,21 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:require-extensions/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "require-extensions"],
parserOptions: {
project: "./tsconfig.json",
},
root: true,
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-floating-promises": "error",
},
};

View File

@@ -0,0 +1,171 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
.DS_Store

View File

@@ -0,0 +1,2 @@
coverage
node_modules

View File

@@ -0,0 +1,25 @@
# jazz-react-auth-local
## 0.5.1
### Patch Changes
- Implement passphrase based auth
- Updated dependencies
- jazz-browser-auth-passphrase@0.5.1
## 0.4.17
### Patch Changes
- Updated dependencies
- jazz-browser-auth-local@0.5.0
- jazz-react@0.5.1
## 0.4.16
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-browser-auth-local@0.4.16

View File

@@ -0,0 +1,24 @@
{
"name": "jazz-react-auth-passphrase",
"version": "0.5.1",
"main": "dist/index.js",
"types": "src/index.tsx",
"license": "MIT",
"dependencies": {
"jazz-browser-auth-passphrase": "^0.5.1",
"jazz-react": "^0.5.1",
"typescript": "^5.1.6"
},
"devDependencies": {
"@types/react": "^18.2.19"
},
"peerDependencies": {
"react": "17 - 18"
},
"scripts": {
"lint": "eslint src/**/*.tsx",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
}

View File

@@ -0,0 +1,234 @@
import { useMemo, useState, ReactNode } from "react";
import { BrowserPassphraseAuth } from "jazz-browser-auth-passphrase";
import { ReactAuthHook } from "jazz-react";
import { generateMnemonic } from "@scure/bip39";
import { cojsonInternals } from "cojson";
export type PassphraseAuthComponent = (props: {
loading: boolean;
logIn: (passphrase: string) => void;
signUp: (username: string, passphrase: string) => void;
generateRandomPassphrase: () => string;
}) => ReactNode;
export function PassphraseAuth({
appName,
appHostname,
wordlist,
Component = PassphraseAuthBasicUI,
}: {
appName: string;
appHostname?: string;
wordlist: string[];
Component?: PassphraseAuthComponent;
}): ReactAuthHook {
return function useLocalAuth() {
const [authState, setAuthState] = useState<
| { state: "loading" }
| {
state: "ready";
logIn: (passphrase: string) => void;
signUp: (username: string, passphrase: string) => void;
}
| { state: "signedIn"; logOut: () => void }
>({ state: "loading" });
const [logOutCounter, setLogOutCounter] = useState(0);
const auth = useMemo(() => {
return new BrowserPassphraseAuth(
{
onReady(next) {
setAuthState({
state: "ready",
logIn: next.logIn,
signUp: next.signUp,
});
},
onSignedIn(next) {
setAuthState({
state: "signedIn",
logOut: () => {
next.logOut();
setAuthState({ state: "loading" });
setLogOutCounter((c) => c + 1);
},
});
},
},
wordlist,
appName,
appHostname
);
}, [appName, appHostname, logOutCounter]);
const generateRandomPassphrase = () => {
return generateMnemonic(
wordlist,
cojsonInternals.secretSeedLength * 8
);
};
const AuthUI =
authState.state === "ready"
? Component({
loading: false,
logIn: authState.logIn,
signUp: authState.signUp,
generateRandomPassphrase,
})
: Component({
loading: false,
logIn: () => {},
signUp: (_) => {},
generateRandomPassphrase,
});
return {
auth,
AuthUI,
logOut:
authState.state === "signedIn" ? authState.logOut : undefined,
};
};
}
export const PassphraseAuthBasicUI = ({
logIn,
signUp,
generateRandomPassphrase,
}: {
logIn: (passphrase: string) => void;
signUp: (username: string, passphrase: string) => void;
generateRandomPassphrase: () => string;
}) => {
const [username, setUsername] = useState<string>("");
const [passphrase, setPassphrase] = useState<string>("");
const [loginPassphrase, setLoginPassphrase] = useState<string>("");
return (
<div
style={{
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
width: "30rem",
display: "flex",
flexDirection: "column",
gap: "2rem",
}}
>
<form
style={{
width: "30rem",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
onSubmit={(e) => {
e.preventDefault();
setPassphrase("");
setUsername("");
signUp(username, passphrase);
}}
>
<div style={{ display: "flex", gap: "0.5rem" }}>
<textarea
placeholder="Passphrase"
value={passphrase}
onChange={(e) => setPassphrase(e.target.value)}
style={{
border: "2px solid #000",
padding: "11px 8px",
borderRadius: "6px",
height: "7rem",
flex: 1
}}
/>
<button
type="button"
onClick={(e) => {
setPassphrase(generateRandomPassphrase());
e.preventDefault();
}}
style={{
padding: "11px 8px",
borderRadius: "6px",
background: "#eee",
}}
>
Random
</button>
</div>
<input
placeholder="Display name"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{
border: "2px solid #000",
padding: "11px 8px",
borderRadius: "6px",
}}
/>
<input
type="submit"
value="Sign Up as new account"
style={{
background: "#000",
color: "#fff",
padding: "13px 5px",
border: "none",
borderRadius: "6px",
cursor: "pointer",
}}
/>
</form>
<div style={{textAlign: "center"}}>&mdash; or &mdash;</div>
<form
style={{
width: "30rem",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
onSubmit={(e) => {
e.preventDefault();
setLoginPassphrase("");
logIn(loginPassphrase);
}}
>
<textarea
placeholder="Passphrase"
value={loginPassphrase}
onChange={(e) => setLoginPassphrase(e.target.value)}
style={{
border: "2px solid #000",
padding: "11px 8px",
borderRadius: "6px",
height: "7rem",
}}
/>
<input
type="submit"
value="Log in as existing account"
style={{
background: "#000",
color: "#fff",
padding: "13px 5px",
border: "none",
borderRadius: "6px",
cursor: "pointer",
}}
/>
</form>
</div>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More