Compare commits

..

7 Commits

Author SHA1 Message Date
Trisha Lim
ea2e2a7f42 clean up 2025-02-25 20:19:49 +07:00
Trisha Lim
cfef71f290 set image width to full 2025-02-25 20:05:30 +07:00
Trisha Lim
b9f7267cce styling for uploading state 2025-02-25 20:00:48 +07:00
Trisha Lim
cf486a9c5a fix tailwind 2025-02-25 19:55:46 +07:00
Trisha Lim
23795ec07b handle different states 2025-02-25 19:53:35 +07:00
Benjamin S. Leveritt
1efcea89d8 Adds unload notification while uploading 2025-02-23 16:52:37 +00:00
Benjamin S. Leveritt
4f31fddcbc Return promise while processing image 2025-02-23 16:51:21 +00:00
2154 changed files with 77254 additions and 176507 deletions

View File

@@ -2,24 +2,34 @@
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [],
"fixed": [
"fixed": [],
"linked": [
[
"cojson",
"cojson-core-wasm",
"cojson-storage",
"cojson-storage-indexeddb",
"cojson-storage-sqlite",
"cojson-transport-ws",
"jazz-auth-betterauth",
"jazz-betterauth-client-plugin",
"jazz-betterauth-server-plugin",
"jazz-react-auth-betterauth",
"jazz-browser",
"jazz-auth-clerk",
"jazz-browser-media-images",
"jazz-nodejs",
"jazz-react",
"jazz-react-auth-clerk",
"jazz-react-native",
"jazz-react-native-auth-clerk",
"jazz-react-native-media-images",
"jazz-run",
"jazz-svelte",
"jazz-tools",
"community-jazz-vue"
"jazz-vue"
]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "minor"
"updateInternalDependencies": "patch",
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}

8
.github/CODEOWNERS vendored
View File

@@ -1,8 +0,0 @@
./packages @garden-co/framework
./tests @garden-co/framework
./packages/quint-ui @garden-co/ui
./homepage @garden-co/ui
./homepage/homepage/content/docs @garden-co/docs
./starters @garden-co/docs
./examples @garden-co/docs @garden-co/ui

View File

@@ -1,10 +0,0 @@
---
name: Docs request
about: Allow people to quickly report issues & improvements for the docs
title: 'Docs: '
labels: docs, requested
assignees: bensleveritt
---

View File

@@ -1,23 +0,0 @@
# Description
<!-- Please include a summary of the change and which issue is fixed -->
<!-- Please also include relevant motivation and context -->
<!-- Include any links to documentation like RFCs if necessary -->
<!-- Add a link to to relevant preview environments or anything that would simplify visual review process -->
<!-- Supplemental screenshots and video are encouraged, but the primary description should be in text -->
## Manual testing instructions
<!-- Add any actions required to manually test the changes -->
## Tests
- [ ] Tests have been added and/or updated
- [ ] Tests have not been updated, because: <!-- Insert reason for not updating tests here -->
- [ ] I need help with writing tests
## Checklist
- [ ] I've updated the part of the docs that are affected the PR changes
- [ ] I've generated a changeset, if a version bump is required
- [ ] I've updated the jsDoc comments to the public APIs I've modified, or added them when missing

37
.github/workflows/build-examples.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build Examples
on:
push:
branches: [ "main" ]
jobs:
build-examples:
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
example: [
"chat",
"clerk",
"passkey",
"inspector",
"music-player",
"password-manager",
"pets",
"reactions",
"todo",
]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: |
pnpm install
pnpm turbo build;
working-directory: ./examples/${{ matrix.example }}

26
.github/workflows/build-starters.yaml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build Starters
on:
push:
branches: ["main"]
jobs:
build-starters:
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
starter: ["react-passkey-auth"]
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: |
pnpm install
pnpm turbo build;
working-directory: ./starters/${{ matrix.starter }}

View File

@@ -1,27 +1,18 @@
name: Code quality
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
branches:
- "main"
pull_request:
jobs:
quality:
runs-on: blacksmith-2vcpu-ubuntu-2404-arm
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: 2.1.3
version: latest
- name: Run Biome
run: biome ci .
run: biome ci .

View File

@@ -1,77 +0,0 @@
name: Test `create-jazz-app` Distribution
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'packages/create-jazz-app/**'
jobs:
test-create-jazz-app-distribution:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Build create-jazz-app
run: pnpm build
working-directory: packages/create-jazz-app
- name: Pack create-jazz-app
run: pnpm pack
working-directory: packages/create-jazz-app
- name: Create test directory
run: mkdir -p /tmp/test-create-jazz-app
- name: Initialize test package
run: |
cd /tmp/test-create-jazz-app
bun init -y
- name: Install packed create-jazz-app
run: |
cd /tmp/test-create-jazz-app
bun install ${{ github.workspace }}/packages/create-jazz-app/create-jazz-app-*.tgz
- name: Test basic functionality
run: |
cd /tmp/test-create-jazz-app
bunx create-jazz-app --help
- name: Create test project and validate catalog resolution
run: |
cd /tmp/test-create-jazz-app
mkdir test-project
cd test-project
echo -e "\n\n\n\n\n\n\n\n" | bunx create-jazz-app . --framework react --starter react-passkey-auth --package-manager bun --git false
- name: Validate no unresolved catalog references
run: |
cd /tmp/test-create-jazz-app/test-project
# Check for unresolved catalog: references in package.json
if grep -r "catalog:" package.json; then
echo "❌ Found unresolved catalog: references in generated project"
exit 1
fi
# Check for unresolved workspace: references
if grep -r "workspace:" package.json; then
echo "❌ Found unresolved workspace: references in generated project"
exit 1
fi
echo "✅ All catalog and workspace references resolved successfully"

View File

@@ -1,11 +1,5 @@
name: End-to-End Tests for React Native
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened]
@@ -13,7 +7,8 @@ on:
- ".github/actions/android-emulator/**"
- ".github/actions/source-code/**"
- ".github/workflows/e2e-rn-test.yml"
- "examples/chat-rn-expo/**"
- "examples/chat-rn/**"
- "examples/chat-rn-clerk/**"
- "packages/**"
jobs:
@@ -41,9 +36,10 @@ jobs:
- name: Pnpm Build
run: pnpm turbo build --filter="./packages/*"
- name: chat-rn-expo App Pre Build
working-directory: ./examples/chat-rn-expo
- name: chat-rn App Pre Build
working-directory: ./examples/chat-rn
run: |
pnpm build
pnpm expo prebuild --clean
- name: Install Maestro
@@ -65,9 +61,8 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
disable-animations: true
working-directory: ./examples/chat-rn-expo/
# killall due to this issue: https://github.com/ReactiveCircus/android-emulator-runner/issues/385
script: ./test/e2e/run.sh && ( killall -INT crashpad_handler || true )
working-directory: ./examples/chat-rn/
script: ./test/e2e/run.sh
- name: Copy Maestro Output
if: steps.e2e_test.outcome != 'success'

View File

@@ -1,11 +1,5 @@
name: Jazz Run Tests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
branches: ["main"]
@@ -14,7 +8,7 @@ on:
jobs:
test:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2204
timeout-minutes: 5
steps:

View File

@@ -1,11 +1,5 @@
name: Playwright Tests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
branches: ["main"]
@@ -15,11 +9,11 @@ on:
jobs:
test:
timeout-minutes: 60
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2204
continue-on-error: true
strategy:
matrix:
shard: ["1/2", "2/2"]
project: ["tests/e2e", "examples/chat", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "starters/react-passkey-auth"]
steps:
- uses: actions/checkout@v4
@@ -29,131 +23,21 @@ jobs:
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: pnpm turbo build
working-directory: ./${{ matrix.project }}
- name: Install Playwright Browsers
run: pnpm exec playwright install
working-directory: ./${{ matrix.project }}
- name: Run Playwright tests for shard ${{ matrix.shard }}
run: |
# Parse shard information (e.g., "1/2" -> shard_num=1, total_shards=2)
IFS='/' read -r shard_num total_shards <<< "${{ matrix.shard }}"
shard_index=$((shard_num - 1)) # Convert to 0-based index
# Debug: Print parsed values
echo "Parsed shard_num: $shard_num"
echo "Parsed total_shards: $total_shards"
echo "Calculated shard_index: $shard_index"
# Define all projects to test
all_projects=(
"tests/e2e"
"examples/chat"
"examples/chat-svelte"
"examples/community-clerk-vue"
"examples/clerk"
"examples/betterauth"
"examples/file-share-svelte"
"examples/form"
"examples/inspector"
"examples/music-player"
"examples/organization"
"examples/server-worker-http"
"starters/react-passkey-auth"
"starters/svelte-passkey-auth"
"tests/jazz-svelte"
)
# Calculate which projects this shard should run
shard_projects=()
for i in "${!all_projects[@]}"; do
if [ $((i % total_shards)) -eq $shard_index ]; then
shard_projects+=("${all_projects[i]}")
fi
done
# Track project results
overall_exit_code=0
failed_projects=()
passed_projects=()
echo "=== Running tests for shard ${{ matrix.shard }} ==="
echo "Projects in this shard:"
printf '%s\n' "${shard_projects[@]}"
echo
# Run tests for each project
for project in "${shard_projects[@]}"; do
echo "=== Testing project: $project ==="
# Check if project directory exists
if [ ! -d "$project" ]; then
echo "❌ FAILED: Project directory $project does not exist"
failed_projects+=("$project (directory not found)")
overall_exit_code=1
continue
fi
# Check if project has package.json
if [ ! -f "$project/package.json" ]; then
echo "❌ FAILED: No package.json found in $project"
failed_projects+=("$project (no package.json)")
overall_exit_code=1
continue
fi
# Build the project
echo "🔨 Building $project..."
cd "$project"
if [ -f .env.test ]; then
cp .env.test .env
fi
if ! pnpm turbo build; then
echo "❌ BUILD FAILED: $project"
failed_projects+=("$project (build failed)")
overall_exit_code=1
cd - > /dev/null
continue
fi
# Run Playwright tests
echo "🧪 Running Playwright tests for $project..."
if ! pnpm exec playwright test; then
echo "❌ TESTS FAILED: $project"
failed_projects+=("$project (tests failed)")
overall_exit_code=1
else
echo "✅ TESTS PASSED: $project"
passed_projects+=("$project")
fi
cd - > /dev/null
echo "=== Finished testing $project ==="
echo
done
# Print summary report
echo "=========================================="
echo "📊 TEST SUMMARY FOR SHARD ${{ matrix.shard }}"
echo "=========================================="
if [ ${#passed_projects[@]} -gt 0 ]; then
echo "✅ PASSED (${#passed_projects[@]}):"
printf ' - %s\n' "${passed_projects[@]}"
echo
fi
if [ ${#failed_projects[@]} -gt 0 ]; then
echo "❌ FAILED (${#failed_projects[@]}):"
printf ' - %s\n' "${failed_projects[@]}"
echo
fi
echo "Total projects in shard: ${#shard_projects[@]}"
echo "Passed: ${#passed_projects[@]}"
echo "Failed: ${#failed_projects[@]}"
echo "=========================================="
# Exit with overall status
exit $overall_exit_code
- name: Run Playwright tests
run: pnpm exec playwright test
working-directory: ./${{ matrix.project }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ hashFiles(format('{0}/package.json', matrix.project)) }}-playwright-report
path: ./${{ matrix.project }}/playwright-report/
retention-days: 30

View File

@@ -1,14 +1,7 @@
name: Pre-Publish tagged Pull Requests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
jobs:
pre-release:
@@ -106,4 +99,4 @@ jobs:
);
await logPublishInfo();
}
}
}

View File

@@ -17,7 +17,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout Repo
uses: actions/checkout@v4
@@ -25,9 +25,6 @@ jobs:
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Build packages
run: pnpm exec turbo run build --filter='./packages/*'
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1

View File

@@ -1,11 +1,5 @@
name: Unit Tests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened]
@@ -15,7 +9,7 @@ on:
jobs:
unit-tests:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout

13
.gitignore vendored
View File

@@ -20,17 +20,8 @@ __screenshots__
# Playwright
test-results
# Java
.java-version
.husky
.vscode/*
.idea/*
.vscode/settings.json
.svelte-kit
.cursorrules
.windsurfrules
playwright-report
.svelte-kit

View File

@@ -36,7 +36,7 @@ We welcome all ideas! If you have suggestions, feel free to open an issue marked
### 5. Local Setup
You'll need Node.js 22.x installed (we're working on support for 23.x), and pnpm 9.x installed. If you're using nix, run `nix develop` to get a shell with the correct versions of everything installed.
You'll need Node.js 20.x or 22.x installed (we're working on support for 23.x), and pnpm 9.x installed. If you're using nix, run `nix develop` to get a shell with the correct versions of everything installed.
1. **Clone the repository**:
```bash
@@ -54,16 +54,10 @@ You'll need Node.js 22.x installed (we're working on support for 23.x), and pnpm
cd homepage && pnpm install
```
4. **Go back to the project root**:
```bash
cd ..
```
4. **Build the packages**:
```bash
pnpm build:packages
pnpm build
```
5. **Run tests** to verify everything is working:

View File

@@ -1,171 +0,0 @@
import { describe, bench } from "vitest";
import * as tools from "jazz-tools";
import * as toolsLatest from "jazz-tools-latest";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { WasmCrypto as WasmCryptoLatest } from "cojson-latest/crypto/WasmCrypto";
import { PureJSCrypto } from "cojson/crypto/PureJSCrypto";
import { PureJSCrypto as PureJSCryptoLatest } from "cojson-latest/crypto/PureJSCrypto";
const sampleReactions = ["👍", "❤️", "😄", "🎉"];
const sampleHiddenIn = ["user1", "user2", "user3"];
// Define the schemas based on the provided Message schema
async function createSchema(
tools: typeof toolsLatest,
WasmCrypto: typeof WasmCryptoLatest,
) {
const Embed = tools.co.map({
url: tools.z.string(),
title: tools.z.string().optional(),
description: tools.z.string().optional(),
image: tools.z.string().optional(),
});
const Message = tools.co.map({
content: tools.z.string(),
createdAt: tools.z.date(),
updatedAt: tools.z.date(),
hiddenIn: tools.co.list(tools.z.string()),
replyTo: tools.z.string().optional(),
reactions: tools.co.list(tools.z.string()),
softDeleted: tools.z.boolean().optional(),
embeds: tools.co.optional(tools.co.list(Embed)),
author: tools.z.string().optional(),
threadId: tools.z.string().optional(),
});
const ctx = await tools.createJazzContextForNewAccount({
creationProps: {
name: "Test Account",
},
// @ts-expect-error
crypto: await WasmCrypto.create(),
});
return {
Message,
sampleReactions,
sampleHiddenIn,
Group: tools.Group,
account: ctx.account,
};
}
const PUREJS = false;
// @ts-expect-error
const schema = await createSchema(tools, PUREJS ? PureJSCrypto : WasmCrypto);
const schemaLatest = await createSchema(
toolsLatest,
// @ts-expect-error
PUREJS ? PureJSCryptoLatest : WasmCryptoLatest,
);
const message = schema.Message.create(
{
content: "A".repeat(1024),
createdAt: new Date(),
updatedAt: new Date(),
hiddenIn: sampleHiddenIn,
reactions: sampleReactions,
author: "user123",
},
schema.Group.create(schema.account).makePublic(),
);
const content = await tools.exportCoValue(schema.Message, message.id, {
// @ts-expect-error
loadAs: schema.account,
});
tools.importContentPieces(content ?? [], schema.account as any);
toolsLatest.importContentPieces(content ?? [], schemaLatest.account);
schema.account._raw.core.node.internalDeleteCoValue(message.id as any);
schemaLatest.account._raw.core.node.internalDeleteCoValue(message.id as any);
describe("Message.create", () => {
bench(
"current version",
() => {
schema.Message.create(
{
content: "A".repeat(1024),
createdAt: new Date(),
updatedAt: new Date(),
hiddenIn: sampleHiddenIn,
reactions: sampleReactions,
author: "user123",
},
schema.Group.create(schema.account),
);
},
{ iterations: 1000 },
);
bench(
"Jazz 0.17.9",
() => {
schemaLatest.Message.create(
{
content: "A".repeat(1024),
createdAt: new Date(),
updatedAt: new Date(),
hiddenIn: sampleHiddenIn,
reactions: sampleReactions,
author: "user123",
},
schemaLatest.Group.create(schemaLatest.account),
);
},
{ iterations: 1000 },
);
});
describe("Message import", () => {
bench(
"current version",
() => {
tools.importContentPieces(content ?? [], schema.account as any);
schema.account._raw.core.node.internalDeleteCoValue(message.id as any);
},
{ iterations: 5000 },
);
bench(
"Jazz 0.17.9",
() => {
toolsLatest.importContentPieces(content ?? [], schemaLatest.account);
schemaLatest.account._raw.core.node.internalDeleteCoValue(
message.id as any,
);
},
{ iterations: 5000 },
);
});
describe("import+ decrypt", () => {
bench(
"current version",
() => {
tools.importContentPieces(content ?? [], schema.account as any);
const node = schema.account._raw.core.node;
node.expectCoValueLoaded(message.id as any).getCurrentContent();
node.internalDeleteCoValue(message.id as any);
},
{ iterations: 5000 },
);
bench(
"Jazz 0.17.9",
() => {
toolsLatest.importContentPieces(content ?? [], schemaLatest.account);
const node = schemaLatest.account._raw.core.node;
node.expectCoValueLoaded(message.id as any).getCurrentContent();
node.internalDeleteCoValue(message.id as any);
},
{ iterations: 5000 },
);
});

View File

@@ -1,14 +0,0 @@
{
"name": "jazz-tools-benchmark",
"private": true,
"type": "module",
"dependencies": {
"cojson": "workspace:*",
"jazz-tools": "workspace:*",
"cojson-latest": "npm:cojson@0.17.9",
"jazz-tools-latest": "npm:jazz-tools@0.17.9"
},
"scripts": {
"bench": "vitest bench"
}
}

View File

@@ -1,7 +0,0 @@
import { defineProject } from "vitest/config";
export default defineProject({
test: {
name: "bench",
},
});

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -7,36 +7,35 @@
},
"files": {
"ignoreUnknown": false,
"includes": [
"**",
"!crates/**",
"!**/jazz-tools.json",
"!**/ios/**",
"!**/android/**",
"!**/tests/jazz-svelte/src/**",
"!**/examples/**/*svelte*/**",
"!**/starters/**/*svelte*/**",
"!**/examples/server-worker-inbox/src/routeTree.gen.ts",
"!**/homepage/homepage/**",
"!**/package.json",
"!**/*svelte*/**"
"ignore": [
"jazz-tools.json",
"**/ios/**",
"**/android/**",
"packages/jazz-svelte/**",
"examples/*svelte*/**"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"assist": { "actions": { "source": { "organizeImports": "off" } } },
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": false,
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "off",
"useImportExtensions": {
"level": "error",
"options": {
"forceJsExtensions": true
"suggestedExtensions": {
"ts": {
"module": "js",
"component": "jsx"
}
}
}
}
}
@@ -44,16 +43,16 @@
},
"overrides": [
{
"includes": ["packages/community-jazz-vue/src/**"],
"include": ["**/package.json"],
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
"enabled": false
},
"formatter": {
"enabled": false
}
},
{
"includes": ["**/packages/**/src/**"],
"include": ["packages/**/src/**"],
"linter": {
"enabled": true,
"rules": {
@@ -62,29 +61,23 @@
}
},
{
"includes": [
"**/packages/cojson/src/storage/**/*/**",
"**/cojson-transport-ws/**"
],
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
},
{
"includes": ["**/tests/**"],
"include": ["packages/**/src/tests/**", "packages/**/src/test/**"],
"linter": {
"rules": {
"correctness": {
"useImportExtensions": "off"
},
"style": {
"noNonNullAssertion": "off"
},
}
}
}
},
{
"include": ["packages/cojson-storage-indexeddb/**"],
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
"noExplicitAny": "info"
}
}
}

8
crates/.gitignore vendored
View File

@@ -1,8 +0,0 @@
# Rust
/target
# Test artifacts
lzy/compressed_66k.lzy
# OS generated files
.DS_Store

1164
crates/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
[workspace]
resolver = "2"
members = [
"lzy",
"cojson-core",
"cojson-core-wasm",
]

View File

@@ -1,5 +0,0 @@
# cojson-core-wasm
## 0.17.11
## 0.17.10

View File

@@ -1,29 +0,0 @@
[package]
name = "cojson-core-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cojson-core = { path = "../cojson-core" }
wasm-bindgen = "0.2"
console_error_panic_hook = { version = "0.1.7", optional = true }
ed25519-dalek = { version = "2.2.0", default-features = false, features = ["rand_core"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"
serde = { version = "1.0", features = ["derive"] }
js-sys = "0.3"
getrandom = { version = "0.2", features = ["js"] }
thiserror = "1.0"
hex = "0.4"
blake3 = "1.5"
x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"] }
crypto_secretbox = { version = "0.1.1", features = ["getrandom"] }
salsa20 = "0.10.2"
rand = "0.8"
bs58 = "0.5"
[features]
default = ["console_error_panic_hook"]

View File

@@ -1,26 +0,0 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
mkdirSync("./public", { recursive: true });
const wasm = readFileSync("./pkg/cojson_core_wasm_bg.wasm");
writeFileSync(
"./public/cojson_core_wasm.wasm.js",
`export const data = "data:application/wasm;base64,${wasm.toString("base64")}";`,
);
writeFileSync(
"./public/cojson_core_wasm.wasm.d.ts",
"export const data: string;",
);
const glueJs = readFileSync("./pkg/cojson_core_wasm.js", "utf8").replace(
"module_or_path = new URL('cojson_core_wasm_bg.wasm', import.meta.url);",
"throw new Error();",
);
writeFileSync("./public/cojson_core_wasm.js", glueJs);
writeFileSync(
"./public/cojson_core_wasm.d.ts",
readFileSync("./pkg/cojson_core_wasm.d.ts", "utf8"),
);

View File

@@ -1,3 +0,0 @@
export * from "./public/cojson_core_wasm.js";
export async function initialize(): Promise<void>;

View File

@@ -1,8 +0,0 @@
export * from "./public/cojson_core_wasm.js";
import __wbg_init from "./public/cojson_core_wasm.js";
import { data } from "./public/cojson_core_wasm.wasm.js";
export async function initialize() {
return await __wbg_init({ module_or_path: data });
}

View File

@@ -1,22 +0,0 @@
{
"name": "cojson-core-wasm",
"type": "module",
"version": "0.17.11",
"files": [
"public/cojson_core_wasm.js",
"public/cojson_core_wasm.d.ts",
"public/cojson_core_wasm.wasm.js",
"public/cojson_core_wasm.wasm.d.ts",
"index.js",
"index.d.ts"
],
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"build:wasm": "wasm-pack build --release --target web && node build.js",
"build:dev": "wasm-pack build --dev --target web && node build.js"
},
"devDependencies": {
"wasm-pack": "^0.13.1"
}
}

View File

@@ -1,291 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* WASM-exposed function for XSalsa20 encryption without authentication.
* - `key`: 32-byte key for encryption
* - `nonce_material`: Raw bytes used to generate a 24-byte nonce via BLAKE3
* - `plaintext`: Raw bytes to encrypt
* Returns the encrypted bytes or throws a JsError if encryption fails.
* Note: This function does not provide authentication. Use encrypt_xsalsa20_poly1305 for authenticated encryption.
*/
export function encrypt_xsalsa20(key: Uint8Array, nonce_material: Uint8Array, plaintext: Uint8Array): Uint8Array;
/**
* WASM-exposed function for XSalsa20 decryption without authentication.
* - `key`: 32-byte key for decryption (must match encryption key)
* - `nonce_material`: Raw bytes used to generate a 24-byte nonce (must match encryption)
* - `ciphertext`: Encrypted bytes to decrypt
* Returns the decrypted bytes or throws a JsError if decryption fails.
* Note: This function does not provide authentication. Use decrypt_xsalsa20_poly1305 for authenticated decryption.
*/
export function decrypt_xsalsa20(key: Uint8Array, nonce_material: Uint8Array, ciphertext: Uint8Array): Uint8Array;
/**
* Generate a new Ed25519 signing key using secure random number generation.
* Returns 32 bytes of raw key material suitable for use with other Ed25519 functions.
*/
export function new_ed25519_signing_key(): Uint8Array;
/**
* WASM-exposed function to derive an Ed25519 verifying key from a signing key.
* - `signing_key`: 32 bytes of signing key material
* Returns 32 bytes of verifying key material or throws JsError if key is invalid.
*/
export function ed25519_verifying_key(signing_key: Uint8Array): Uint8Array;
/**
* WASM-exposed function to sign a message using Ed25519.
* - `signing_key`: 32 bytes of signing key material
* - `message`: Raw bytes to sign
* Returns 64 bytes of signature material or throws JsError if signing fails.
*/
export function ed25519_sign(signing_key: Uint8Array, message: Uint8Array): Uint8Array;
/**
* WASM-exposed function to verify an Ed25519 signature.
* - `verifying_key`: 32 bytes of verifying key material
* - `message`: Raw bytes that were signed
* - `signature`: 64 bytes of signature material
* Returns true if signature is valid, false otherwise, or throws JsError if verification fails.
*/
export function ed25519_verify(verifying_key: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
/**
* WASM-exposed function to validate and copy Ed25519 signing key bytes.
* - `bytes`: 32 bytes of signing key material to validate
* Returns the same 32 bytes if valid or throws JsError if invalid.
*/
export function ed25519_signing_key_from_bytes(bytes: Uint8Array): Uint8Array;
/**
* WASM-exposed function to derive the public key from an Ed25519 signing key.
* - `signing_key`: 32 bytes of signing key material
* Returns 32 bytes of public key material or throws JsError if key is invalid.
*/
export function ed25519_signing_key_to_public(signing_key: Uint8Array): Uint8Array;
/**
* WASM-exposed function to sign a message with an Ed25519 signing key.
* - `signing_key`: 32 bytes of signing key material
* - `message`: Raw bytes to sign
* Returns 64 bytes of signature material or throws JsError if signing fails.
*/
export function ed25519_signing_key_sign(signing_key: Uint8Array, message: Uint8Array): Uint8Array;
/**
* WASM-exposed function to validate and copy Ed25519 verifying key bytes.
* - `bytes`: 32 bytes of verifying key material to validate
* Returns the same 32 bytes if valid or throws JsError if invalid.
*/
export function ed25519_verifying_key_from_bytes(bytes: Uint8Array): Uint8Array;
/**
* WASM-exposed function to validate and copy Ed25519 signature bytes.
* - `bytes`: 64 bytes of signature material to validate
* Returns the same 64 bytes if valid or throws JsError if invalid.
*/
export function ed25519_signature_from_bytes(bytes: Uint8Array): Uint8Array;
/**
* WASM-exposed function to sign a message using Ed25519.
* - `message`: Raw bytes to sign
* - `secret`: Raw Ed25519 signing key bytes
* Returns base58-encoded signature with "signature_z" prefix or throws JsError if signing fails.
*/
export function sign(message: Uint8Array, secret: Uint8Array): string;
/**
* WASM-exposed function to verify an Ed25519 signature.
* - `signature`: Raw signature bytes
* - `message`: Raw bytes that were signed
* - `id`: Raw Ed25519 verifying key bytes
* Returns true if signature is valid, false otherwise, or throws JsError if verification fails.
*/
export function verify(signature: Uint8Array, message: Uint8Array, id: Uint8Array): boolean;
/**
* WASM-exposed function to derive a signer ID from a signing key.
* - `secret`: Raw Ed25519 signing key bytes
* Returns base58-encoded verifying key with "signer_z" prefix or throws JsError if derivation fails.
*/
export function get_signer_id(secret: Uint8Array): string;
/**
* Generate a 24-byte nonce from input material using BLAKE3.
* - `nonce_material`: Raw bytes to derive the nonce from
* Returns 24 bytes suitable for use as a nonce in cryptographic operations.
* This function is deterministic - the same input will produce the same nonce.
*/
export function generate_nonce(nonce_material: Uint8Array): Uint8Array;
/**
* Hash data once using BLAKE3.
* - `data`: Raw bytes to hash
* Returns 32 bytes of hash output.
* This is the simplest way to compute a BLAKE3 hash of a single piece of data.
*/
export function blake3_hash_once(data: Uint8Array): Uint8Array;
/**
* Hash data once using BLAKE3 with a context prefix.
* - `data`: Raw bytes to hash
* - `context`: Context bytes to prefix to the data
* Returns 32 bytes of hash output.
* This is useful for domain separation - the same data hashed with different contexts will produce different outputs.
*/
export function blake3_hash_once_with_context(data: Uint8Array, context: Uint8Array): Uint8Array;
/**
* Get an empty BLAKE3 state for incremental hashing.
* Returns a new Blake3Hasher instance for incremental hashing.
*/
export function blake3_empty_state(): Blake3Hasher;
/**
* Update a BLAKE3 state with new data for incremental hashing.
* - `state`: Current Blake3Hasher instance
* - `data`: New data to incorporate into the hash
* Returns the updated Blake3Hasher.
*/
export function blake3_update_state(state: Blake3Hasher, data: Uint8Array): void;
/**
* Get the final hash from a BLAKE3 state.
* - `state`: The Blake3Hasher to finalize
* Returns 32 bytes of hash output.
* This finalizes an incremental hashing operation.
*/
export function blake3_digest_for_state(state: Blake3Hasher): Uint8Array;
/**
* Generate a new X25519 private key using secure random number generation.
* Returns 32 bytes of raw key material suitable for use with other X25519 functions.
* This key can be reused for multiple Diffie-Hellman exchanges.
*/
export function new_x25519_private_key(): Uint8Array;
/**
* WASM-exposed function to derive an X25519 public key from a private key.
* - `private_key`: 32 bytes of private key material
* Returns 32 bytes of public key material or throws JsError if key is invalid.
*/
export function x25519_public_key(private_key: Uint8Array): Uint8Array;
/**
* WASM-exposed function to perform X25519 Diffie-Hellman key exchange.
* - `private_key`: 32 bytes of private key material
* - `public_key`: 32 bytes of public key material
* Returns 32 bytes of shared secret material or throws JsError if key exchange fails.
*/
export function x25519_diffie_hellman(private_key: Uint8Array, public_key: Uint8Array): Uint8Array;
/**
* WASM-exposed function to derive a sealer ID from a sealer secret.
* - `secret`: Raw bytes of the sealer secret
* Returns a base58-encoded sealer ID with "sealer_z" prefix or throws JsError if derivation fails.
*/
export function get_sealer_id(secret: Uint8Array): string;
/**
* WASM-exposed function for sealing a message using X25519 + XSalsa20-Poly1305.
* Provides authenticated encryption with perfect forward secrecy.
* - `message`: Raw bytes to seal
* - `sender_secret`: Base58-encoded sender's private key with "sealerSecret_z" prefix
* - `recipient_id`: Base58-encoded recipient's public key with "sealer_z" prefix
* - `nonce_material`: Raw bytes used to generate the nonce
* Returns sealed bytes or throws JsError if sealing fails.
*/
export function seal(message: Uint8Array, sender_secret: string, recipient_id: string, nonce_material: Uint8Array): Uint8Array;
/**
* WASM-exposed function for unsealing a message using X25519 + XSalsa20-Poly1305.
* Provides authenticated decryption with perfect forward secrecy.
* - `sealed_message`: The sealed bytes to decrypt
* - `recipient_secret`: Base58-encoded recipient's private key with "sealerSecret_z" prefix
* - `sender_id`: Base58-encoded sender's public key with "sealer_z" prefix
* - `nonce_material`: Raw bytes used to generate the nonce (must match sealing)
* Returns unsealed bytes or throws JsError if unsealing fails.
*/
export function unseal(sealed_message: Uint8Array, recipient_secret: string, sender_id: string, nonce_material: Uint8Array): Uint8Array;
/**
* WASM-exposed function to encrypt bytes with a key secret and nonce material.
* - `value`: The raw bytes to encrypt
* - `key_secret`: A base58-encoded key secret with "keySecret_z" prefix
* - `nonce_material`: Raw bytes used to generate the nonce
* Returns the encrypted bytes or throws a JsError if encryption fails.
*/
export function encrypt(value: Uint8Array, key_secret: string, nonce_material: Uint8Array): Uint8Array;
/**
* WASM-exposed function to decrypt bytes with a key secret and nonce material.
* - `ciphertext`: The encrypted bytes to decrypt
* - `key_secret`: A base58-encoded key secret with "keySecret_z" prefix
* - `nonce_material`: Raw bytes used to generate the nonce (must match encryption)
* Returns the decrypted bytes or throws a JsError if decryption fails.
*/
export function decrypt(ciphertext: Uint8Array, key_secret: string, nonce_material: Uint8Array): Uint8Array;
export class Blake3Hasher {
free(): void;
constructor();
update(data: Uint8Array): void;
finalize(): Uint8Array;
clone(): Blake3Hasher;
}
export class SessionLog {
free(): void;
constructor(co_id: string, session_id: string, signer_id?: string | null);
clone(): SessionLog;
tryAdd(transactions_json: string[], new_signature_str: string, skip_verify: boolean): void;
addNewPrivateTransaction(changes_json: string, signer_secret: string, encryption_key: string, key_id: string, made_at: number): string;
addNewTrustingTransaction(changes_json: string, signer_secret: string, made_at: number): string;
decryptNextTransactionChangesJson(tx_index: number, encryption_key: string): string;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly decrypt_xsalsa20: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
readonly encrypt_xsalsa20: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
readonly __wbg_sessionlog_free: (a: number, b: number) => void;
readonly sessionlog_new: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
readonly sessionlog_clone: (a: number) => number;
readonly sessionlog_tryAdd: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
readonly sessionlog_addNewPrivateTransaction: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number, number];
readonly sessionlog_addNewTrustingTransaction: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
readonly sessionlog_decryptNextTransactionChangesJson: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly new_ed25519_signing_key: () => [number, number];
readonly ed25519_sign: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly ed25519_verify: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
readonly ed25519_signing_key_from_bytes: (a: number, b: number) => [number, number, number, number];
readonly ed25519_signing_key_to_public: (a: number, b: number) => [number, number, number, number];
readonly ed25519_verifying_key_from_bytes: (a: number, b: number) => [number, number, number, number];
readonly ed25519_signature_from_bytes: (a: number, b: number) => [number, number, number, number];
readonly ed25519_verifying_key: (a: number, b: number) => [number, number, number, number];
readonly ed25519_signing_key_sign: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly sign: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly verify: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
readonly get_signer_id: (a: number, b: number) => [number, number, number, number];
readonly generate_nonce: (a: number, b: number) => [number, number];
readonly blake3_hash_once: (a: number, b: number) => [number, number];
readonly blake3_hash_once_with_context: (a: number, b: number, c: number, d: number) => [number, number];
readonly __wbg_blake3hasher_free: (a: number, b: number) => void;
readonly blake3hasher_finalize: (a: number) => [number, number];
readonly blake3hasher_clone: (a: number) => number;
readonly blake3_empty_state: () => number;
readonly blake3_update_state: (a: number, b: number, c: number) => void;
readonly blake3_digest_for_state: (a: number) => [number, number];
readonly blake3hasher_update: (a: number, b: number, c: number) => void;
readonly blake3hasher_new: () => number;
readonly new_x25519_private_key: () => [number, number];
readonly x25519_public_key: (a: number, b: number) => [number, number, number, number];
readonly x25519_diffie_hellman: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly get_sealer_id: (a: number, b: number) => [number, number, number, number];
readonly seal: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number];
readonly unseal: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number];
readonly encrypt: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
readonly decrypt: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_4: WebAssembly.Table;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
export const data: string;

File diff suppressed because one or more lines are too long

View File

@@ -1,240 +0,0 @@
use crate::error::CryptoError;
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use rand::rngs::OsRng;
use wasm_bindgen::prelude::*;
/// Generate a new Ed25519 signing key using secure random number generation.
/// Returns 32 bytes of raw key material suitable for use with other Ed25519 functions.
#[wasm_bindgen]
pub fn new_ed25519_signing_key() -> Box<[u8]> {
let mut rng = OsRng;
let signing_key = SigningKey::generate(&mut rng);
signing_key.to_bytes().into()
}
/// Internal function to derive an Ed25519 verifying key from a signing key.
/// Takes 32 bytes of signing key material and returns 32 bytes of verifying key material.
/// Returns CryptoError if the key length is invalid.
pub(crate) fn ed25519_verifying_key_internal(signing_key: &[u8]) -> Result<Box<[u8]>, CryptoError> {
let key_bytes: [u8; 32] = signing_key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, signing_key.len()))?;
let signing_key = SigningKey::from_bytes(&key_bytes);
Ok(signing_key.verifying_key().to_bytes().into())
}
/// WASM-exposed function to derive an Ed25519 verifying key from a signing key.
/// - `signing_key`: 32 bytes of signing key material
/// Returns 32 bytes of verifying key material or throws JsError if key is invalid.
#[wasm_bindgen]
pub fn ed25519_verifying_key(signing_key: &[u8]) -> Result<Box<[u8]>, JsError> {
ed25519_verifying_key_internal(signing_key).map_err(|e| JsError::new(&e.to_string()))
}
/// Internal function to sign a message using Ed25519.
/// Takes 32 bytes of signing key material and arbitrary message bytes.
/// Returns 64 bytes of signature material or CryptoError if key is invalid.
pub(crate) fn ed25519_sign_internal(
signing_key: &[u8],
message: &[u8],
) -> Result<[u8; 64], CryptoError> {
let key_bytes: [u8; 32] = signing_key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, signing_key.len()))?;
let signing_key = SigningKey::from_bytes(&key_bytes);
Ok(signing_key.sign(message).to_bytes())
}
/// WASM-exposed function to sign a message using Ed25519.
/// - `signing_key`: 32 bytes of signing key material
/// - `message`: Raw bytes to sign
/// Returns 64 bytes of signature material or throws JsError if signing fails.
#[wasm_bindgen]
pub fn ed25519_sign(signing_key: &[u8], message: &[u8]) -> Result<Box<[u8]>, JsError> {
Ok(ed25519_sign_internal(signing_key, message)?.into())
}
/// Internal function to verify an Ed25519 signature.
/// - `verifying_key`: 32 bytes of verifying key material
/// - `message`: Raw bytes that were signed
/// - `signature`: 64 bytes of signature material
/// Returns true if signature is valid, false otherwise, or CryptoError if key/signature format is invalid.
pub(crate) fn ed25519_verify_internal(
verifying_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<bool, CryptoError> {
let key_bytes: [u8; 32] = verifying_key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, verifying_key.len()))?;
let verifying_key = VerifyingKey::from_bytes(&key_bytes)
.map_err(|e| CryptoError::InvalidVerifyingKey(e.to_string()))?;
let sig_bytes: [u8; 64] = signature
.try_into()
.map_err(|_| CryptoError::InvalidSignatureLength)?;
let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
Ok(verifying_key.verify(message, &signature).is_ok())
}
/// WASM-exposed function to verify an Ed25519 signature.
/// - `verifying_key`: 32 bytes of verifying key material
/// - `message`: Raw bytes that were signed
/// - `signature`: 64 bytes of signature material
/// Returns true if signature is valid, false otherwise, or throws JsError if verification fails.
#[wasm_bindgen]
pub fn ed25519_verify(
verifying_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<bool, JsError> {
ed25519_verify_internal(verifying_key, message, signature)
.map_err(|e| JsError::new(&e.to_string()))
}
/// WASM-exposed function to validate and copy Ed25519 signing key bytes.
/// - `bytes`: 32 bytes of signing key material to validate
/// Returns the same 32 bytes if valid or throws JsError if invalid.
#[wasm_bindgen]
pub fn ed25519_signing_key_from_bytes(bytes: &[u8]) -> Result<Box<[u8]>, JsError> {
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| JsError::new("Invalid signing key length"))?;
Ok(key_bytes.into())
}
/// WASM-exposed function to derive the public key from an Ed25519 signing key.
/// - `signing_key`: 32 bytes of signing key material
/// Returns 32 bytes of public key material or throws JsError if key is invalid.
#[wasm_bindgen]
pub fn ed25519_signing_key_to_public(signing_key: &[u8]) -> Result<Box<[u8]>, JsError> {
ed25519_verifying_key_internal(signing_key).map_err(|e| JsError::new(&e.to_string()))
}
/// WASM-exposed function to sign a message with an Ed25519 signing key.
/// - `signing_key`: 32 bytes of signing key material
/// - `message`: Raw bytes to sign
/// Returns 64 bytes of signature material or throws JsError if signing fails.
#[wasm_bindgen]
pub fn ed25519_signing_key_sign(signing_key: &[u8], message: &[u8]) -> Result<Box<[u8]>, JsError> {
Ok(ed25519_sign_internal(signing_key, message)?.into())
}
/// WASM-exposed function to validate and copy Ed25519 verifying key bytes.
/// - `bytes`: 32 bytes of verifying key material to validate
/// Returns the same 32 bytes if valid or throws JsError if invalid.
#[wasm_bindgen]
pub fn ed25519_verifying_key_from_bytes(bytes: &[u8]) -> Result<Box<[u8]>, JsError> {
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| JsError::new("Invalid verifying key length"))?;
Ok(key_bytes.into())
}
/// WASM-exposed function to validate and copy Ed25519 signature bytes.
/// - `bytes`: 64 bytes of signature material to validate
/// Returns the same 64 bytes if valid or throws JsError if invalid.
#[wasm_bindgen]
pub fn ed25519_signature_from_bytes(bytes: &[u8]) -> Result<Box<[u8]>, JsError> {
let sig_bytes: [u8; 64] = bytes
.try_into()
.map_err(|_| JsError::new("Invalid signature length"))?;
Ok(sig_bytes.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ed25519_key_generation_and_signing() {
// Test key generation
let signing_key = new_ed25519_signing_key();
assert_eq!(signing_key.len(), 32, "Signing key should be 32 bytes");
// Test verifying key derivation
let verifying_key = ed25519_verifying_key_internal(&signing_key).unwrap();
assert_eq!(verifying_key.len(), 32, "Verifying key should be 32 bytes");
// Test that different signing keys produce different verifying keys
let signing_key2 = new_ed25519_signing_key();
let verifying_key2 = ed25519_verifying_key_internal(&signing_key2).unwrap();
assert_ne!(
verifying_key, verifying_key2,
"Different signing keys should produce different verifying keys"
);
// Test signing and verification
let message = b"Test message";
let signature = ed25519_sign_internal(&signing_key, message).unwrap();
assert_eq!(signature.len(), 64, "Signature should be 64 bytes");
// Test successful verification
let verification_result =
ed25519_verify_internal(&verifying_key, message, &signature).unwrap();
assert!(
verification_result,
"Valid signature should verify successfully"
);
// Test verification with wrong message
let wrong_message = b"Wrong message";
let wrong_verification =
ed25519_verify_internal(&verifying_key, wrong_message, &signature).unwrap();
assert!(
!wrong_verification,
"Signature should not verify with wrong message"
);
// Test verification with wrong key
let wrong_verification =
ed25519_verify_internal(&verifying_key2, message, &signature).unwrap();
assert!(
!wrong_verification,
"Signature should not verify with wrong key"
);
// Test verification with tampered signature
let mut tampered_signature = signature.clone();
tampered_signature[0] ^= 1;
let wrong_verification =
ed25519_verify_internal(&verifying_key, message, &tampered_signature).unwrap();
assert!(!wrong_verification, "Tampered signature should not verify");
}
#[test]
fn test_ed25519_error_cases() {
// Test invalid signing key length
let invalid_signing_key = vec![0u8; 31]; // Too short
let result = ed25519_verifying_key_internal(&invalid_signing_key);
assert!(result.is_err());
let result = ed25519_sign_internal(&invalid_signing_key, b"test");
assert!(result.is_err());
// Test invalid verifying key length
let invalid_verifying_key = vec![0u8; 31]; // Too short
let valid_signing_key = new_ed25519_signing_key();
let valid_signature = ed25519_sign_internal(&valid_signing_key, b"test").unwrap();
let result = ed25519_verify_internal(&invalid_verifying_key, b"test", &valid_signature);
assert!(result.is_err());
// Test invalid signature length
let valid_verifying_key = ed25519_verifying_key_internal(&valid_signing_key).unwrap();
let invalid_signature = vec![0u8; 63]; // Too short
let result = ed25519_verify_internal(&valid_verifying_key, b"test", &invalid_signature);
assert!(result.is_err());
// Test with too long keys
let too_long_key = vec![0u8; 33]; // Too long
let result = ed25519_verifying_key_internal(&too_long_key);
assert!(result.is_err());
let result = ed25519_sign_internal(&too_long_key, b"test");
assert!(result.is_err());
// Test with too long signature
let too_long_signature = vec![0u8; 65]; // Too long
let result = ed25519_verify_internal(&valid_verifying_key, b"test", &too_long_signature);
assert!(result.is_err());
}
}

View File

@@ -1,113 +0,0 @@
use crate::error::CryptoError;
use crate::hash::blake3::generate_nonce;
use bs58;
use wasm_bindgen::prelude::*;
/// Internal function to encrypt bytes with a key secret and nonce material.
/// Takes a base58-encoded key secret with "keySecret_z" prefix and raw nonce material.
/// Returns the encrypted bytes or a CryptoError if the key format is invalid.
pub fn encrypt_internal(
plaintext: &[u8],
key_secret: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Decode the base58 key secret (removing the "keySecret_z" prefix)
let key_secret = key_secret
.strip_prefix("keySecret_z")
.ok_or(CryptoError::InvalidPrefix("key secret", "keySecret_z"))?;
let key = bs58::decode(key_secret)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
// Generate nonce from nonce material
let nonce = generate_nonce(nonce_material);
// Encrypt using XSalsa20
Ok(super::xsalsa20::encrypt_xsalsa20_raw_internal(&key, &nonce, plaintext)?.into())
}
/// Internal function to decrypt bytes with a key secret and nonce material.
/// Takes a base58-encoded key secret with "keySecret_z" prefix and raw nonce material.
/// Returns the decrypted bytes or a CryptoError if the key format is invalid.
pub fn decrypt_internal(
ciphertext: &[u8],
key_secret: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Decode the base58 key secret (removing the "keySecret_z" prefix)
let key_secret = key_secret
.strip_prefix("keySecret_z")
.ok_or(CryptoError::InvalidPrefix("key secret", "keySecret_z"))?;
let key = bs58::decode(key_secret)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
// Generate nonce from nonce material
let nonce = generate_nonce(nonce_material);
// Decrypt using XSalsa20
Ok(super::xsalsa20::decrypt_xsalsa20_raw_internal(&key, &nonce, ciphertext)?.into())
}
/// WASM-exposed function to encrypt bytes with a key secret and nonce material.
/// - `value`: The raw bytes to encrypt
/// - `key_secret`: A base58-encoded key secret with "keySecret_z" prefix
/// - `nonce_material`: Raw bytes used to generate the nonce
/// Returns the encrypted bytes or throws a JsError if encryption fails.
#[wasm_bindgen(js_name = encrypt)]
pub fn encrypt(
value: &[u8],
key_secret: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, JsError> {
encrypt_internal(value, key_secret, nonce_material).map_err(|e| JsError::new(&e.to_string()))
}
/// WASM-exposed function to decrypt bytes with a key secret and nonce material.
/// - `ciphertext`: The encrypted bytes to decrypt
/// - `key_secret`: A base58-encoded key secret with "keySecret_z" prefix
/// - `nonce_material`: Raw bytes used to generate the nonce (must match encryption)
/// Returns the decrypted bytes or throws a JsError if decryption fails.
#[wasm_bindgen(js_name = decrypt)]
pub fn decrypt(
ciphertext: &[u8],
key_secret: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, JsError> {
Ok(decrypt_internal(ciphertext, key_secret, nonce_material)?.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt() {
// Test data
let plaintext = b"Hello, World!";
let key_secret = "keySecret_z11111111111111111111111111111111"; // Example base58 encoded key
let nonce_material = b"test_nonce_material";
// Test encryption
let ciphertext = encrypt_internal(plaintext, key_secret, nonce_material).unwrap();
assert!(!ciphertext.is_empty());
// Test decryption
let decrypted = decrypt_internal(&ciphertext, key_secret, nonce_material).unwrap();
assert_eq!(&*decrypted, plaintext);
}
#[test]
fn test_invalid_key_secret() {
let plaintext = b"test";
let nonce_material = b"nonce";
// Test with invalid key secret format
let result = encrypt_internal(plaintext, "invalid_key", nonce_material);
assert!(result.is_err());
// Test with invalid base58 encoding
let result = encrypt_internal(plaintext, "keySecret_z!!!!", nonce_material);
assert!(result.is_err());
}
}

View File

@@ -1,200 +0,0 @@
use crate::crypto::x25519::x25519_diffie_hellman_internal;
use crate::crypto::xsalsa20::{decrypt_xsalsa20_poly1305, encrypt_xsalsa20_poly1305};
use crate::error::CryptoError;
use crate::hash::blake3::generate_nonce;
use bs58;
use wasm_bindgen::prelude::*;
/// Internal function to seal a message using X25519 + XSalsa20-Poly1305.
/// - `message`: Raw bytes to seal
/// - `sender_secret`: Base58-encoded sender's private key with "sealerSecret_z" prefix
/// - `recipient_id`: Base58-encoded recipient's public key with "sealer_z" prefix
/// - `nonce_material`: Raw bytes used to generate the nonce
/// Returns sealed bytes or CryptoError if key formats are invalid.
///
/// The sealing process:
/// 1. Decode base58 keys and validate prefixes
/// 2. Generate shared secret using X25519 key exchange
/// 3. Generate nonce from nonce material using BLAKE3
/// 4. Encrypt message using XSalsa20-Poly1305 with the shared secret
pub fn seal_internal(
message: &[u8],
sender_secret: &str,
recipient_id: &str,
nonce_material: &[u8],
) -> Result<Vec<u8>, CryptoError> {
// Decode the base58 sender secret (removing the "sealerSecret_z" prefix)
let sender_secret =
sender_secret
.strip_prefix("sealerSecret_z")
.ok_or(CryptoError::InvalidPrefix(
"sealer secret",
"sealerSecret_z",
))?;
let sender_private_key = bs58::decode(sender_secret)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
// Decode the base58 recipient ID (removing the "sealer_z" prefix)
let recipient_id = recipient_id
.strip_prefix("sealer_z")
.ok_or(CryptoError::InvalidPrefix("sealer ID", "sealer_z"))?;
let recipient_public_key = bs58::decode(recipient_id)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
let nonce = generate_nonce(nonce_material);
// Generate shared secret using X25519
let shared_secret = x25519_diffie_hellman_internal(&sender_private_key, &recipient_public_key)?;
// Encrypt message using XSalsa20-Poly1305
Ok(encrypt_xsalsa20_poly1305(&shared_secret, &nonce, message)?.into())
}
/// Internal function to unseal a message using X25519 + XSalsa20-Poly1305.
/// - `sealed_message`: The sealed bytes to decrypt
/// - `recipient_secret`: Base58-encoded recipient's private key with "sealerSecret_z" prefix
/// - `sender_id`: Base58-encoded sender's public key with "sealer_z" prefix
/// - `nonce_material`: Raw bytes used to generate the nonce (must match sealing)
/// Returns unsealed bytes or CryptoError if key formats are invalid or authentication fails.
///
/// The unsealing process:
/// 1. Decode base58 keys and validate prefixes
/// 2. Generate shared secret using X25519 key exchange
/// 3. Generate nonce from nonce material using BLAKE3
/// 4. Decrypt and authenticate message using XSalsa20-Poly1305 with the shared secret
fn unseal_internal(
sealed_message: &[u8],
recipient_secret: &str,
sender_id: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Decode the base58 recipient secret (removing the "sealerSecret_z" prefix)
let recipient_secret =
recipient_secret
.strip_prefix("sealerSecret_z")
.ok_or(CryptoError::InvalidPrefix(
"sealer secret",
"sealerSecret_z",
))?;
let recipient_private_key = bs58::decode(recipient_secret)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
// Decode the base58 sender ID (removing the "sealer_z" prefix)
let sender_id = sender_id
.strip_prefix("sealer_z")
.ok_or(CryptoError::InvalidPrefix("sealer ID", "sealer_z"))?;
let sender_public_key = bs58::decode(sender_id)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
let nonce = generate_nonce(nonce_material);
// Generate shared secret using X25519
let shared_secret = x25519_diffie_hellman_internal(&recipient_private_key, &sender_public_key)?;
// Decrypt message using XSalsa20-Poly1305
Ok(decrypt_xsalsa20_poly1305(&shared_secret, &nonce, sealed_message)?.into())
}
/// WASM-exposed function for sealing a message using X25519 + XSalsa20-Poly1305.
/// Provides authenticated encryption with perfect forward secrecy.
/// - `message`: Raw bytes to seal
/// - `sender_secret`: Base58-encoded sender's private key with "sealerSecret_z" prefix
/// - `recipient_id`: Base58-encoded recipient's public key with "sealer_z" prefix
/// - `nonce_material`: Raw bytes used to generate the nonce
/// Returns sealed bytes or throws JsError if sealing fails.
#[wasm_bindgen(js_name = seal)]
pub fn seal(
message: &[u8],
sender_secret: &str,
recipient_id: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, JsError> {
Ok(seal_internal(message, sender_secret, recipient_id, nonce_material)?.into())
}
/// WASM-exposed function for unsealing a message using X25519 + XSalsa20-Poly1305.
/// Provides authenticated decryption with perfect forward secrecy.
/// - `sealed_message`: The sealed bytes to decrypt
/// - `recipient_secret`: Base58-encoded recipient's private key with "sealerSecret_z" prefix
/// - `sender_id`: Base58-encoded sender's public key with "sealer_z" prefix
/// - `nonce_material`: Raw bytes used to generate the nonce (must match sealing)
/// Returns unsealed bytes or throws JsError if unsealing fails.
#[wasm_bindgen(js_name = unseal)]
pub fn unseal(
sealed_message: &[u8],
recipient_secret: &str,
sender_id: &str,
nonce_material: &[u8],
) -> Result<Box<[u8]>, JsError> {
Ok(unseal_internal(sealed_message, recipient_secret, sender_id, nonce_material)?.into())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::x25519::{new_x25519_private_key, x25519_public_key_internal};
#[test]
fn test_seal_unseal() {
// Generate real keys
let sender_private = new_x25519_private_key();
let sender_public = x25519_public_key_internal(&sender_private).unwrap();
// Encode keys with proper prefixes
let sender_secret = format!(
"sealerSecret_z{}",
bs58::encode(&sender_private).into_string()
);
let recipient_id = format!("sealer_z{}", bs58::encode(&sender_public).into_string());
// Test data
let message = b"Secret message";
let nonce_material = b"test_nonce_material";
// Test sealing
let sealed = seal_internal(message, &sender_secret, &recipient_id, nonce_material).unwrap();
assert!(!sealed.is_empty());
// Test unsealing (using same keys since it's a test)
let unsealed =
unseal_internal(&sealed, &sender_secret, &recipient_id, nonce_material).unwrap();
assert_eq!(&*unsealed, message);
}
#[test]
fn test_invalid_keys() {
let message = b"test";
let nonce_material = b"nonce";
// Test with invalid sender secret format
let result = seal_internal(
message,
"invalid_key",
"sealer_z22222222222222222222222222222222",
nonce_material,
);
assert!(result.is_err());
// Test with invalid recipient ID format
let result = seal_internal(
message,
"sealerSecret_z11111111111111111111111111111111",
"invalid_key",
nonce_material,
);
assert!(result.is_err());
// Test with invalid base58 encoding
let result = seal_internal(
message,
"sealerSecret_z!!!!",
"sealer_z22222222222222222222222222222222",
nonce_material,
);
assert!(result.is_err());
}
}

View File

@@ -1,184 +0,0 @@
use crate::crypto::ed25519::{
ed25519_sign_internal, ed25519_verify_internal, ed25519_verifying_key_internal,
};
use crate::error::CryptoError;
use bs58;
use wasm_bindgen::prelude::*;
/// Internal function to sign a message using Ed25519.
/// - `message`: Raw bytes to sign
/// - `secret`: Base58-encoded signing key with "signerSecret_z" prefix
/// Returns base58-encoded signature with "signature_z" prefix or error string.
pub fn sign_internal(message: &[u8], secret: &str) -> Result<String, CryptoError> {
let secret_bytes = bs58::decode(secret.strip_prefix("signerSecret_z").ok_or(
CryptoError::InvalidPrefix("signer secret", "signerSecret_z"),
)?)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
let signature = ed25519_sign_internal(&secret_bytes, message)
.map_err(|e| CryptoError::InvalidVerifyingKey(e.to_string()))?;
Ok(format!(
"signature_z{}",
bs58::encode(signature).into_string()
))
}
/// Internal function to verify an Ed25519 signature.
/// - `signature`: Base58-encoded signature with "signature_z" prefix
/// - `message`: Raw bytes that were signed
/// - `id`: Base58-encoded verifying key with "signer_z" prefix
/// Returns true if signature is valid, false otherwise, or error string if formats are invalid.
pub fn verify_internal(signature: &str, message: &[u8], id: &str) -> Result<bool, CryptoError> {
let signature_bytes = bs58::decode(
signature
.strip_prefix("signature_z")
.ok_or(CryptoError::InvalidPrefix("signature_z", "signature"))?,
)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
let verifying_key = bs58::decode(
id.strip_prefix("signer_z")
.ok_or(CryptoError::InvalidPrefix("signer_z", "signer ID"))?,
)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
ed25519_verify_internal(&verifying_key, message, &signature_bytes)
.map_err(|e| CryptoError::InvalidVerifyingKey(e.to_string()))
}
/// Internal function to derive a signer ID from a signing key.
/// - `secret`: Base58-encoded signing key with "signerSecret_z" prefix
/// Returns base58-encoded verifying key with "signer_z" prefix or error string.
pub fn get_signer_id_internal(secret: &str) -> Result<String, CryptoError> {
let secret_bytes = bs58::decode(secret.strip_prefix("signerSecret_z").ok_or(
CryptoError::InvalidPrefix("signerSecret_z", "signer secret"),
)?)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
let verifying_key = ed25519_verifying_key_internal(&secret_bytes)
.map_err(|e| CryptoError::InvalidVerifyingKey(e.to_string()))?;
Ok(format!(
"signer_z{}",
bs58::encode(verifying_key).into_string()
))
}
/// WASM-exposed function to sign a message using Ed25519.
/// - `message`: Raw bytes to sign
/// - `secret`: Raw Ed25519 signing key bytes
/// Returns base58-encoded signature with "signature_z" prefix or throws JsError if signing fails.
#[wasm_bindgen(js_name = sign)]
pub fn sign(message: &[u8], secret: &[u8]) -> Result<String, JsError> {
let secret_str = std::str::from_utf8(secret)
.map_err(|e| JsError::new(&format!("Invalid UTF-8 in secret: {:?}", e)))?;
sign_internal(message, secret_str).map_err(|e| JsError::new(&e.to_string()))
}
/// WASM-exposed function to verify an Ed25519 signature.
/// - `signature`: Raw signature bytes
/// - `message`: Raw bytes that were signed
/// - `id`: Raw Ed25519 verifying key bytes
/// Returns true if signature is valid, false otherwise, or throws JsError if verification fails.
#[wasm_bindgen(js_name = verify)]
pub fn verify(signature: &[u8], message: &[u8], id: &[u8]) -> Result<bool, JsError> {
let signature_str = std::str::from_utf8(signature)
.map_err(|e| JsError::new(&format!("Invalid UTF-8 in signature: {:?}", e)))?;
let id_str = std::str::from_utf8(id)
.map_err(|e| JsError::new(&format!("Invalid UTF-8 in id: {:?}", e)))?;
verify_internal(signature_str, message, id_str).map_err(|e| JsError::new(&e.to_string()))
}
/// WASM-exposed function to derive a signer ID from a signing key.
/// - `secret`: Raw Ed25519 signing key bytes
/// Returns base58-encoded verifying key with "signer_z" prefix or throws JsError if derivation fails.
#[wasm_bindgen(js_name = get_signer_id)]
pub fn get_signer_id(secret: &[u8]) -> Result<String, JsError> {
let secret_str = std::str::from_utf8(secret)
.map_err(|e| JsError::new(&format!("Invalid UTF-8 in secret: {:?}", e)))?;
get_signer_id_internal(secret_str).map_err(|e| JsError::new(&e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::ed25519::new_ed25519_signing_key;
#[test]
fn test_sign_and_verify() {
let message = b"hello world";
// Create a test signing key
let signing_key = new_ed25519_signing_key();
let secret = format!("signerSecret_z{}", bs58::encode(&signing_key).into_string());
// Sign the message
let signature = sign_internal(message, &secret).unwrap();
// Get the public key for verification
let secret_bytes = bs58::decode(secret.strip_prefix("signerSecret_z").unwrap())
.into_vec()
.unwrap();
let verifying_key = ed25519_verifying_key_internal(&secret_bytes).unwrap();
let signer_id = format!("signer_z{}", bs58::encode(&verifying_key).into_string());
// Verify the signature
assert!(verify_internal(&signature, message, &signer_id).unwrap());
}
#[test]
fn test_invalid_inputs() {
let message = b"hello world";
// Test invalid base58 in secret
let result = sign_internal(message, "signerSecret_z!!!invalid!!!");
assert!(matches!(result, Err(CryptoError::Base58Error(_))));
// Test invalid signature format
let result = verify_internal("not_a_signature", message, "signer_z123");
assert!(matches!(
result,
Err(CryptoError::InvalidPrefix("signature_z", "signature"))
));
// Test invalid signer ID format
let result = verify_internal("signature_z123", message, "not_a_signer");
assert!(matches!(
result,
Err(CryptoError::InvalidPrefix("signer_z", "signer ID"))
));
}
#[test]
fn test_get_signer_id() {
// Create a test signing key
let signing_key = new_ed25519_signing_key();
let secret = format!("signerSecret_z{}", bs58::encode(&signing_key).into_string());
// Get signer ID
let signer_id = get_signer_id_internal(&secret).unwrap();
assert!(signer_id.starts_with("signer_z"));
// Test that same secret produces same ID
let signer_id2 = get_signer_id_internal(&secret).unwrap();
assert_eq!(signer_id, signer_id2);
// Test invalid secret format
let result = get_signer_id_internal("invalid_secret");
assert!(matches!(
result,
Err(CryptoError::InvalidPrefix(
"signerSecret_z",
"signer secret"
))
));
// Test invalid base58
let result = get_signer_id_internal("signerSecret_z!!!invalid!!!");
assert!(matches!(result, Err(CryptoError::Base58Error(_))));
}
}

View File

@@ -1,168 +0,0 @@
use crate::error::CryptoError;
use bs58;
use wasm_bindgen::prelude::*;
use x25519_dalek::{PublicKey, StaticSecret};
/// Generate a new X25519 private key using secure random number generation.
/// Returns 32 bytes of raw key material suitable for use with other X25519 functions.
/// This key can be reused for multiple Diffie-Hellman exchanges.
#[wasm_bindgen]
pub fn new_x25519_private_key() -> Vec<u8> {
let secret = StaticSecret::random();
secret.to_bytes().to_vec()
}
/// Internal function to derive an X25519 public key from a private key.
/// Takes 32 bytes of private key material and returns 32 bytes of public key material.
/// Returns CryptoError if the key length is invalid.
pub(crate) fn x25519_public_key_internal(private_key: &[u8]) -> Result<[u8; 32], CryptoError> {
let bytes: [u8; 32] = private_key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, private_key.len()))?;
let secret = StaticSecret::from(bytes);
Ok(PublicKey::from(&secret).to_bytes())
}
/// WASM-exposed function to derive an X25519 public key from a private key.
/// - `private_key`: 32 bytes of private key material
/// Returns 32 bytes of public key material or throws JsError if key is invalid.
#[wasm_bindgen]
pub fn x25519_public_key(private_key: &[u8]) -> Result<Vec<u8>, JsError> {
Ok(x25519_public_key_internal(private_key)?.to_vec())
}
/// Internal function to perform X25519 Diffie-Hellman key exchange.
/// Takes 32 bytes each of private and public key material.
/// Returns 32 bytes of shared secret material or CryptoError if key lengths are invalid.
pub(crate) fn x25519_diffie_hellman_internal(
private_key: &[u8],
public_key: &[u8],
) -> Result<[u8; 32], CryptoError> {
let private_bytes: [u8; 32] = private_key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, private_key.len()))?;
let public_bytes: [u8; 32] = public_key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, public_key.len()))?;
let secret = StaticSecret::from(private_bytes);
let public = PublicKey::from(public_bytes);
Ok(secret.diffie_hellman(&public).to_bytes())
}
/// WASM-exposed function to perform X25519 Diffie-Hellman key exchange.
/// - `private_key`: 32 bytes of private key material
/// - `public_key`: 32 bytes of public key material
/// Returns 32 bytes of shared secret material or throws JsError if key exchange fails.
#[wasm_bindgen]
pub fn x25519_diffie_hellman(private_key: &[u8], public_key: &[u8]) -> Result<Vec<u8>, JsError> {
Ok(x25519_diffie_hellman_internal(private_key, public_key)?.to_vec())
}
/// Internal function to derive a sealer ID from a sealer secret.
/// Takes a base58-encoded sealer secret with "sealerSecret_z" prefix.
/// Returns a base58-encoded sealer ID with "sealer_z" prefix or error string if format is invalid.
pub fn get_sealer_id_internal(secret: &str) -> Result<String, CryptoError> {
let private_bytes = bs58::decode(secret.strip_prefix("sealerSecret_z").ok_or(
CryptoError::InvalidPrefix("sealerSecret_z", "sealer secret"),
)?)
.into_vec()
.map_err(|e| CryptoError::Base58Error(e.to_string()))?;
let public_bytes = x25519_public_key_internal(&private_bytes)
.map_err(|e| CryptoError::InvalidPublicKey(e.to_string()))?;
Ok(format!(
"sealer_z{}",
bs58::encode(public_bytes).into_string()
))
}
/// WASM-exposed function to derive a sealer ID from a sealer secret.
/// - `secret`: Raw bytes of the sealer secret
/// Returns a base58-encoded sealer ID with "sealer_z" prefix or throws JsError if derivation fails.
#[wasm_bindgen]
pub fn get_sealer_id(secret: &[u8]) -> Result<String, JsError> {
let secret_str = std::str::from_utf8(secret)
.map_err(|e| JsError::new(&format!("Invalid UTF-8 in secret: {:?}", e)))?;
get_sealer_id_internal(secret_str).map_err(|e| JsError::new(&e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_x25519_key_generation() {
// Test that we get the correct length keys
let private_key = new_x25519_private_key();
assert_eq!(private_key.len(), 32);
// Test that public key generation works and produces correct length
let public_key = x25519_public_key_internal(&private_key).unwrap();
assert_eq!(public_key.len(), 32);
// Test that different private keys produce different public keys
let private_key2 = new_x25519_private_key();
let public_key2 = x25519_public_key_internal(&private_key2).unwrap();
assert_ne!(public_key, public_key2);
}
#[test]
fn test_x25519_key_exchange() {
// Generate sender's keypair
let sender_private = new_x25519_private_key();
let sender_public = x25519_public_key_internal(&sender_private).unwrap();
// Generate recipient's keypair
let recipient_private = new_x25519_private_key();
let recipient_public = x25519_public_key_internal(&recipient_private).unwrap();
// Test properties we expect from the shared secret
let shared_secret1 =
x25519_diffie_hellman_internal(&sender_private, &recipient_public).unwrap();
let shared_secret2 =
x25519_diffie_hellman_internal(&recipient_private, &sender_public).unwrap();
// Both sides should arrive at the same shared secret
assert_eq!(shared_secret1, shared_secret2);
// Shared secret should be 32 bytes
assert_eq!(shared_secret1.len(), 32);
// Different recipient should produce different shared secret
let other_recipient_private = new_x25519_private_key();
let other_recipient_public = x25519_public_key_internal(&other_recipient_private).unwrap();
let different_shared_secret =
x25519_diffie_hellman_internal(&sender_private, &other_recipient_public).unwrap();
assert_ne!(shared_secret1, different_shared_secret);
}
#[test]
fn test_get_sealer_id() {
// Create a test private key
let private_key = new_x25519_private_key();
let secret = format!("sealerSecret_z{}", bs58::encode(&private_key).into_string());
// Get sealer ID
let sealer_id = get_sealer_id_internal(&secret).unwrap();
assert!(sealer_id.starts_with("sealer_z"));
// Test that same secret produces same ID
let sealer_id2 = get_sealer_id_internal(&secret).unwrap();
assert_eq!(sealer_id, sealer_id2);
// Test invalid secret format
let result = get_sealer_id_internal("invalid_secret");
assert!(matches!(
result,
Err(CryptoError::InvalidPrefix(
"sealerSecret_z",
"sealer secret"
))
));
// Test invalid base58
let result = get_sealer_id_internal("sealerSecret_z!!!invalid!!!");
assert!(matches!(result, Err(CryptoError::Base58Error(_))));
}
}

View File

@@ -1,256 +0,0 @@
use crate::error::CryptoError;
use crate::hash::blake3::generate_nonce;
use crypto_secretbox::{
aead::{Aead, KeyInit},
XSalsa20Poly1305,
};
use salsa20::cipher::{KeyIvInit, StreamCipher};
use salsa20::XSalsa20;
use wasm_bindgen::prelude::*;
/// WASM-exposed function for XSalsa20 encryption without authentication.
/// - `key`: 32-byte key for encryption
/// - `nonce_material`: Raw bytes used to generate a 24-byte nonce via BLAKE3
/// - `plaintext`: Raw bytes to encrypt
/// Returns the encrypted bytes or throws a JsError if encryption fails.
/// Note: This function does not provide authentication. Use encrypt_xsalsa20_poly1305 for authenticated encryption.
#[wasm_bindgen]
pub fn encrypt_xsalsa20(
key: &[u8],
nonce_material: &[u8],
plaintext: &[u8],
) -> Result<Box<[u8]>, JsError> {
let nonce = generate_nonce(nonce_material);
Ok(encrypt_xsalsa20_raw_internal(key, &nonce, plaintext)?.into())
}
/// WASM-exposed function for XSalsa20 decryption without authentication.
/// - `key`: 32-byte key for decryption (must match encryption key)
/// - `nonce_material`: Raw bytes used to generate a 24-byte nonce (must match encryption)
/// - `ciphertext`: Encrypted bytes to decrypt
/// Returns the decrypted bytes or throws a JsError if decryption fails.
/// Note: This function does not provide authentication. Use decrypt_xsalsa20_poly1305 for authenticated decryption.
#[wasm_bindgen]
pub fn decrypt_xsalsa20(
key: &[u8],
nonce_material: &[u8],
ciphertext: &[u8],
) -> Result<Box<[u8]>, JsError> {
let nonce = generate_nonce(nonce_material);
Ok(decrypt_xsalsa20_raw_internal(key, &nonce, ciphertext)?.into())
}
/// Internal function for raw XSalsa20 encryption without nonce generation.
/// Takes a 32-byte key and 24-byte nonce directly.
/// Returns encrypted bytes or CryptoError if key/nonce lengths are invalid.
pub fn encrypt_xsalsa20_raw_internal(
key: &[u8],
nonce: &[u8],
plaintext: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Key must be 32 bytes
let key_bytes: [u8; 32] = key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, key.len()))?;
// Nonce must be 24 bytes
let nonce_bytes: [u8; 24] = nonce
.try_into()
.map_err(|_| CryptoError::InvalidNonceLength)?;
// Create cipher instance and encrypt
let mut cipher = XSalsa20::new_from_slices(&key_bytes, &nonce_bytes)
.map_err(|_| CryptoError::CipherError)?;
let mut buffer = plaintext.to_vec();
cipher.apply_keystream(&mut buffer);
Ok(buffer.into_boxed_slice())
}
/// Internal function for raw XSalsa20 decryption without nonce generation.
/// Takes a 32-byte key and 24-byte nonce directly.
/// Returns decrypted bytes or CryptoError if key/nonce lengths are invalid.
pub fn decrypt_xsalsa20_raw_internal(
key: &[u8],
nonce: &[u8],
ciphertext: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Key must be 32 bytes
let key_bytes: [u8; 32] = key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, key.len()))?;
// Nonce must be 24 bytes
let nonce_bytes: [u8; 24] = nonce
.try_into()
.map_err(|_| CryptoError::InvalidNonceLength)?;
// Create cipher instance and decrypt (XSalsa20 is symmetric)
let mut cipher = XSalsa20::new_from_slices(&key_bytes, &nonce_bytes)
.map_err(|_| CryptoError::CipherError)?;
let mut buffer = ciphertext.to_vec();
cipher.apply_keystream(&mut buffer);
Ok(buffer.into_boxed_slice())
}
/// XSalsa20-Poly1305 encryption
pub fn encrypt_xsalsa20_poly1305(
key: &[u8],
nonce: &[u8],
plaintext: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Key must be 32 bytes
let key_bytes: [u8; 32] = key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, key.len()))?;
// Nonce must be 24 bytes
let nonce_bytes: [u8; 24] = nonce
.try_into()
.map_err(|_| CryptoError::InvalidNonceLength)?;
// Create cipher instance
let cipher = XSalsa20Poly1305::new(&key_bytes.into());
// Encrypt the plaintext
cipher
.encrypt(&nonce_bytes.into(), plaintext)
.map(|v| v.into_boxed_slice())
.map_err(|_| CryptoError::WrongTag)
}
/// XSalsa20-Poly1305 decryption
pub fn decrypt_xsalsa20_poly1305(
key: &[u8],
nonce: &[u8],
ciphertext: &[u8],
) -> Result<Box<[u8]>, CryptoError> {
// Key must be 32 bytes
let key_bytes: [u8; 32] = key
.try_into()
.map_err(|_| CryptoError::InvalidKeyLength(32, key.len()))?;
// Nonce must be 24 bytes
let nonce_bytes: [u8; 24] = nonce
.try_into()
.map_err(|_| CryptoError::InvalidNonceLength)?;
// Create cipher instance
let cipher = XSalsa20Poly1305::new(&key_bytes.into());
// Decrypt the ciphertext
cipher
.decrypt(&nonce_bytes.into(), ciphertext)
.map(|v| v.into_boxed_slice())
.map_err(|_| CryptoError::WrongTag)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xsalsa20() {
// Test vectors
let key = [0u8; 32]; // All zeros key
let nonce = [0u8; 24]; // All zeros nonce
let plaintext = b"Hello, World!";
// Test encryption
let ciphertext = encrypt_xsalsa20_raw_internal(&key, &nonce, plaintext).unwrap();
assert_ne!(&*ciphertext, plaintext); // Ciphertext should be different from plaintext
// Test decryption
let decrypted = decrypt_xsalsa20_raw_internal(&key, &nonce, &ciphertext).unwrap();
assert_eq!(&*decrypted, plaintext);
// Test that different nonce produces different ciphertext
let nonce2 = [1u8; 24];
let ciphertext2 = encrypt_xsalsa20_raw_internal(&key, &nonce2, plaintext).unwrap();
assert_ne!(ciphertext, ciphertext2);
// Test that different key produces different ciphertext
let key2 = [1u8; 32];
let ciphertext3 = encrypt_xsalsa20_raw_internal(&key2, &nonce, plaintext).unwrap();
assert_ne!(ciphertext, ciphertext3);
// Test invalid key length
assert!(encrypt_xsalsa20_raw_internal(&key[..31], &nonce, plaintext).is_err());
assert!(decrypt_xsalsa20_raw_internal(&key[..31], &nonce, &ciphertext).is_err());
// Test invalid nonce length
assert!(encrypt_xsalsa20_raw_internal(&key, &nonce[..23], plaintext).is_err());
assert!(decrypt_xsalsa20_raw_internal(&key, &nonce[..23], &ciphertext).is_err());
}
#[test]
fn test_xsalsa20_error_handling() {
let key = [0u8; 32];
let nonce = [0u8; 24];
let plaintext = b"test message";
// Test encryption with invalid key length
let invalid_key = vec![0u8; 31]; // Too short
let result = encrypt_xsalsa20_raw_internal(&invalid_key, &nonce, plaintext);
assert!(result.is_err());
// Test with too long key
let too_long_key = vec![0u8; 33]; // Too long
let result = encrypt_xsalsa20_raw_internal(&too_long_key, &nonce, plaintext);
assert!(result.is_err());
// Test decryption with invalid key length
let ciphertext = encrypt_xsalsa20_raw_internal(&key, &nonce, plaintext).unwrap();
let result = decrypt_xsalsa20_raw_internal(&invalid_key, &nonce, &ciphertext);
assert!(result.is_err());
// Test decryption with too long key
let result = decrypt_xsalsa20_raw_internal(&too_long_key, &nonce, &ciphertext);
assert!(result.is_err());
// Test with invalid nonce length
let invalid_nonce = vec![0u8; 23]; // Too short
let result = encrypt_xsalsa20_raw_internal(&key, &invalid_nonce, plaintext);
assert!(result.is_err());
let result = decrypt_xsalsa20_raw_internal(&key, &invalid_nonce, &ciphertext);
assert!(result.is_err());
// Test with too long nonce
let too_long_nonce = vec![0u8; 25]; // Too long
let result = encrypt_xsalsa20_raw_internal(&key, &too_long_nonce, plaintext);
assert!(result.is_err());
let result = decrypt_xsalsa20_raw_internal(&key, &too_long_nonce, &ciphertext);
assert!(result.is_err());
}
#[test]
fn test_xsalsa20_poly1305() {
let key = [0u8; 32]; // All zeros key
let nonce = [0u8; 24]; // All zeros nonce
let plaintext = b"Hello, World!";
// Test encryption
let ciphertext = encrypt_xsalsa20_poly1305(&key, &nonce, plaintext).unwrap();
assert!(ciphertext.len() > plaintext.len()); // Should include authentication tag
// Test decryption
let decrypted = decrypt_xsalsa20_poly1305(&key, &nonce, &ciphertext).unwrap();
assert_eq!(&*decrypted, plaintext);
// Test that different nonce produces different ciphertext
let nonce2 = [1u8; 24];
let ciphertext2 = encrypt_xsalsa20_poly1305(&key, &nonce2, plaintext).unwrap();
assert_ne!(ciphertext, ciphertext2);
// Test that different key produces different ciphertext
let key2 = [1u8; 32];
let ciphertext3 = encrypt_xsalsa20_poly1305(&key2, &nonce, plaintext).unwrap();
assert_ne!(ciphertext, ciphertext3);
// Test that decryption fails with wrong key
assert!(decrypt_xsalsa20_poly1305(&key2, &nonce, &ciphertext).is_err());
// Test that decryption fails with wrong nonce
assert!(decrypt_xsalsa20_poly1305(&key, &nonce2, &ciphertext).is_err());
// Test that decryption fails with tampered ciphertext
let mut tampered = ciphertext.clone();
tampered[0] ^= 1;
assert!(decrypt_xsalsa20_poly1305(&key, &nonce, &tampered).is_err());
}
}

View File

@@ -1,43 +0,0 @@
use std::fmt;
#[derive(Debug)]
pub enum CryptoError {
InvalidKeyLength(usize, usize),
InvalidNonceLength,
InvalidSealerSecretFormat,
InvalidSignatureLength,
InvalidVerifyingKey(String),
InvalidPublicKey(String),
WrongTag,
CipherError,
InvalidPrefix(&'static str, &'static str),
Base58Error(String),
}
impl fmt::Display for CryptoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CryptoError::InvalidKeyLength(expected, actual) => {
write!(f, "Invalid key length (expected {expected}, got {actual})")
}
CryptoError::InvalidNonceLength => write!(f, "Invalid nonce length"),
CryptoError::InvalidSealerSecretFormat => {
write!(
f,
"Invalid sealer secret format: must start with 'sealerSecret_z'"
)
}
CryptoError::InvalidSignatureLength => write!(f, "Invalid signature length"),
CryptoError::InvalidVerifyingKey(e) => write!(f, "Invalid verifying key: {}", e),
CryptoError::InvalidPublicKey(e) => write!(f, "Invalid public key: {}", e),
CryptoError::WrongTag => write!(f, "Wrong tag"),
CryptoError::CipherError => write!(f, "Failed to create cipher"),
CryptoError::InvalidPrefix(prefix, field) => {
write!(f, "Invalid {} format: must start with '{}'", field, prefix)
}
CryptoError::Base58Error(e) => write!(f, "Invalid base58: {}", e),
}
}
}
impl std::error::Error for CryptoError {}

View File

@@ -1,218 +0,0 @@
use wasm_bindgen::prelude::*;
/// Generate a 24-byte nonce from input material using BLAKE3.
/// - `nonce_material`: Raw bytes to derive the nonce from
/// Returns 24 bytes suitable for use as a nonce in cryptographic operations.
/// This function is deterministic - the same input will produce the same nonce.
#[wasm_bindgen]
pub fn generate_nonce(nonce_material: &[u8]) -> Box<[u8]> {
let mut hasher = blake3::Hasher::new();
hasher.update(nonce_material);
hasher.finalize().as_bytes()[..24].into()
}
/// Hash data once using BLAKE3.
/// - `data`: Raw bytes to hash
/// Returns 32 bytes of hash output.
/// This is the simplest way to compute a BLAKE3 hash of a single piece of data.
#[wasm_bindgen]
pub fn blake3_hash_once(data: &[u8]) -> Box<[u8]> {
let mut hasher = blake3::Hasher::new();
hasher.update(data);
hasher.finalize().as_bytes().to_vec().into_boxed_slice()
}
/// Hash data once using BLAKE3 with a context prefix.
/// - `data`: Raw bytes to hash
/// - `context`: Context bytes to prefix to the data
/// Returns 32 bytes of hash output.
/// This is useful for domain separation - the same data hashed with different contexts will produce different outputs.
#[wasm_bindgen]
pub fn blake3_hash_once_with_context(data: &[u8], context: &[u8]) -> Box<[u8]> {
let mut hasher = blake3::Hasher::new();
hasher.update(context);
hasher.update(data);
hasher.finalize().as_bytes().to_vec().into_boxed_slice()
}
#[wasm_bindgen]
pub struct Blake3Hasher(blake3::Hasher);
#[wasm_bindgen]
impl Blake3Hasher {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Blake3Hasher(blake3::Hasher::new())
}
pub fn update(&mut self, data: &[u8]) {
self.0.update(data);
}
pub fn finalize(&self) -> Box<[u8]> {
self.0.finalize().as_bytes().to_vec().into_boxed_slice()
}
pub fn clone(&self) -> Self {
// The blake3::Hasher type implements Clone
Blake3Hasher(self.0.clone())
}
}
/// Get an empty BLAKE3 state for incremental hashing.
/// Returns a new Blake3Hasher instance for incremental hashing.
#[wasm_bindgen]
pub fn blake3_empty_state() -> Blake3Hasher {
Blake3Hasher::new()
}
/// Update a BLAKE3 state with new data for incremental hashing.
/// - `state`: Current Blake3Hasher instance
/// - `data`: New data to incorporate into the hash
/// Returns the updated Blake3Hasher.
#[wasm_bindgen]
pub fn blake3_update_state(state: &mut Blake3Hasher, data: &[u8]) {
state.update(data);
}
/// Get the final hash from a BLAKE3 state.
/// - `state`: The Blake3Hasher to finalize
/// Returns 32 bytes of hash output.
/// This finalizes an incremental hashing operation.
#[wasm_bindgen]
pub fn blake3_digest_for_state(state: Blake3Hasher) -> Box<[u8]> {
state.finalize()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nonce_generation() {
let input = b"test input";
let nonce = generate_nonce(input);
assert_eq!(nonce.len(), 24);
// Same input should produce same nonce
let nonce2 = generate_nonce(input);
assert_eq!(nonce, nonce2);
// Different input should produce different nonce
let nonce3 = generate_nonce(b"different input");
assert_ne!(nonce, nonce3);
}
#[test]
fn test_blake3_hash_once() {
let input = b"test input";
let hash = blake3_hash_once(input);
// BLAKE3 produces 32-byte hashes
assert_eq!(hash.len(), 32);
// Same input should produce same hash
let hash2 = blake3_hash_once(input);
assert_eq!(hash, hash2);
// Different input should produce different hash
let hash3 = blake3_hash_once(b"different input");
assert_ne!(hash, hash3);
}
#[test]
fn test_blake3_hash_once_with_context() {
let input = b"test input";
let context = b"test context";
let hash = blake3_hash_once_with_context(input, context);
// BLAKE3 produces 32-byte hashes
assert_eq!(hash.len(), 32);
// Same input and context should produce same hash
let hash2 = blake3_hash_once_with_context(input, context);
assert_eq!(hash, hash2);
// Different input should produce different hash
let hash3 = blake3_hash_once_with_context(b"different input", context);
assert_ne!(hash, hash3);
// Different context should produce different hash
let hash4 = blake3_hash_once_with_context(input, b"different context");
assert_ne!(hash, hash4);
// Hash with context should be different from hash without context
let hash_no_context = blake3_hash_once(input);
assert_ne!(hash, hash_no_context);
}
#[test]
fn test_blake3_incremental() {
// Initial state
let mut state = blake3_empty_state();
// First update with [1,2,3,4,5]
let data1 = &[1u8, 2, 3, 4, 5];
blake3_update_state(&mut state, data1);
// Check that this matches a direct hash
let direct_hash = blake3_hash_once(data1);
let state_hash = state.finalize();
assert_eq!(
state_hash, direct_hash,
"First update should match direct hash"
);
// Create new state for second test
let mut state = blake3_empty_state();
blake3_update_state(&mut state, data1);
// Verify the exact expected hash from the TypeScript test for the first update
let expected_first_hash = [
2, 79, 103, 192, 66, 90, 61, 192, 47, 186, 245, 140, 185, 61, 229, 19, 46, 61, 117,
197, 25, 250, 160, 186, 218, 33, 73, 29, 136, 201, 112, 87,
]
.to_vec()
.into_boxed_slice();
assert_eq!(
state.finalize(),
expected_first_hash,
"First update should match expected hash"
);
// Test with two updates
let mut state = blake3_empty_state();
let data1 = &[1u8, 2, 3, 4, 5];
let data2 = &[6u8, 7, 8, 9, 10];
blake3_update_state(&mut state, data1);
blake3_update_state(&mut state, data2);
// Compare with a single hash of all data
let mut all_data = Vec::new();
all_data.extend_from_slice(data1);
all_data.extend_from_slice(data2);
let direct_hash_all = blake3_hash_once(&all_data);
assert_eq!(
state.finalize(),
direct_hash_all,
"Final state should match direct hash of all data"
);
// Test final hash matches expected value
let mut state = blake3_empty_state();
blake3_update_state(&mut state, data1);
blake3_update_state(&mut state, data2);
let expected_final_hash = [
165, 131, 141, 69, 2, 69, 39, 236, 196, 244, 180, 213, 147, 124, 222, 39, 68, 223, 54,
176, 242, 97, 200, 101, 204, 79, 21, 233, 56, 51, 1, 199,
]
.to_vec()
.into_boxed_slice();
assert_eq!(
state.finalize(),
expected_final_hash,
"Final state should match expected hash"
);
}
}

View File

@@ -1,165 +0,0 @@
use cojson_core::{
CoID, CoJsonCoreError, KeyID, KeySecret, SessionID, SessionLogInternal, Signature, SignerID, SignerSecret, TransactionMode
};
use serde_json::value::RawValue;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use wasm_bindgen::prelude::*;
mod error;
pub use error::CryptoError;
pub mod hash {
pub mod blake3;
pub use blake3::*;
}
pub mod crypto {
pub mod ed25519;
pub mod encrypt;
pub mod seal;
pub mod sign;
pub mod x25519;
pub mod xsalsa20;
pub use ed25519::*;
pub use encrypt::*;
pub use seal::*;
pub use sign::*;
pub use x25519::*;
pub use xsalsa20::*;
}
#[derive(Error, Debug)]
pub enum CojsonCoreWasmError {
#[error(transparent)]
CoJson(#[from] CoJsonCoreError),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error(transparent)]
SerdeWasmBindgen(#[from] serde_wasm_bindgen::Error),
#[error("JsValue Error: {0:?}")]
Js(JsValue),
}
impl From<CojsonCoreWasmError> for JsValue {
fn from(err: CojsonCoreWasmError) -> Self {
JsValue::from_str(&err.to_string())
}
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct SessionLog {
internal: SessionLogInternal,
}
#[derive(Serialize, Deserialize)]
struct PrivateTransactionResult {
signature: String,
encrypted_changes: String,
}
#[wasm_bindgen]
impl SessionLog {
#[wasm_bindgen(constructor)]
pub fn new(co_id: String, session_id: String, signer_id: Option<String>) -> SessionLog {
let co_id = CoID(co_id);
let session_id = SessionID(session_id);
let signer_id = signer_id.map(|id| SignerID(id));
let internal = SessionLogInternal::new(co_id, session_id, signer_id);
SessionLog { internal }
}
#[wasm_bindgen(js_name = clone)]
pub fn clone_js(&self) -> SessionLog {
self.clone()
}
#[wasm_bindgen(js_name = tryAdd)]
pub fn try_add(
&mut self,
transactions_json: Vec<String>,
new_signature_str: String,
skip_verify: bool,
) -> Result<(), CojsonCoreWasmError> {
let transactions: Vec<Box<RawValue>> = transactions_json
.into_iter()
.map(|s| {
serde_json::from_str(&s).map_err(|e| {
CojsonCoreWasmError::Js(JsValue::from(format!(
"Failed to parse transaction string: {}",
e
)))
})
})
.collect::<Result<Vec<_>, _>>()?;
let new_signature = Signature(new_signature_str);
self.internal
.try_add(transactions, &new_signature, skip_verify)?;
Ok(())
}
#[wasm_bindgen(js_name = addNewPrivateTransaction)]
pub fn add_new_private_transaction(
&mut self,
changes_json: &str,
signer_secret: String,
encryption_key: String,
key_id: String,
made_at: f64,
) -> Result<String, CojsonCoreWasmError> {
let (signature, transaction) = self.internal.add_new_transaction(
changes_json,
TransactionMode::Private{key_id: KeyID(key_id), key_secret: KeySecret(encryption_key)},
&SignerSecret(signer_secret),
made_at as u64,
);
// Extract encrypted_changes from the private transaction
let encrypted_changes = match transaction {
cojson_core::Transaction::Private(private_tx) => private_tx.encrypted_changes.value,
_ => return Err(CojsonCoreWasmError::Js(JsValue::from_str("Expected private transaction"))),
};
let result = PrivateTransactionResult{
signature: signature.0,
encrypted_changes,
};
Ok(serde_json::to_string(&result)?)
}
#[wasm_bindgen(js_name = addNewTrustingTransaction)]
pub fn add_new_trusting_transaction(
&mut self,
changes_json: &str,
signer_secret: String,
made_at: f64,
) -> Result<String, CojsonCoreWasmError> {
let (signature, _) = self.internal.add_new_transaction(
changes_json,
TransactionMode::Trusting,
&SignerSecret(signer_secret),
made_at as u64,
);
Ok(signature.0)
}
#[wasm_bindgen(js_name = decryptNextTransactionChangesJson)]
pub fn decrypt_next_transaction_changes_json(
&self,
tx_index: u32,
encryption_key: String,
) -> Result<String, CojsonCoreWasmError> {
Ok(self
.internal
.decrypt_next_transaction_changes_json(tx_index, KeySecret(encryption_key))?)
}
}

View File

@@ -1,18 +0,0 @@
[package]
name = "cojson-core"
version = "0.1.0"
edition = "2021"
[dependencies]
lzy = { path = "../lzy", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
ed25519-dalek = { version = "2.2.0", features = ["rand_core"] }
bs58 = "0.5.1"
blake3 = "1.5.1"
salsa20 = "0.10.2"
base64 = "0.22.1"
thiserror = "1.0"
[dev-dependencies]
rand_core = { version = "0.6", features = ["getrandom"] }

View File

@@ -1,8 +0,0 @@
{
"coID": "co_zUsz4gkwCCWqMXa4LHXdwyAkVK3",
"signerID":"signer_z3FdM2ucYXUkbJQgPRf8R4Di6exd2sNPVaHaJHhQ8WAqi",
"knownKeys":[],
"exampleBase": {
"co_zkNajJ1BhLzR962jpzvXxx917ZB_session_zXzrQLTtp8rR":{"transactions":[{"changes":"[{\"key\":\"co_zkNajJ1BhLzR962jpzvXxx917ZB\",\"op\":\"set\",\"value\":\"admin\"}]","madeAt":1750685354142,"privacy":"trusting"},{"changes":"[{\"key\":\"key_z268nqpkZYFFWPoGzL_for_co_zkNajJ1BhLzR962jpzvXxx917ZB\",\"op\":\"set\",\"value\":\"sealed_UmZaEEzCUrP3Q-t2KrN00keV66wzA4LWadqhEmw0jlku5frSW2QyXUY3zYIC_XLig6BDS9rcZZdTm3CwnLjTPzp9hgd9TlJLf_Q==\"}]","madeAt":1750685354142,"privacy":"trusting"},{"changes":"[{\"key\":\"readKey\",\"op\":\"set\",\"value\":\"key_z268nqpkZYFFWPoGzL\"}]","madeAt":1750685354143,"privacy":"trusting"},{"changes":"[{\"key\":\"everyone\",\"op\":\"set\",\"value\":\"writer\"}]","madeAt":1750685354143,"privacy":"trusting"},{"changes":"[{\"key\":\"key_z268nqpkZYFFWPoGzL_for_everyone\",\"op\":\"set\",\"value\":\"keySecret_zHRFDaEsnpYSZh6rUAvXS8uUrKCxJAzeBPSSaVU1r9RZY\"}]","madeAt":1750685354143,"privacy":"trusting"}],"lastHash":"hash_z5j1DUZjBiTKm5XnLi8ZrNPV3P7zGuXnMNCZfh2qGXGC7","streamingHash":{"state":{"__wbg_ptr":1127736},"crypto":{}},"lastSignature":"signature_z4LoRVDLnJBfAzHvRn3avgK4RVBd7iAfqUMJdpDEtV8HGLKGAqLyweBkNp8jggcNUQZatrMeU9tdc31ct9qxw7rib","signatureAfter":{}}
}
}

View File

@@ -1,6 +0,0 @@
{
"coID": "co_zWnX74VrMP3n3dkm9wZVPszfiCw",
"signerID":"signer_z3FdM2ucYXUkbJQgPRf8R4Di6exd2sNPVaHaJHhQ8WAqi",
"knownKeys":[{"secret":"keySecret_zHRFDaEsnpYSZh6rUAvXS8uUrKCxJAzeBPSSaVU1r9RZY","id":"key_z268nqpkZYFFWPoGzL"}],
"exampleBase":{"co_zkNajJ1BhLzR962jpzvXxx917ZB_session_zXzrQLTtp8rR":{"transactions":[{"encryptedChanges":"encrypted_UxN_r7X7p-3GUE3GRGRO4NfIhEUvB01m-HaSSipRRrUsTmNBW9dZ-pkAk-NoVP_iEB0moLFbG9GDq9U9S-rUDfSPcaWCJtpE=","keyUsed":"key_z268nqpkZYFFWPoGzL","madeAt":1750685368555,"privacy":"private"}],"lastHash":"hash_zJCdoTRgDuFdUK2XogR7qgNnxezfYAVih3qve2UV65L5X","streamingHash":{"state":{"__wbg_ptr":1129680},"crypto":{}},"lastSignature":"signature_z3UErpugJAqDEYKgzUhs88xBMohzmaL228PgkNhEomf6AeVr7NYNxY17iUoCmPQTpGJNqYPo3y82mGX4oWBhkqN4y","signatureAfter":{}}}
}

View File

@@ -1,689 +0,0 @@
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
use bs58;
use ed25519_dalek::{Signature as Ed25519Signature, Signer, SigningKey, Verifier, VerifyingKey};
use salsa20::{
cipher::{KeyIvInit, StreamCipher},
XSalsa20,
};
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue, Number, Value as JsonValue};
use thiserror::Error;
// Re-export lzy for convenience
#[cfg(feature = "lzy")]
pub use lzy;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionID(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SignerID(pub String);
impl From<VerifyingKey> for SignerID {
fn from(key: VerifyingKey) -> Self {
SignerID(format!(
"signer_z{}",
bs58::encode(key.to_bytes()).into_string()
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SignerSecret(pub String);
impl From<SigningKey> for SignerSecret {
fn from(key: SigningKey) -> Self {
SignerSecret(format!(
"signerSecret_z{}",
bs58::encode(key.to_bytes()).into_string()
))
}
}
impl Into<SigningKey> for &SignerSecret {
fn into(self) -> SigningKey {
let key_bytes = decode_z(&self.0).expect("Invalid key secret");
SigningKey::from_bytes(&key_bytes.try_into().expect("Invalid key secret length"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Signature(pub String);
impl From<Ed25519Signature> for Signature {
fn from(signature: Ed25519Signature) -> Self {
Signature(format!(
"signature_z{}",
bs58::encode(signature.to_bytes()).into_string()
))
}
}
impl Into<Ed25519Signature> for &Signature {
fn into(self) -> Ed25519Signature {
let signature_bytes = decode_z(&self.0).expect("Invalid signature");
Ed25519Signature::from_bytes(
&signature_bytes
.try_into()
.expect("Invalid signature length"),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Hash(pub String);
impl From<blake3::Hash> for Hash {
fn from(hash: blake3::Hash) -> Self {
Hash(format!("hash_z{}", bs58::encode(hash.as_bytes()).into_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct KeyID(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct KeySecret(pub String);
impl Into<[u8; 32]> for &KeySecret {
fn into(self) -> [u8; 32] {
let key_bytes = decode_z(&self.0).expect("Invalid key secret");
key_bytes.try_into().expect("Invalid key secret length")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CoID(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransactionID {
#[serde(rename = "sessionID")]
pub session_id: SessionID,
#[serde(rename = "txIndex")]
pub tx_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Encrypted<T> {
pub value: String,
_phantom: std::marker::PhantomData<T>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrivateTransaction {
#[serde(rename = "encryptedChanges")]
pub encrypted_changes: Encrypted<JsonValue>,
#[serde(rename = "keyUsed")]
pub key_used: KeyID,
#[serde(rename = "madeAt")]
pub made_at: Number,
pub privacy: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustingTransaction {
pub changes: String,
#[serde(rename = "madeAt")]
pub made_at: Number,
pub privacy: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Transaction {
Private(PrivateTransaction),
Trusting(TrustingTransaction),
}
pub enum TransactionMode {
Private {
key_id: KeyID,
key_secret: KeySecret,
},
Trusting,
}
#[derive(Error, Debug)]
pub enum CoJsonCoreError {
#[error("Transaction not found at index {0}")]
TransactionNotFound(u32),
#[error("Invalid encrypted prefix in transaction")]
InvalidEncryptedPrefix,
#[error("Base64 decoding failed")]
Base64Decode(#[from] base64::DecodeError),
#[error("UTF-8 conversion failed")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("JSON deserialization failed")]
Json(#[from] serde_json::Error),
#[error("Signature verification failed: (hash: {0})")]
SignatureVerification(String),
}
#[derive(Clone)]
pub struct SessionLogInternal {
co_id: CoID,
session_id: SessionID,
public_key: Option<VerifyingKey>,
hasher: blake3::Hasher,
transactions_json: Vec<String>,
last_signature: Option<Signature>,
}
impl SessionLogInternal {
pub fn new(co_id: CoID, session_id: SessionID, signer_id: Option<SignerID>) -> Self {
let hasher = blake3::Hasher::new();
let public_key = match signer_id {
Some(signer_id) => Some(VerifyingKey::try_from(
decode_z(&signer_id.0)
.expect("Invalid public key")
.as_slice(),
)
.expect("Invalid public key")),
None => None,
};
Self {
co_id,
session_id,
public_key,
hasher,
transactions_json: Vec::new(),
last_signature: None,
}
}
pub fn transactions_json(&self) -> &Vec<String> {
&self.transactions_json
}
pub fn last_signature(&self) -> Option<&Signature> {
self.last_signature.as_ref()
}
fn expected_hash_after(&self, transactions: &[Box<RawValue>]) -> blake3::Hasher {
let mut hasher = self.hasher.clone();
for tx in transactions {
hasher.update(tx.get().as_bytes());
}
hasher
}
pub fn try_add(
&mut self,
transactions: Vec<Box<RawValue>>,
new_signature: &Signature,
skip_verify: bool,
) -> Result<(), CoJsonCoreError> {
if !skip_verify {
let hasher = self.expected_hash_after(&transactions);
let new_hash_encoded_stringified = format!(
"\"hash_z{}\"",
bs58::encode(hasher.finalize().as_bytes()).into_string()
);
if let Some(public_key) = self.public_key {
match public_key.verify(
new_hash_encoded_stringified.as_bytes(),
&(new_signature).into(),
) {
Ok(()) => {}
Err(_) => {
return Err(CoJsonCoreError::SignatureVerification(
new_hash_encoded_stringified.replace("\"", ""),
));
}
}
} else {
return Err(CoJsonCoreError::SignatureVerification(
new_hash_encoded_stringified.replace("\"", ""),
));
}
self.hasher = hasher;
}
for tx in transactions {
self.transactions_json.push(tx.get().to_string());
}
self.last_signature = Some(new_signature.clone());
Ok(())
}
pub fn add_new_transaction(
&mut self,
changes_json: &str,
mode: TransactionMode,
signer_secret: &SignerSecret,
made_at: u64,
) -> (Signature, Transaction) {
let new_tx = match mode {
TransactionMode::Private { key_id, key_secret } => {
let tx_index = self.transactions_json.len() as u32;
let nonce_material = JsonValue::Object(serde_json::Map::from_iter(vec![
("in".to_string(), JsonValue::String(self.co_id.0.clone())),
(
"tx".to_string(),
serde_json::to_value(TransactionID {
session_id: self.session_id.clone(),
tx_index,
})
.unwrap(),
),
]));
let nonce = self.generate_json_nonce(&nonce_material);
let secret_key_bytes: [u8; 32] = (&key_secret).into();
let mut ciphertext = changes_json.as_bytes().to_vec();
let mut cipher = XSalsa20::new(&secret_key_bytes.into(), &nonce.into());
cipher.apply_keystream(&mut ciphertext);
let encrypted_str = format!("encrypted_U{}", URL_SAFE.encode(&ciphertext));
Transaction::Private(PrivateTransaction {
encrypted_changes: Encrypted {
value: encrypted_str,
_phantom: std::marker::PhantomData,
},
key_used: key_id.clone(),
made_at: Number::from(made_at),
privacy: "private".to_string(),
})
}
TransactionMode::Trusting => Transaction::Trusting(TrustingTransaction {
changes: changes_json.to_string(),
made_at: Number::from(made_at),
privacy: "trusting".to_string(),
}),
};
let tx_json = serde_json::to_string(&new_tx).unwrap();
self.hasher.update(tx_json.as_bytes());
self.transactions_json.push(tx_json);
let new_hash = self.hasher.finalize();
let new_hash_encoded_stringified = format!("\"hash_z{}\"", bs58::encode(new_hash.as_bytes()).into_string());
let signing_key: SigningKey = signer_secret.into();
let new_signature: Signature = signing_key.sign(new_hash_encoded_stringified.as_bytes()).into();
self.last_signature = Some(new_signature.clone());
(new_signature, new_tx)
}
pub fn decrypt_next_transaction_changes_json(
&self,
tx_index: u32,
key_secret: KeySecret,
) -> Result<String, CoJsonCoreError> {
let tx_json = self
.transactions_json
.get(tx_index as usize)
.ok_or(CoJsonCoreError::TransactionNotFound(tx_index))?;
let tx: Transaction = serde_json::from_str(tx_json)?;
match tx {
Transaction::Private(private_tx) => {
let nonce_material = JsonValue::Object(serde_json::Map::from_iter(vec![
("in".to_string(), JsonValue::String(self.co_id.0.clone())),
(
"tx".to_string(),
serde_json::to_value(TransactionID {
session_id: self.session_id.clone(),
tx_index,
})?,
),
]));
let nonce = self.generate_json_nonce(&nonce_material);
let encrypted_val = private_tx.encrypted_changes.value;
let prefix = "encrypted_U";
if !encrypted_val.starts_with(prefix) {
return Err(CoJsonCoreError::InvalidEncryptedPrefix);
}
let ciphertext_b64 = &encrypted_val[prefix.len()..];
let mut ciphertext = URL_SAFE.decode(ciphertext_b64)?;
let secret_key_bytes: [u8; 32] = (&key_secret).into();
let mut cipher = XSalsa20::new((&secret_key_bytes).into(), &nonce.into());
cipher.apply_keystream(&mut ciphertext);
Ok(String::from_utf8(ciphertext)?)
}
Transaction::Trusting(trusting_tx) => Ok(trusting_tx.changes),
}
}
fn generate_nonce(&self, material: &[u8]) -> [u8; 24] {
let mut hasher = blake3::Hasher::new();
hasher.update(material);
let mut output = [0u8; 24];
let mut output_reader = hasher.finalize_xof();
output_reader.fill(&mut output);
output
}
fn generate_json_nonce(&self, material: &JsonValue) -> [u8; 24] {
let stable_json = serde_json::to_string(&material).unwrap();
self.generate_nonce(stable_json.as_bytes())
}
}
pub fn decode_z(value: &str) -> Result<Vec<u8>, String> {
let prefix_end = value.find("_z").ok_or("Invalid prefix")? + 2;
bs58::decode(&value[prefix_end..])
.into_vec()
.map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use rand_core::OsRng;
use std::{collections::HashMap, fs};
#[test]
fn it_works() {
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let session = SessionLogInternal::new(
CoID("co_test1".to_string()),
SessionID("session_test1".to_string()),
verifying_key.into(),
);
assert!(session.last_signature.is_none());
}
#[test]
fn test_add_from_example_json() {
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TestSession<'a> {
last_signature: Signature,
#[serde(borrow)]
transactions: Vec<&'a RawValue>,
last_hash: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Root<'a> {
#[serde(borrow)]
example_base: HashMap<String, TestSession<'a>>,
#[serde(rename = "signerID")]
signer_id: SignerID,
}
let data = fs::read_to_string("data/singleTxSession.json")
.expect("Unable to read singleTxSession.json");
let root: Root = serde_json::from_str(&data).unwrap();
let (session_id_str, example) = root.example_base.into_iter().next().unwrap();
let session_id = SessionID(session_id_str.clone());
let co_id = CoID(
session_id_str
.split("_session_")
.next()
.unwrap()
.to_string(),
);
let mut session = SessionLogInternal::new(co_id, session_id, root.signer_id);
let new_signature = example.last_signature;
let result = session.try_add(
vec![example.transactions[0].to_owned()],
&new_signature,
false,
);
match result {
Ok(returned_final_hash) => {
let final_hash = session.hasher.finalize();
let final_hash_encoded = format!(
"hash_z{}",
bs58::encode(final_hash.as_bytes()).into_string()
);
assert_eq!(final_hash_encoded, example.last_hash);
assert_eq!(session.last_signature, Some(new_signature));
}
Err(CoJsonCoreError::SignatureVerification(new_hash_encoded)) => {
assert_eq!(new_hash_encoded, example.last_hash);
panic!("Signature verification failed despite same hash");
}
Err(e) => {
panic!("Unexpected error: {:?}", e);
}
}
}
#[test]
fn test_add_from_example_json_multi_tx() {
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TestSession<'a> {
last_signature: Signature,
#[serde(borrow)]
transactions: Vec<&'a RawValue>,
last_hash: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Root<'a> {
#[serde(borrow)]
example_base: HashMap<String, TestSession<'a>>,
#[serde(rename = "signerID")]
signer_id: SignerID,
}
let data = fs::read_to_string("data/multiTxSession.json")
.expect("Unable to read multiTxSession.json");
let root: Root = serde_json::from_str(&data).unwrap();
let (session_id_str, example) = root.example_base.into_iter().next().unwrap();
let session_id = SessionID(session_id_str.clone());
let co_id = CoID(
session_id_str
.split("_session_")
.next()
.unwrap()
.to_string(),
);
let mut session = SessionLogInternal::new(co_id, session_id, root.signer_id);
let new_signature = example.last_signature;
let result = session.try_add(
example.transactions.into_iter().map(|tx| tx.to_owned()).collect(),
&new_signature,
false,
);
match result {
Ok(returned_final_hash) => {
let final_hash = session.hasher.finalize();
let final_hash_encoded = format!(
"hash_z{}",
bs58::encode(final_hash.as_bytes()).into_string()
);
assert_eq!(final_hash_encoded, example.last_hash);
assert_eq!(session.last_signature, Some(new_signature));
}
Err(CoJsonCoreError::SignatureVerification(new_hash_encoded)) => {
assert_eq!(new_hash_encoded, example.last_hash);
panic!("Signature verification failed despite same hash");
}
Err(e) => {
panic!("Unexpected error: {:?}", e);
}
}
}
#[test]
fn test_add_new_transaction() {
// Load the example data to get all the pieces we need
let data = fs::read_to_string("data/singleTxSession.json")
.expect("Unable to read singleTxSession.json");
let root: serde_json::Value = serde_json::from_str(&data).unwrap();
let session_data =
&root["exampleBase"]["co_zkNajJ1BhLzR962jpzvXxx917ZB_session_zXzrQLTtp8rR"];
let tx_from_example = &session_data["transactions"][0];
let known_key = &root["knownKeys"][0];
// Since we don't have the original private key, we generate a new one for this test.
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let public_key = signing_key.verifying_key();
// Initialize an empty session
let mut session = SessionLogInternal::new(
CoID(root["coID"].as_str().unwrap().to_string()),
SessionID("co_zkNajJ1BhLzR962jpzvXxx917ZB_session_zXzrQLTtp8rR".to_string()),
public_key.into(),
);
// The plaintext changes we want to add
let changes_json =
r#"[{"after":"start","op":"app","value":"co_zMphsnYN6GU8nn2HDY5suvyGufY"}]"#;
// Extract all the necessary components from the example data
let key_secret = KeySecret(known_key["secret"].as_str().unwrap().to_string());
let key_id = KeyID(known_key["id"].as_str().unwrap().to_string());
let made_at = tx_from_example["madeAt"].as_u64().unwrap();
// Call the function we are testing
let (new_signature, _new_tx) = session.add_new_transaction(
changes_json,
TransactionMode::Private {
key_id: key_id,
key_secret: key_secret,
},
&signing_key.into(),
made_at,
);
// 1. Check that the transaction we created matches the one in the file
let created_tx_json = &session.transactions_json[0];
let expected_tx_json = serde_json::to_string(tx_from_example).unwrap();
assert_eq!(created_tx_json, &expected_tx_json);
// 2. Check that the final hash of the session matches the one in the file
let final_hash = session.hasher.finalize();
let final_hash_encoded = format!(
"hash_z{}",
bs58::encode(final_hash.as_bytes()).into_string()
);
assert_eq!(
final_hash_encoded,
session_data["lastHash"].as_str().unwrap()
);
let final_hash_encoded_stringified = format!(
"\"{}\"",
final_hash_encoded
);
// 3. Check that the signature is valid for our generated key
assert!(session
.public_key
.verify(final_hash_encoded_stringified.as_bytes(), &(&new_signature).into())
.is_ok());
assert_eq!(session.last_signature, Some(new_signature));
}
#[test]
fn test_decrypt_from_example_json() {
#[derive(Deserialize, Debug)]
struct KnownKey {
secret: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(bound(deserialize = "'de: 'a"))]
struct TestSession<'a> {
last_signature: String,
#[serde(borrow)]
transactions: Vec<&'a RawValue>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(bound(deserialize = "'de: 'a"))]
struct Root<'a> {
#[serde(borrow)]
example_base: HashMap<String, TestSession<'a>>,
#[serde(rename = "signerID")]
signer_id: SignerID,
known_keys: Vec<KnownKey>,
#[serde(rename = "coID")]
co_id: CoID,
}
let data = fs::read_to_string("data/singleTxSession.json")
.expect("Unable to read singleTxSession.json");
let root: Root = serde_json::from_str(&data).unwrap();
let (session_id_str, example) = root.example_base.into_iter().next().unwrap();
let session_id = SessionID(session_id_str.clone());
let public_key =
VerifyingKey::from_bytes(&decode_z(&root.signer_id.0).unwrap().try_into().unwrap())
.unwrap();
let mut session = SessionLogInternal::new(root.co_id, session_id, public_key.into());
let new_signature = Signature(example.last_signature);
session
.try_add(
example
.transactions
.into_iter()
.map(|v| v.to_owned())
.collect(),
&new_signature,
true, // Skipping verification because we don't have the right initial state
)
.unwrap();
let key_secret = KeySecret(root.known_keys[0].secret.clone());
let decrypted = session
.decrypt_next_transaction_changes_json(0, key_secret)
.unwrap();
assert_eq!(
decrypted,
r#"[{"after":"start","op":"app","value":"co_zMphsnYN6GU8nn2HDY5suvyGufY"}]"#
);
}
}

View File

@@ -1,15 +0,0 @@
[package]
name = "lzy"
version = "0.1.0"
edition = "2021"
[dependencies]
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[[bench]]
name = "compression_benchmark"
harness = false

View File

@@ -1,36 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput};
use lzy::{compress, decompress};
use std::fs;
use std::time::Duration;
fn compression_benchmark(c: &mut Criterion) {
let data = fs::read("data/compression_66k_JSON.txt").expect("Failed to read benchmark data");
let mut group = c.benchmark_group("LZY Compression");
group.measurement_time(Duration::from_secs(10));
group.sample_size(10);
group.throughput(Throughput::Bytes(data.len() as u64));
let compressed = compress(&data);
let compression_ratio = compressed.len() as f64 / data.len() as f64;
println!(
"Compression ratio (compressed/original): {:.4} ({} / {} bytes)",
compression_ratio,
compressed.len(),
data.len()
);
group.bench_function("compress", |b| {
b.iter(|| compress(black_box(&data)))
});
let decompressed = decompress(&compressed).unwrap();
assert_eq!(data, decompressed);
group.bench_function("decompress", |b| {
b.iter(|| decompress(black_box(&compressed)))
});
}
criterion_group!(benches, compression_benchmark);
criterion_main!(benches);

File diff suppressed because one or more lines are too long

View File

@@ -1,348 +0,0 @@
const MIN_MATCH_LEN: usize = 4;
const MAX_MATCH_LEN: usize = 15 + 3;
const MAX_LITERALS: usize = 15;
const HASH_LOG: u32 = 16;
const HASH_TABLE_SIZE: usize = 1 << HASH_LOG;
fn hash(data: &[u8]) -> usize {
const KNUTH_MULT_PRIME: u32 = 2654435761;
let val = u32::from_le_bytes(data.try_into().unwrap());
((val.wrapping_mul(KNUTH_MULT_PRIME)) >> (32 - HASH_LOG)) as usize
}
#[derive(Debug, PartialEq)]
pub enum DecompressionError {
InvalidToken,
UnexpectedEof,
}
pub fn decompress(input: &[u8]) -> Result<Vec<u8>, DecompressionError> {
let mut decompressed = Vec::with_capacity(input.len() * 2);
let mut i = 0;
while i < input.len() {
let token = input[i];
i += 1;
let literal_len = (token >> 4) as usize;
let match_len_token = (token & 0x0F) as usize;
if i + literal_len > input.len() {
return Err(DecompressionError::UnexpectedEof);
}
decompressed.extend_from_slice(&input[i..i + literal_len]);
i += literal_len;
if match_len_token > 0 {
if i + 2 > input.len() {
return Err(DecompressionError::UnexpectedEof);
}
let offset = u16::from_le_bytes([input[i], input[i + 1]]) as usize;
i += 2;
if offset == 0 || offset > decompressed.len() {
return Err(DecompressionError::InvalidToken);
}
let match_len = match_len_token + 3;
let match_start = decompressed.len() - offset;
for k in 0..match_len {
decompressed.push(decompressed[match_start + k]);
}
}
}
Ok(decompressed)
}
pub fn compress(input: &[u8]) -> Vec<u8> {
let mut compressor = Compressor::new();
compressor.compress_chunk(input)
}
fn emit_sequence(out: &mut Vec<u8>, mut literals: &[u8], match_len: usize, offset: u16) {
while literals.len() > MAX_LITERALS {
let token = (MAX_LITERALS as u8) << 4;
out.push(token);
out.extend_from_slice(&literals[..MAX_LITERALS]);
literals = &literals[MAX_LITERALS..];
}
let lit_len_token = literals.len() as u8;
let match_len_token = if match_len > 0 {
(match_len - 3) as u8
} else {
0
};
let token = lit_len_token << 4 | match_len_token;
out.push(token);
out.extend_from_slice(literals);
if match_len > 0 {
out.extend_from_slice(&offset.to_le_bytes());
}
}
pub struct Compressor {
hash_table: Vec<u32>,
history: Vec<u8>,
}
impl Compressor {
pub fn new() -> Self {
Self {
hash_table: vec![0; HASH_TABLE_SIZE],
history: Vec::new(),
}
}
pub fn compress_chunk(&mut self, chunk: &[u8]) -> Vec<u8> {
let mut compressed_chunk = Vec::new();
let chunk_start_cursor = self.history.len();
self.history.extend_from_slice(chunk);
let mut cursor = chunk_start_cursor;
let mut literal_anchor = chunk_start_cursor;
while cursor < self.history.len() {
let mut best_match: Option<(u16, usize)> = None;
if self.history.len() - cursor >= MIN_MATCH_LEN {
let h = hash(&self.history[cursor..cursor + 4]);
let match_pos = self.hash_table[h] as usize;
if match_pos < cursor && cursor - match_pos < u16::MAX as usize {
if self.history.get(match_pos..match_pos + MIN_MATCH_LEN) == Some(&self.history[cursor..cursor + MIN_MATCH_LEN]) {
let mut match_len = MIN_MATCH_LEN;
while cursor + match_len < self.history.len()
&& match_len < MAX_MATCH_LEN
&& self.history.get(match_pos + match_len) == self.history.get(cursor + match_len)
{
match_len += 1;
}
best_match = Some(((cursor - match_pos) as u16, match_len));
}
}
self.hash_table[h] = cursor as u32;
}
if let Some((offset, match_len)) = best_match {
let literals = &self.history[literal_anchor..cursor];
emit_sequence(&mut compressed_chunk, literals, match_len, offset);
cursor += match_len;
literal_anchor = cursor;
} else {
cursor += 1;
}
}
if literal_anchor < cursor {
let literals = &self.history[literal_anchor..cursor];
emit_sequence(&mut compressed_chunk, literals, 0, 0);
}
compressed_chunk
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_roundtrip() {
let data = b"hello world, hello people";
let compressed = compress(data);
println!("Compressed '{}': {:x?}", std::str::from_utf8(data).unwrap(), compressed);
let decompressed = decompress(&compressed).unwrap();
assert_eq!(data, decompressed.as_slice());
}
#[test]
fn test_long_literals() {
let data = b"abcdefghijklmnopqrstuvwxyz";
let compressed = compress(data);
println!("Compressed '{}': {:x?}", std::str::from_utf8(data).unwrap(), compressed);
let decompressed = decompress(&compressed).unwrap();
assert_eq!(data, decompressed.as_slice());
}
#[test]
fn test_decompress_empty() {
let data = b"";
let compressed = compress(data);
assert!(compressed.is_empty());
let decompressed = decompress(&compressed).unwrap();
assert_eq!(data, decompressed.as_slice());
}
#[test]
fn test_overlapping_match() {
let data = b"abcdeabcdeabcdeabcde"; // repeating sequence
let compressed = compress(data);
println!("Compressed '{}': {:x?}", std::str::from_utf8(data).unwrap(), compressed);
let decompressed = decompress(&compressed).unwrap();
assert_eq!(data, decompressed.as_slice());
let data2 = b"abababababababababab";
let compressed2 = compress(data2);
println!("Compressed '{}': {:x?}", std::str::from_utf8(data2).unwrap(), compressed2);
let decompressed2 = decompress(&compressed2).unwrap();
assert_eq!(data2, decompressed2.as_slice());
}
#[test]
fn test_json_roundtrip() {
let data = std::fs::read("data/compression_66k_JSON.txt").unwrap();
let compressed = compress(&data);
std::fs::write("compressed_66k.lzy", &compressed).unwrap();
let decompressed = decompress(&compressed).unwrap();
assert_eq!(data, decompressed.as_slice());
}
mod crdt_helpers {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct After {
pub session_id: String,
pub tx_index: u32,
pub change_idx: u32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Transaction {
pub op: String,
pub value: String,
pub after: After,
}
pub fn generate_transactions(text: &str, session_id: &str) -> Vec<String> {
let mut transactions = Vec::new();
for (i, c) in text.chars().enumerate() {
let tx = Transaction {
op: "app".to_string(),
value: c.to_string(),
after: After {
session_id: session_id.to_string(),
tx_index: i as u32,
change_idx: 0,
},
};
transactions.push(serde_json::to_string(&tx).unwrap());
}
transactions
}
pub fn generate_shorthand_transactions(text: &str) -> Vec<String> {
let mut transactions = Vec::new();
for c in text.chars() {
transactions.push(serde_json::to_string(&c.to_string()).unwrap());
}
transactions
}
}
#[test]
fn test_crdt_transaction_generation() {
let sample_text = "This is a sample text for our CRDT simulation. \
It should be long enough to see some interesting compression results later on. \
Let's add another sentence to make it a bit more substantial.";
let session_id = "co_zRtnoNffeMHge9wvyL5mK1RWbdz_session_zKvAVFSV5cqW";
let transactions = crdt_helpers::generate_transactions(sample_text, session_id);
println!("--- Generated CRDT Transactions ---");
for tx in &transactions {
println!("{}", tx);
}
println!("--- End of CRDT Transactions ---");
assert!(!transactions.is_empty());
assert_eq!(transactions.len(), sample_text.chars().count());
}
#[test]
fn test_crdt_chunked_compression() {
let sample_text = "This is a sample text for our CRDT simulation. \
It should be long enough to see some interesting compression results later on. \
Let's add another sentence to make it a bit more substantial.";
let session_id = "co_zRtnoNffeMHge9wvyL5mK1RWbdz_session_zKvAVFSV5cqW";
let transactions_json = crdt_helpers::generate_transactions(sample_text, session_id);
let mut compressor = Compressor::new();
let mut compressed_log = Vec::new();
let mut total_json_len = 0;
for tx_json in &transactions_json {
let compressed_chunk = compressor.compress_chunk(tx_json.as_bytes());
compressed_log.extend_from_slice(&compressed_chunk);
total_json_len += tx_json.len();
}
let decompressed = decompress(&compressed_log).unwrap();
// Verify roundtrip
let original_log_concatenated = transactions_json.join("");
assert_eq!(decompressed, original_log_concatenated.as_bytes());
let plaintext_len = sample_text.len();
let compressed_len = compressed_log.len();
let compression_ratio = compressed_len as f64 / total_json_len as f64;
let overhead_ratio = compressed_len as f64 / plaintext_len as f64;
println!("\n--- CRDT Chunked Compression Test ---");
println!("Plaintext size: {} bytes", plaintext_len);
println!("Total JSON size: {} bytes", total_json_len);
println!("Compressed log size: {} bytes", compressed_len);
println!("Compression ratio (compressed/json): {:.4}", compression_ratio);
println!("Overhead ratio (compressed/plaintext): {:.4}", overhead_ratio);
println!("--- End of Test ---");
}
#[test]
fn test_crdt_shorthand_compression() {
let sample_text = "This is a sample text for our CRDT simulation. \
It should be long enough to see some interesting compression results later on. \
Let's add another sentence to make it a bit more substantial.";
let transactions_json = crdt_helpers::generate_shorthand_transactions(sample_text);
let mut compressor = Compressor::new();
let mut compressed_log = Vec::new();
let mut total_json_len = 0;
for tx_json in &transactions_json {
let compressed_chunk = compressor.compress_chunk(tx_json.as_bytes());
compressed_log.extend_from_slice(&compressed_chunk);
total_json_len += tx_json.len();
}
let decompressed = decompress(&compressed_log).unwrap();
// Verify roundtrip
let original_log_concatenated = transactions_json.join("");
assert_eq!(decompressed, original_log_concatenated.as_bytes());
let plaintext_len = sample_text.len();
let compressed_len = compressed_log.len();
let compression_ratio = compressed_len as f64 / total_json_len as f64;
let overhead_ratio = compressed_len as f64 / plaintext_len as f64;
println!("\n--- CRDT Shorthand Compression Test ---");
println!("Plaintext size: {} bytes", plaintext_len);
println!("Total JSON size: {} bytes", total_json_len);
println!("Compressed log size: {} bytes", compressed_len);
println!("Compression ratio (compressed/json): {:.4}", compression_ratio);
println!("Overhead ratio (compressed/plaintext): {:.4}", overhead_ratio);
println!("--- End of Test ---");
}
}

View File

@@ -1 +0,0 @@
BETTER_AUTH_SECRET="TEST_SECRET"

View File

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

View File

@@ -1,298 +0,0 @@
# betterauth
## 0.1.25
### Patch Changes
- Updated dependencies [048ac1d]
- jazz-tools@0.14.22
- jazz-betterauth-server-plugin@0.14.22
- jazz-inspector@0.14.22
- jazz-react@0.14.22
- jazz-react-auth-betterauth@0.14.22
- jazz-betterauth-client-plugin@0.14.22
## 0.1.24
### Patch Changes
- Updated dependencies [e7e505e]
- Updated dependencies [13b57aa]
- Updated dependencies [5662faa]
- Updated dependencies [2116a59]
- jazz-tools@0.14.21
- jazz-betterauth-server-plugin@0.14.21
- jazz-inspector@0.14.21
- jazz-react@0.14.21
- jazz-react-auth-betterauth@0.14.21
- jazz-betterauth-client-plugin@0.14.21
## 0.1.23
### Patch Changes
- Updated dependencies [6f72419]
- Updated dependencies [04b20c2]
- jazz-tools@0.14.20
- jazz-betterauth-server-plugin@0.14.20
- jazz-inspector@0.14.20
- jazz-react@0.14.20
- jazz-react-auth-betterauth@0.14.20
- jazz-betterauth-client-plugin@0.14.20
## 0.1.22
### Patch Changes
- jazz-betterauth-client-plugin@0.14.19
- jazz-betterauth-server-plugin@0.14.19
- jazz-react-auth-betterauth@0.14.19
- jazz-inspector@0.14.19
- jazz-react@0.14.19
- jazz-tools@0.14.19
## 0.1.21
### Patch Changes
- Updated dependencies [4b950bc]
- Updated dependencies [d6d9c0a]
- Updated dependencies [c559054]
- jazz-tools@0.14.18
- jazz-betterauth-server-plugin@0.14.18
- jazz-inspector@0.14.18
- jazz-react@0.14.18
- jazz-react-auth-betterauth@0.14.18
- jazz-betterauth-client-plugin@0.14.18
## 0.1.20
### Patch Changes
- Updated dependencies [e512df4]
- jazz-betterauth-server-plugin@0.14.17
- jazz-tools@0.14.17
- jazz-betterauth-client-plugin@0.14.17
- jazz-inspector@0.14.17
- jazz-react@0.14.17
- jazz-react-auth-betterauth@0.14.17
## 0.1.19
### Patch Changes
- jazz-betterauth-server-plugin@0.14.16
- jazz-inspector@0.14.16
- jazz-react@0.14.16
- jazz-react-auth-betterauth@0.14.16
- jazz-tools@0.14.16
- jazz-betterauth-client-plugin@0.14.16
## 0.1.18
### Patch Changes
- Updated dependencies [f9590f9]
- jazz-react@0.14.15
- jazz-betterauth-server-plugin@0.14.15
- jazz-inspector@0.14.15
- jazz-react-auth-betterauth@0.14.15
- jazz-tools@0.14.15
- jazz-betterauth-client-plugin@0.14.15
## 0.1.17
### Patch Changes
- Updated dependencies [e32a1f7]
- jazz-tools@0.14.14
- jazz-betterauth-server-plugin@0.14.14
- jazz-inspector@0.14.14
- jazz-react@0.14.14
- jazz-react-auth-betterauth@0.14.14
- jazz-betterauth-client-plugin@0.14.14
## 0.1.16
### Patch Changes
- jazz-inspector@0.14.13
- jazz-react@0.14.13
- jazz-react-auth-betterauth@0.14.13
## 0.1.15
### Patch Changes
- jazz-inspector@0.14.12
- jazz-react@0.14.12
- jazz-react-auth-betterauth@0.14.12
## 0.1.14
### Patch Changes
- Updated dependencies [dc746a2]
- Updated dependencies [f869d9a]
- Updated dependencies [3fe6832]
- jazz-react-auth-betterauth@0.14.10
- jazz-inspector@0.14.10
- jazz-react@0.14.10
- jazz-tools@0.14.10
- jazz-betterauth-server-plugin@0.14.10
- jazz-betterauth-client-plugin@0.14.10
## 0.1.13
### Patch Changes
- Updated dependencies [22c2600]
- jazz-tools@0.14.9
- jazz-betterauth-server-plugin@0.14.9
- jazz-inspector@0.14.9
- jazz-react@0.14.9
- jazz-react-auth-betterauth@0.14.9
- jazz-betterauth-client-plugin@0.14.9
## 0.1.12
### Patch Changes
- Updated dependencies [637ae13]
- jazz-tools@0.14.8
- jazz-betterauth-server-plugin@0.14.8
- jazz-inspector@0.14.8
- jazz-react@0.14.8
- jazz-react-auth-betterauth@0.14.8
- jazz-betterauth-client-plugin@0.14.8
## 0.1.11
### Patch Changes
- Updated dependencies [365b0ea]
- jazz-tools@0.14.7
- jazz-betterauth-server-plugin@0.14.7
- jazz-inspector@0.14.7
- jazz-react@0.14.7
- jazz-react-auth-betterauth@0.14.7
- jazz-betterauth-client-plugin@0.14.7
## 0.1.10
### Patch Changes
- Updated dependencies [9d6d9fe]
- Updated dependencies [9d6d9fe]
- jazz-tools@0.14.6
- jazz-betterauth-server-plugin@0.14.6
- jazz-inspector@0.14.6
- jazz-react@0.14.6
- jazz-react-auth-betterauth@0.14.6
- jazz-betterauth-client-plugin@0.14.6
## 0.1.9
### Patch Changes
- Updated dependencies [91cbb2f]
- Updated dependencies [20b3d88]
- jazz-tools@0.14.5
- jazz-betterauth-server-plugin@0.14.5
- jazz-inspector@0.14.5
- jazz-react@0.14.5
- jazz-react-auth-betterauth@0.14.5
- jazz-betterauth-client-plugin@0.14.5
## 0.1.8
### Patch Changes
- Updated dependencies [011af55]
- jazz-tools@0.14.4
- jazz-betterauth-server-plugin@0.14.4
- jazz-inspector@0.14.4
- jazz-react@0.14.4
- jazz-react-auth-betterauth@0.14.4
- jazz-betterauth-client-plugin@0.14.4
## 0.1.7
### Patch Changes
- Updated dependencies [3d1027f]
- Updated dependencies [c240eed]
- jazz-tools@0.14.2
- jazz-betterauth-server-plugin@0.14.2
- jazz-inspector@0.14.2
- jazz-react@0.14.2
- jazz-react-auth-betterauth@0.14.2
- jazz-betterauth-client-plugin@0.14.2
## 0.1.6
### Patch Changes
- Updated dependencies [cdfc105]
- jazz-tools@0.14.1
- jazz-betterauth-server-plugin@0.14.1
- jazz-inspector@0.14.1
- jazz-react@0.14.1
- jazz-react-auth-betterauth@0.14.1
- jazz-betterauth-client-plugin@0.14.1
## 0.1.5
### Patch Changes
- Updated dependencies [5835ed1]
- jazz-tools@0.14.0
- jazz-betterauth-server-plugin@0.14.0
- jazz-inspector@0.14.0
- jazz-react@0.14.0
- jazz-react-auth-betterauth@0.14.0
- jazz-betterauth-client-plugin@0.14.0
## 0.1.4
### Patch Changes
- jazz-betterauth-server-plugin@0.13.32
- jazz-react@0.13.32
- jazz-react-auth-betterauth@0.13.32
- jazz-betterauth-client-plugin@0.13.32
## 0.1.3
### Patch Changes
- Updated dependencies [e5b170f]
- jazz-tools@0.13.31
- jazz-betterauth-server-plugin@0.13.31
- jazz-inspector@0.13.31
- jazz-react@0.13.31
- jazz-react-auth-betterauth@0.13.31
- jazz-betterauth-client-plugin@0.13.31
## 0.1.2
### Patch Changes
- jazz-betterauth-server-plugin@0.13.30
- jazz-inspector@0.13.30
- jazz-react@0.13.30
- jazz-react-auth-betterauth@0.13.30
- jazz-tools@0.13.30
- jazz-betterauth-client-plugin@0.13.30
## 0.1.1
### Patch Changes
- Updated dependencies [8e5ff13]
- jazz-inspector@0.13.29
- jazz-betterauth-server-plugin@0.0.1
- jazz-react@0.13.29
- jazz-react-auth-betterauth@0.0.1
- jazz-tools@0.13.29
- jazz-betterauth-client-plugin@0.0.1

View File

@@ -1,82 +0,0 @@
# Better Auth Integration Example
This example demonstrates how to integrate [Better Auth](https://www.better-auth.com/) with Jazz.
## Getting started
To run this example, you may either:
- Clone the Jazz monorepo and run this example from within.
- Create a new Jazz project using this example as a template, and run that new project.
### Setting environment variables
- `NEXT_PUBLIC_AUTH_BASE_URL`: A URL to a Better Auth server. If undefined, the example will self-host a Better Auth server.
- `BETTER_AUTH_SECRET`: The encryption secret used by the self-hosted Better Auth server (required only if `NEXT_PUBLIC_AUTH_BASE_URL` is undefined)
- `GITHUB_CLIENT_ID`: The client ID for the GitHub OAuth provider used by the self-hosted Better Auth server (required only if `NEXT_PUBLIC_AUTH_BASE_URL` is undefined)
- `GITHUB_CLIENT_SECRET`: The client secret for the GitHub OAuth provider used by the self-hosted Better Auth server (required only if `NEXT_PUBLIC_AUTH_BASE_URL` is undefined)
### Using this example as a template
1. Create a new Jazz project, and use this example as a template.
```sh
npx create-jazz-app@latest betterauth-app --example betterauth
```
2. Navigate to the new project and install dependencies.
```sh
cd betterauth-app
pnpm install
```
3. Create a .env file (don't forget to set your [BETTER_AUTH_SECRET](https://www.better-auth.com/docs/installation#set-environment-variables)!)
```sh
mv .env.example .env
```
4. Start the development server
```sh
pnpm dev
```
https://www.better-auth.com/docs/installation#set-environment-variables
### Using the monorepo
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
Clone the jazz repository.
```bash
git clone https://github.com/garden-co/jazz.git
```
Install and build dependencies.
```bash
pnpm i && npx turbo build
```
Go to the example directory.
```bash
cd jazz/examples/betterauth/
```
Create a .env file (don't forget to set your [BETTER_AUTH_SECRET](https://www.better-auth.com/docs/installation#set-environment-variables)!)
```sh
mv .env.example .env
```
Start the dev server.
```bash
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

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

View File

@@ -1,48 +0,0 @@
{
"name": "betterauth",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"email": "email dev --dir src/components/emails"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^12.8.0",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-slot": "^1.2.2",
"better-auth": "^1.2.4",
"better-sqlite3": "^11.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jazz-react-auth-betterauth": "workspace:*",
"jazz-betterauth-client-plugin": "workspace:*",
"jazz-betterauth-server-plugin": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "catalog:react",
"react-dom": "catalog:react",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^20",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"react-email": "^4.0.11",
"tailwindcss": "^4",
"typescript": "catalog:default"
}
}

View File

@@ -1,53 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
import isCI from "is-ci";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm dev",
url: "http://localhost:3000/",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -1,5 +0,0 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@@ -1,15 +0,0 @@
<svg
viewBox="0 0 386 146"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path id="text"
d="M176.725 33.865H188.275V22.7H176.725V33.865ZM164.9 129.4H172.875C182.72 129.4 188.275 123.9 188.275 114.22V43.6H176.725V109.545C176.725 115.65 173.975 118.51 167.925 118.51H164.9V129.4ZM245.298 53.28C241.613 45.47 233.363 41.95 222.748 41.95C208.998 41.95 200.748 48.44 197.888 58.615L208.613 61.915C210.648 55.315 216.368 52.565 222.638 52.565C231.933 52.565 235.673 56.415 236.058 64.61C226.433 65.93 216.643 67.195 209.768 69.23C200.583 72.145 195.743 77.865 195.743 86.83C195.743 96.51 202.673 104.65 215.818 104.65C225.443 104.65 232.318 101.35 237.213 94.365V103H247.388V66.425C247.388 61.475 247.168 57.185 245.298 53.28ZM217.853 95.245C210.483 95.245 207.128 91.34 207.128 86.72C207.128 82.045 210.593 79.515 215.323 77.92C220.328 76.435 226.983 75.5 235.948 74.18C235.893 76.93 235.673 80.725 234.738 83.475C233.418 89.25 227.643 95.245 217.853 95.245ZM251.22 103H301.545V92.715H269.535L303.195 45.47V43.6H254.3V53.885H284.935L251.22 101.185V103ZM304.815 103H355.14V92.715H323.13L356.79 45.47V43.6H307.895V53.885H338.53L304.815 101.185V103Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M136.179 44.8277C136.179 44.8277 136.179 44.8277 136.179 44.8276V21.168C117.931 28.5527 97.9854 32.6192 77.0897 32.6192C65.1466 32.6192 53.5138 31.2908 42.331 28.7737V51.4076C42.331 51.4076 42.331 51.4076 42.331 51.4076V81.1508C41.2955 80.4385 40.1568 79.8458 38.9405 79.3915C36.1732 78.358 33.128 78.0876 30.1902 78.6145C27.2524 79.1414 24.5539 80.4419 22.4358 82.3516C20.3178 84.2613 18.8754 86.6944 18.291 89.3433C17.7066 91.9921 18.0066 94.7377 19.1528 97.2329C20.2991 99.728 22.2403 101.861 24.7308 103.361C27.2214 104.862 30.1495 105.662 33.1448 105.662H33.1455C33.6061 105.662 33.8365 105.662 34.0314 105.659C44.5583 105.449 53.042 96.9656 53.2513 86.4386C53.2534 86.3306 53.2544 86.2116 53.2548 86.0486H53.2552V85.7149L53.2552 85.5521V82.0762L53.2552 53.1993C61.0533 54.2324 69.0092 54.7656 77.0897 54.7656C77.6696 54.7656 78.2489 54.7629 78.8276 54.7574V110.696C77.792 109.983 76.6533 109.391 75.437 108.936C72.6697 107.903 69.6246 107.632 66.6867 108.159C63.7489 108.686 61.0504 109.987 58.9323 111.896C56.8143 113.806 55.3719 116.239 54.7875 118.888C54.2032 121.537 54.5031 124.283 55.6494 126.778C56.7956 129.273 58.7368 131.405 61.2273 132.906C63.7179 134.406 66.646 135.207 69.6414 135.207C70.1024 135.207 70.3329 135.207 70.5279 135.203C81.0548 134.994 89.5385 126.51 89.7478 115.983C89.7517 115.788 89.7517 115.558 89.7517 115.097V111.621L89.7517 54.3266C101.962 53.4768 113.837 51.4075 125.255 48.2397V80.9017C124.219 80.1894 123.081 79.5966 121.864 79.1424C119.097 78.1089 116.052 77.8384 113.114 78.3653C110.176 78.8922 107.478 80.1927 105.36 82.1025C103.242 84.0122 101.799 86.4453 101.215 89.0941C100.631 91.743 100.931 94.4886 102.077 96.9837C103.223 99.4789 105.164 101.612 107.655 103.112C110.145 104.612 113.073 105.413 116.069 105.413C116.53 105.413 116.76 105.413 116.955 105.409C127.482 105.2 135.966 96.7164 136.175 86.1895C136.179 85.9945 136.179 85.764 136.179 85.3029V81.8271L136.179 44.8277Z"
fill="#146AFF"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,12 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 125 125"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M118.179 29.6597V6C99.931 13.3847 79.9854 17.4512 59.0897 17.4512C47.1466 17.4512 35.5138 16.1228 24.331 13.6057V36.2396V65.9828C23.2955 65.2705 22.1568 64.6778 20.9405 64.2235C18.1732 63.19 15.128 62.9196 12.1902 63.4465C9.25242 63.9734 6.55392 65.2739 4.43582 67.1836C2.31782 69.0933 0.875416 71.5264 0.291016 74.1753C-0.293384 76.8241 0.00661504 79.5697 1.15281 82.0649C2.29911 84.56 4.24032 86.693 6.73082 88.193C9.22142 89.694 12.1495 90.494 15.1448 90.494C15.6054 90.494 15.8365 90.494 16.0314 90.491C26.5583 90.281 35.042 81.7976 35.2513 71.2706C35.2534 71.1626 35.2544 71.0436 35.2548 70.8806L35.2552 70.5469V70.3841V66.9082V38.0313C43.0533 39.0644 51.0092 39.5976 59.0897 39.5976C59.6696 39.5976 60.2489 39.5949 60.8276 39.5894V95.528C59.792 94.815 58.6533 94.223 57.437 93.768C54.6697 92.735 51.6246 92.464 48.6867 92.991C45.7489 93.518 43.0504 94.819 40.9323 96.728C38.8143 98.638 37.3719 101.071 36.7875 103.72C36.2032 106.369 36.5031 109.115 37.6494 111.61C38.7956 114.105 40.7368 116.237 43.2273 117.738C45.7179 119.238 48.646 120.039 51.6414 120.039C52.1024 120.039 52.3329 120.039 52.5279 120.035C63.0548 119.826 71.5385 111.342 71.7478 100.815C71.7517 100.62 71.7517 100.39 71.7517 99.929V96.453V39.1586C83.962 38.3088 95.837 36.2395 107.255 33.0717V65.7337C106.219 65.0214 105.081 64.4286 103.864 63.9744C101.097 62.9409 98.052 62.6704 95.114 63.1973C92.176 63.7242 89.478 65.0247 87.36 66.9345C85.242 68.8442 83.799 71.2773 83.215 73.9261C82.631 76.575 82.931 79.3206 84.077 81.8157C85.223 84.3109 87.164 86.444 89.655 87.944C92.145 89.444 95.073 90.245 98.069 90.245C98.53 90.245 98.76 90.245 98.955 90.241C109.482 90.032 117.966 81.5484 118.175 71.0215C118.179 70.8265 118.179 70.596 118.179 70.1349V66.6591V29.6597Z"
fill="#146AFF"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,22 +0,0 @@
import { Navbar } from "@/components/navbar";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Jazz Example: Better Auth",
description: "Jazz example application demonstrating Better Auth integration",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Navbar />
<div className="container mx-auto pt-16 min-h-screen flex flex-col items-center justify-center">
{children}
</div>
</>
);
}

View File

@@ -1,113 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { Account } from "jazz-tools";
import { useAccount } from "jazz-tools/react";
import {
AppWindowMacIcon,
FileTextIcon,
GlobeIcon,
WrenchIcon,
} from "lucide-react";
import Image from "next/image";
export default function Home() {
const { me } = useAccount(Account, { resolve: { profile: {} } });
if (!me) {
return null;
}
return (
<div className="grow flex flex-col items-center justify-center">
<main className="flex flex-col gap-8 row-start-2 grow justify-center">
<Image
src="/jazz-logo.svg"
alt="Jazz logo"
width={180}
height={38}
priority
/>
<p className="text-sm/6 text-center sm:text-left">
Signed in as{" "}
<span className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-mono font-semibold">
{me.profile.name}
</span>{" "}
with id{" "}
<span className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-mono font-semibold">
{me.id}
</span>
</p>
<div className="flex gap-4 items-center flex-row">
<Button asChild size="lg">
<a
href="https://jazz.tools/docs"
target="_blank"
rel="noopener noreferrer"
>
<Image src="/jazz.svg" alt="" width={20} height={20} />
Start building
</a>
</Button>
<Button asChild variant="secondary" size="lg">
<a
href="https://jazz.tools/docs"
target="_blank"
rel="noopener noreferrer"
>
<FileTextIcon className="size-4" />
Read the docs
</a>
</Button>
</div>
</main>
<footer className="flex gap-4 py-8">
<Button asChild variant="ghost">
<a
href="https://jazz.tools/api-reference"
target="_blank"
rel="noopener noreferrer"
>
<FileTextIcon className="size-4" />
API reference
</a>
</Button>
<Button asChild variant="ghost">
<a
href="https://jazz.tools/examples"
target="_blank"
rel="noopener noreferrer"
>
<AppWindowMacIcon className="size-4" />
Examples
</a>
</Button>
<Button asChild variant="ghost">
<a
href="https://jazz.tools/status"
target="_blank"
rel="noopener noreferrer"
>
<GlobeIcon className="size-4" />
Status
</a>
</Button>
<Button asChild variant="ghost">
<a
href="https://jazz.tools/showcase"
target="_blank"
rel="noopener noreferrer"
>
<WrenchIcon className="size-4" />
Built with Jazz
</a>
</Button>
</footer>
</div>
);
}

View File

@@ -1,14 +0,0 @@
import { UserSettings } from "@/components/user-settings";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Settings | Jazz Example: Better Auth",
};
export default function SettingsPage() {
return (
<div className="max-w-screen-md w-full mx-auto px-4">
<UserSettings />
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = (() => {
if (!process.env.NEXT_PUBLIC_AUTH_BASE_URL) {
return toNextJsHandler(auth.handler);
} else {
return { GET: undefined, POST: undefined };
}
})();

View File

@@ -1,10 +0,0 @@
import { ForgotPasswordForm } from "@/components/forgot-password-form";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Forgot password | Jazz Example: Better Auth",
};
export default function ForgotPasswordPage() {
return <ForgotPasswordForm />;
}

View File

@@ -1,23 +0,0 @@
import Image from "next/image";
import Link from "next/link";
interface Props {
children: React.ReactNode;
}
export default function AuthLayout({ children }: Props) {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<Link
href="/"
className="flex items-center gap-2 self-center font-medium"
>
<Image src="/jazz.svg" alt="Jazz Logo" width={24} height={24} />
Jazz BetterAuth Demo
</Link>
{children}
</div>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { ResetPasswordForm } from "@/components/reset-password-form";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Reset password | Jazz Example: Better Auth",
};
export default function ResetPasswordPage() {
return <ResetPasswordForm />;
}

View File

@@ -1,11 +0,0 @@
import { SigninForm } from "@/components/signin-form";
import type { Metadata } from "next";
import { ssoProviders } from "../sso-providers";
export const metadata: Metadata = {
title: "Sign in | Jazz Example: Better Auth",
};
export default function LoginPage() {
return <SigninForm providers={ssoProviders} />;
}

View File

@@ -1,11 +0,0 @@
import { SignupForm } from "@/components/signup-form";
import type { Metadata } from "next";
import { ssoProviders } from "../sso-providers";
export const metadata: Metadata = {
title: "Sign up | Jazz Example: Better Auth",
};
export default function LoginPage() {
return <SignupForm providers={ssoProviders} />;
}

View File

@@ -1,4 +0,0 @@
import type { SSOProviderType } from "jazz-react-auth-betterauth";
// Fill in the providers you want to use
export const ssoProviders: SSOProviderType[] = ["github"];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,123 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,26 +0,0 @@
import { JazzAndAuth } from "@/components/JazzAndAuth";
import { Toaster } from "@/components/ui/sonner";
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Jazz Example: Better Auth",
description: "Jazz example application demonstrating Better Auth integration",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="antialiased">
<JazzAndAuth>
{children}
<Toaster richColors />
</JazzAndAuth>
</body>
</html>
);
}

View File

@@ -1,33 +0,0 @@
"use client";
import { AuthProvider } from "jazz-react-auth-betterauth";
import { JazzReactProvider } from "jazz-tools/react";
import { type ReactNode, lazy } from "react";
const JazzDevTools =
process.env.NODE_ENV === "production"
? () => null
: lazy(() =>
import("jazz-tools/inspector").then((res) => ({
default: res.JazzInspector,
})),
);
export function JazzAndAuth({ children }: { children: ReactNode }) {
return (
<JazzReactProvider
sync={{
peer: "wss://cloud.jazz.tools/?key=betterauth-example@garden.co",
}}
>
<AuthProvider
options={{
baseURL: process.env.NEXT_PUBLIC_AUTH_BASE_URL,
}}
>
{children}
</AuthProvider>
<JazzDevTools />
</JazzReactProvider>
);
}

View File

@@ -1,137 +0,0 @@
import { Button } from "@/components/ui/button";
import {
SiApple,
SiDiscord,
SiDropbox,
SiFacebook,
SiGithub,
SiGitlab,
SiGoogle,
SiKick,
SiReddit,
SiRoblox,
SiSpotify,
SiTiktok,
SiTwitch,
SiVk,
SiX,
SiZoom,
} from "@icons-pack/react-simple-icons";
import { type SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { toast } from "sonner";
interface SocialProvider {
name: string;
icon?: ReactNode;
}
const socialProviderMap: Record<SSOProviderType, SocialProvider> = {
github: {
name: "GitHub",
icon: <SiGithub />,
},
google: {
name: "Google",
icon: <SiGoogle />,
},
apple: {
name: "Apple",
icon: <SiApple />,
},
discord: {
name: "Discord",
icon: <SiDiscord />,
},
facebook: {
name: "Facebook",
icon: <SiFacebook />,
},
microsoft: {
name: "Microsoft",
},
twitter: {
name: "X",
icon: <SiX />,
},
dropbox: {
name: "Dropbox",
icon: <SiDropbox />,
},
linkedin: {
name: "LinkedIn",
},
gitlab: {
name: "GitLab",
icon: <SiGitlab />,
},
kick: {
name: "Kick",
icon: <SiKick />,
},
tiktok: {
name: "TikTok",
icon: <SiTiktok />,
},
twitch: {
name: "Twitch",
icon: <SiTwitch />,
},
vk: {
name: "VK",
icon: <SiVk />,
},
zoom: {
name: "Zoom",
icon: <SiZoom />,
},
roblox: {
name: "Roblox",
icon: <SiRoblox />,
},
reddit: {
name: "Reddit",
icon: <SiReddit />,
},
spotify: {
name: "Spotify",
icon: <SiSpotify />,
},
};
interface Props {
provider: SSOProviderType;
}
export function SSOButton({ provider }: Props) {
const auth = useAuth();
const router = useRouter();
return (
<Button
type="button"
variant="outline"
onClick={() => {
auth.authClient.signIn.social(
{
provider,
},
{
onSuccess: () => {
router.push("/");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
}}
>
{socialProviderMap[provider].icon}
Continue with {socialProviderMap[provider].name}
</Button>
);
}

View File

@@ -1,85 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
export function ForgotPasswordForm() {
const router = useRouter();
const [email, setEmail] = useState("");
const auth = useAuth();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
auth.authClient.forgetPassword(
{ email, redirectTo: `${window.location.origin}/auth/reset-password` },
{
onSuccess: () => {
toast.success("Email sent");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Recover password</CardTitle>
<CardDescription>
Enter your email to receive a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Recover password
</Button>
</div>
<div className="text-center text-sm">
Back to{" "}
<Link
href="/auth/sign-in"
className="underline underline-offset-4"
>
Sign in
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,55 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { useAuth } from "jazz-react-auth-betterauth";
import { Account } from "jazz-tools";
import { useAccount, useIsAuthenticated } from "jazz-tools/react";
import Image from "next/image";
import Link from "next/link";
import { useCallback } from "react";
export function Navbar() {
const { authClient } = useAuth();
const { logOut } = useAccount(Account, { resolve: { profile: {} } });
const isAuthenticated = useIsAuthenticated();
const signOut = useCallback(() => {
authClient.signOut().catch(console.error).finally(logOut);
}, [logOut, authClient]);
return (
<header className="absolute p-4 top-0 left-0 w-full z-10 flex justify-between">
<nav className="flex gap-4">
<Link href="/">
<Image
src="/jazz-logo.svg"
alt="Jazz logo"
width={96}
height={96}
priority
/>
</Link>
{isAuthenticated && (
<Button asChild variant="link">
<Link href="/settings">Settings</Link>
</Button>
)}
</nav>
<nav className="flex gap-4">
{isAuthenticated ? (
<Button onClick={signOut}>Sign out</Button>
) : (
<>
<Button asChild variant="secondary">
<Link href="/auth/sign-in">Sign in</Link>
</Button>
<Button asChild>
<Link href="/auth/sign-up">Sign up</Link>
</Button>
</>
)}
</nav>
</header>
);
}

View File

@@ -1,108 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
export function ResetPasswordForm() {
const router = useRouter();
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const auth = useAuth();
const searchParams = useSearchParams();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const token = searchParams.get("token");
if (password !== confirmPassword) {
toast.error("Passwords do not match");
return;
}
if (!token) {
toast.error("Invalid token");
return;
}
auth.authClient.resetPassword(
{ newPassword: password, token },
{
onSuccess: () => {
toast.success("Password successfully updated");
router.push("/auth/sign-in");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Reset password</CardTitle>
<CardDescription>Enter your new password</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="confirmPassword">Confirm password</Label>
<Input
id="confirmPassword"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Set password
</Button>
</div>
<div className="text-center text-sm">
Back to{" "}
<Link
href="/auth/sign-in"
className="underline underline-offset-4"
>
Sign in
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,124 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { SSOButton } from "./SSOButton";
interface Props {
providers: SSOProviderType[];
}
export function SigninForm({ providers }: Props) {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const auth = useAuth();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await auth.authClient.signIn.email(
{ email, password },
{
onSuccess: async () => {
await auth.logIn();
router.push("/");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Welcome back</CardTitle>
<CardDescription>
Sign in with one of the following providers
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
{providers.length > 0 && (
<>
<div className="flex flex-col gap-4">
{providers?.map((provider) => (
<SSOButton key={provider} provider={provider} />
))}
</div>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
</>
)}
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-3">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
href="/auth/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Sign in
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<Link
href="/auth/sign-up"
className="underline underline-offset-4"
>
Sign up
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,151 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { type SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { SSOButton } from "./SSOButton";
interface Props {
providers: SSOProviderType[];
}
export function SignupForm({ providers }: Props) {
const router = useRouter();
const auth = useAuth();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (password !== confirmPassword) {
toast.error("Error", {
description: "Passwords do not match",
});
return;
}
await auth.authClient.signUp.email(
{
email,
password,
name,
},
{
onSuccess: async () => {
await auth.signIn();
router.push("/");
},
onError: (error) => {
toast.error("Sign up error", {
description: error.error.message,
});
},
},
);
};
return (
<div className="flex flex-col gap-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Welcome</CardTitle>
<CardDescription>
Sign up with one of the following providers
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
{providers.length > 0 && (
<>
<div className="flex flex-col gap-4">
{providers?.map((provider) => (
<SSOButton key={provider} provider={provider} />
))}
</div>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
</>
)}
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="********"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-3">
<Label htmlFor="confirmPassword">Confirm password</Label>
<Input
id="confirmPassword"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full">
Sign up
</Button>
</div>
<div className="text-center text-sm">
Already have an account?{" "}
<Link
href="/auth/sign-in"
className="underline underline-offset-4"
>
Sign in
</Link>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,59 +0,0 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -1,92 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -1,21 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -1,22 +0,0 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -1,22 +0,0 @@
"use client";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
theme="light"
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@@ -1,60 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { useAuth } from "jazz-react-auth-betterauth";
import { useAccount, useIsAuthenticated } from "jazz-tools/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
export function UserSettings() {
const router = useRouter();
const { authClient } = useAuth();
const { logOut } = useAccount();
const isAuthenticated = useIsAuthenticated();
if (!isAuthenticated) {
return (
<div className="flex flex-col gap-8">
<h1 className="text-center text-2xl">Forbidden</h1>
<p className="text-center text-sm text-muted-foreground">
Please{" "}
<Link href="/auth/sign-in" className="underline text-primary">
sign in
</Link>{" "}
to access this page.
</p>
</div>
);
}
return (
<>
<div className="flex flex-col gap-8">
<h1 className="text-4xl font-semibold">Settings</h1>
<Button
variant="destructive"
className="relative"
type="button"
onClick={async (e) => {
e.preventDefault();
authClient.deleteUser(undefined, {
onSuccess: () => {
logOut();
router.push("/");
},
onError: (error) => {
toast.error("Error", {
description: error.error.message,
});
},
});
}}
>
Delete account
</Button>
</div>
</>
);
}

View File

@@ -1,50 +0,0 @@
import { betterAuth } from "better-auth";
import { getMigrations } from "better-auth/db";
import Database from "better-sqlite3";
import { jazzPlugin } from "jazz-betterauth-server-plugin";
import { socialProviders } from "./socialProviders";
export const auth = await (async () => {
// Configure Better Auth server
const auth = betterAuth({
appName: "Jazz Example: Better Auth",
database: new Database("sqlite.db"),
emailAndPassword: {
enabled: true,
async sendResetPassword({ url }) {
// Here we can send an email to the user with the reset password link
console.log("****** RESET PASSWORD ******");
console.log("navigate to", url, "to reset your password");
console.log("******");
},
},
emailVerification: {
async sendVerificationEmail(data) {
console.error("Not implemented");
},
},
socialProviders,
user: {
deleteUser: {
enabled: true,
},
},
plugins: [jazzPlugin()],
databaseHooks: {
user: {
create: {
async after(user) {
// Here we can send a welcome email to the user
console.error("Not implemented");
},
},
},
},
});
// Run database migrations
const migrations = await getMigrations(auth.options);
await migrations.runMigrations();
return auth;
})();

View File

@@ -1,186 +0,0 @@
const apple =
process.env.APPLE_CLIENT_ID &&
process.env.APPLE_CLIENT_SECRET &&
process.env.APPLE_APP_BUNDLE_IDENTIFIER
? {
apple: {
clientId: process.env.APPLE_CLIENT_ID as string,
clientSecret: process.env.APPLE_CLIENT_SECRET as string,
// For native iOS
appBundleIdentifier: process.env
.APPLE_APP_BUNDLE_IDENTIFIER as string,
},
}
: undefined;
const discord =
process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET
? {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
}
: undefined;
const facebook =
process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET
? {
facebook: {
clientId: process.env.FACEBOOK_CLIENT_ID as string,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string,
},
}
: undefined;
const github =
process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
? {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
}
: undefined;
const google =
process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
? {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
}
: undefined;
const kick =
process.env.KICK_CLIENT_ID && process.env.KICK_CLIENT_SECRET
? {
kick: {
clientId: process.env.KICK_CLIENT_ID as string,
clientSecret: process.env.KICK_CLIENT_SECRET as string,
},
}
: undefined;
const microsoft =
process.env.MICROSOFT_CLIENT_ID && process.env.MICROSOFT_CLIENT_SECRET
? {
microsoft: {
clientId: process.env.MICROSOFT_CLIENT_ID as string,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string,
tenantId: "common",
requireSelectAccount: true,
},
}
: undefined;
const tiktok =
process.env.TIKTOK_CLIENT_ID &&
process.env.TIKTOK_CLIENT_SECRET &&
process.env.TIKTOK_CLIENT_KEY
? {
tiktok: {
clientId: process.env.TIKTOK_CLIENT_ID,
clientSecret: process.env.TIKTOK_CLIENT_SECRET,
clientKey: process.env.TIKTOK_CLIENT_KEY,
},
}
: undefined;
const twitch =
process.env.TWITCH_CLIENT_ID && process.env.TWITCH_CLIENT_SECRET
? {
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
}
: undefined;
const twitter =
process.env.TWITTER_CLIENT_ID && process.env.TWITTER_CLIENT_SECRET
? {
twitter: {
clientId: process.env.TWITTER_CLIENT_ID,
clientSecret: process.env.TWITTER_CLIENT_SECRET,
},
}
: undefined;
const dropbox =
process.env.DROPBOX_CLIENT_ID && process.env.DROPBOX_CLIENT_SECRET
? {
dropbox: {
clientId: process.env.DROPBOX_CLIENT_ID as string,
clientSecret: process.env.DROPBOX_CLIENT_SECRET as string,
},
}
: undefined;
const linkedin =
process.env.LINKEDIN_CLIENT_ID && process.env.LINKEDIN_CLIENT_SECRET
? {
linkedin: {
clientId: process.env.LINKEDIN_CLIENT_ID as string,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string,
},
}
: undefined;
const gitlab =
process.env.GITLAB_CLIENT_ID &&
process.env.GITLAB_CLIENT_SECRET &&
process.env.GITLAB_ISSUER
? {
gitlab: {
clientId: process.env.GITLAB_CLIENT_ID as string,
clientSecret: process.env.GITLAB_CLIENT_SECRET as string,
issuer: process.env.GITLAB_ISSUER as string,
},
}
: undefined;
const reddit =
process.env.REDDIT_CLIENT_ID && process.env.REDDIT_CLIENT_SECRET
? {
reddit: {
clientId: process.env.REDDIT_CLIENT_ID as string,
clientSecret: process.env.REDDIT_CLIENT_SECRET as string,
},
}
: undefined;
const roblox =
process.env.ROBLOX_CLIENT_ID && process.env.ROBLOX_CLIENT_SECRET
? {
roblox: {
clientId: process.env.ROBLOX_CLIENT_ID,
clientSecret: process.env.ROBLOX_CLIENT_SECRET,
},
}
: undefined;
const spotify =
process.env.SPOTIFY_CLIENT_ID && process.env.SPOTIFY_CLIENT_SECRET
? {
spotify: {
clientId: process.env.SPOTIFY_CLIENT_ID as string,
clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string,
},
}
: undefined;
const vk =
process.env.VK_CLIENT_ID && process.env.VK_CLIENT_SECRET
? {
vk: {
clientId: process.env.VK_CLIENT_ID as string,
clientSecret: process.env.VK_CLIENT_SECRET as string,
},
}
: undefined;
export const socialProviders = {
...(apple && { ...apple }),
...(discord && { ...discord }),
...(facebook && { ...facebook }),
...(github && { ...github }),
...(google && { ...google }),
...(kick && { ...kick }),
...(microsoft && { ...microsoft }),
...(tiktok && { ...tiktok }),
...(twitch && { ...twitch }),
...(twitter && { ...twitter }),
...(dropbox && { ...dropbox }),
...(linkedin && { ...linkedin }),
...(gitlab && { ...gitlab }),
...(reddit && { ...reddit }),
...(roblox && { ...roblox }),
...(spotify && { ...spotify }),
...(vk && { ...vk }),
};

View File

@@ -1,28 +0,0 @@
import { randomBytes } from "node:crypto";
import { test } from "@playwright/test";
import { HomePage } from "./pages/HomePage";
test("should sign up, sign in, and logout", async ({ page }) => {
const username = randomBytes(4).toString("hex");
const email = `${username}@example.com`;
const password = randomBytes(8).toString("hex");
// Sign up
await page.goto("/");
const homePage = new HomePage(page);
await homePage.expectLoggedOut();
await homePage.signUpLink.click();
await homePage.signUpEmail(username, email, password);
await homePage.expectLoggedIn(username);
// Log out & sign in
await homePage.logout();
await homePage.expectLoggedOut();
await homePage.signInLink.click();
await homePage.signInEmail(email, password);
await homePage.expectLoggedIn(username);
// Logout
await homePage.logout();
await homePage.expectLoggedOut();
});

View File

@@ -1,76 +0,0 @@
import { type Page, expect } from "@playwright/test";
export class HomePage {
constructor(public page: Page) {}
usernameInput = this.page.getByRole("textbox", {
name: "Name",
exact: true,
});
emailInput = this.page.getByRole("textbox", {
name: "Email",
exact: true,
});
passwordInput = this.page.getByRole("textbox", {
name: "Password",
exact: true,
});
confirmPasswordInput = this.page.getByRole("textbox", {
name: "Confirm password",
exact: true,
});
signUpButton = this.page.getByRole("button", {
name: "Sign up",
exact: true,
});
signInButton = this.page.getByRole("button", {
name: "Sign in",
exact: true,
});
signUpLink = this.page.getByRole("link", {
name: "Sign up",
exact: true,
});
signInLink = this.page.getByRole("link", {
name: "Sign in",
exact: true,
});
logoutButton = this.page.getByRole("button", {
name: "Sign out",
exact: true,
});
async signUpEmail(name: string, email: string, password: string) {
await this.usernameInput.fill(name);
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.confirmPasswordInput.fill(password);
await this.signUpButton.click();
}
async signInEmail(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.signInButton.click();
}
async logout() {
await this.logoutButton.click();
}
async expectLoggedIn(name?: string) {
await expect(this.logoutButton).toBeVisible();
await expect(this.signInLink).not.toBeVisible();
await expect(this.signUpLink).not.toBeVisible();
if (name) {
await expect(this.page.getByText(`Signed in as ${name}`)).toBeVisible();
}
}
async expectLoggedOut() {
await expect(this.logoutButton).not.toBeVisible();
await expect(this.signInLink).toBeVisible();
await expect(this.signUpLink).toBeVisible();
await expect(this.page.getByText(`Anonymous user`)).toBeVisible();
}
}

View File

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

17
examples/chat-rn-clerk/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store
ios
android

View File

@@ -0,0 +1,698 @@
# chat-rn-clerk
## 1.0.75
### Patch Changes
- Updated dependencies [3405d8f]
- jazz-react-native@0.10.10
- jazz-react-native-auth-clerk@0.10.10
## 1.0.74
### Patch Changes
- jazz-react-native-auth-clerk@0.10.9
## 1.0.73
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react-native@0.10.8
- jazz-react-native-auth-clerk@0.10.8
- jazz-react-native-media-images@0.10.8
## 1.0.72
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react-native@0.10.7
- jazz-tools@0.10.7
- jazz-react-native-auth-clerk@0.10.7
- jazz-react-native-media-images@0.10.7
## 1.0.71
### Patch Changes
- Updated dependencies [ada802b]
- jazz-tools@0.10.6
- jazz-react-native@0.10.6
- jazz-react-native-auth-clerk@0.10.6
- jazz-react-native-media-images@0.10.6
## 1.0.70
### Patch Changes
- Updated dependencies [59ff77e]
- jazz-tools@0.10.5
- jazz-react-native@0.10.5
- jazz-react-native-auth-clerk@0.10.5
- jazz-react-native-media-images@0.10.5
## 1.0.69
### Patch Changes
- jazz-react-native@0.10.4
- jazz-react-native-auth-clerk@0.10.4
- jazz-tools@0.10.4
- jazz-react-native-media-images@0.10.4
## 1.0.68
### Patch Changes
- Updated dependencies [d8582fc]
- jazz-tools@0.10.3
- jazz-react-native@0.10.3
- jazz-react-native-auth-clerk@0.10.3
- jazz-react-native-media-images@0.10.3
## 1.0.67
### Patch Changes
- jazz-react-native@0.10.2
- jazz-react-native-auth-clerk@0.10.2
- jazz-tools@0.10.2
- jazz-react-native-media-images@0.10.2
## 1.0.66
### Patch Changes
- Updated dependencies [5a63cba]
- jazz-tools@0.10.1
- jazz-react-native@0.10.1
- jazz-react-native-auth-clerk@0.10.1
- jazz-react-native-media-images@0.10.1
## 1.0.65
### Patch Changes
- Updated dependencies [498954f]
- Updated dependencies [d42c2aa]
- Updated dependencies [dd03464]
- Updated dependencies [b426342]
- jazz-react-native-auth-clerk@0.10.0
- jazz-react-native@0.10.0
- jazz-tools@0.10.0
- jazz-react-native-media-images@0.10.0
## 1.0.64
### Patch Changes
- jazz-react-native@0.9.23
- jazz-react-native-auth-clerk@0.9.23
- jazz-tools@0.9.23
- jazz-react-native-media-images@0.9.23
## 1.0.63
### Patch Changes
- jazz-react-native@0.9.22
- jazz-react-native-auth-clerk@0.9.22
## 1.0.62
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react-native@0.9.21
- jazz-react-native-auth-clerk@0.9.21
- jazz-react-native-media-images@0.9.21
## 1.0.61
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react-native@0.9.20
- jazz-react-native-auth-clerk@0.9.20
- jazz-react-native-media-images@0.9.20
## 1.0.60
### Patch Changes
- jazz-react-native@0.9.19
- jazz-react-native-auth-clerk@0.9.19
- jazz-tools@0.9.19
- jazz-react-native-media-images@0.9.19
## 1.0.59
### Patch Changes
- jazz-react-native@0.9.18
- jazz-react-native-auth-clerk@0.9.18
- jazz-tools@0.9.18
- jazz-react-native-media-images@0.9.18
## 1.0.58
### Patch Changes
- Updated dependencies [c2ca1fe]
- Updated dependencies [1227047]
- jazz-tools@0.9.17
- jazz-react-native@0.9.17
- jazz-react-native-auth-clerk@0.9.17
- jazz-react-native-media-images@0.9.17
## 1.0.57
### Patch Changes
- Updated dependencies [24b3b6a]
- jazz-react-native-auth-clerk@0.9.16
- jazz-tools@0.9.16
- jazz-react-native@0.9.16
- jazz-react-native-media-images@0.9.16
## 1.0.56
### Patch Changes
- Updated dependencies [7491711]
- jazz-tools@0.9.15
- jazz-react-native@0.9.15
- jazz-react-native-auth-clerk@0.9.15
- jazz-react-native-media-images@0.9.15
## 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
- jazz-react-native@0.9.11
- jazz-react-native-auth-clerk@0.9.11
- jazz-tools@0.9.11
- jazz-react-native-media-images@0.9.11
## 1.0.51
### Patch Changes
- f76274c: Fix image handling in react-native
- Updated dependencies [f76274c]
- Updated dependencies [5e83864]
- jazz-react-native@0.9.10
- jazz-tools@0.9.10
- jazz-react-native-auth-clerk@0.9.10
- jazz-react-native-media-images@0.9.10
## 1.0.50
### Patch Changes
- Updated dependencies [8eb9247]
- jazz-tools@0.9.9
- jazz-react-native@0.9.9
- jazz-react-native-auth-clerk@0.9.9
- jazz-react-native-media-images@0.9.9
## 1.0.49
### Patch Changes
- Updated dependencies [d1d773b]
- jazz-tools@0.9.8
- jazz-react-native@0.9.8
- jazz-react-native-auth-clerk@0.9.8
- jazz-react-native-media-images@0.9.8
## 1.0.48
### Patch Changes
- Updated dependencies [8a390d2]
- jazz-react-native@0.9.6
- jazz-react-native-auth-clerk@0.9.6
## 1.0.47
### Patch Changes
- Updated dependencies [c871912]
- jazz-react-native@0.9.5
- jazz-react-native-auth-clerk@0.9.5
## 1.0.46
### Patch Changes
- jazz-react-native@0.9.4
- jazz-react-native-auth-clerk@0.9.4
## 1.0.45
### Patch Changes
- Updated dependencies [7cd691f]
- jazz-react-native@0.9.3
- jazz-react-native-auth-clerk@0.9.3
## 1.0.44
### Patch Changes
- Updated dependencies [80fd3e9]
- jazz-react-native@0.9.2
- jazz-react-native-auth-clerk@0.9.2
## 1.0.43
### Patch Changes
- Updated dependencies [1b71969]
- jazz-tools@0.9.1
- jazz-react-native@0.9.1
- jazz-react-native-auth-clerk@0.9.1
- jazz-react-native-media-images@0.9.1
## 1.0.42
### Patch Changes
- Updated dependencies [1da4d55]
- Updated dependencies [8eda792]
- Updated dependencies [1e5e3a1]
- jazz-react-native@0.9.0
- jazz-tools@0.9.0
- jazz-react-native-auth-clerk@0.9.0
- jazz-react-native-media-images@0.9.0
## 1.0.41
### Patch Changes
- Updated dependencies [dc62b95]
- Updated dependencies [1de26f8]
- jazz-tools@0.8.51
- jazz-react-native@0.8.51
- jazz-react-native-auth-clerk@0.8.51
- jazz-react-native-media-images@0.8.51
## 1.0.40
### Patch Changes
- jazz-react-native@0.8.50
- jazz-react-native-auth-clerk@0.8.50
- jazz-tools@0.8.50
- jazz-react-native-media-images@0.8.50
## 1.0.39
### Patch Changes
- jazz-react-native@0.8.49
- jazz-react-native-auth-clerk@0.8.49
- jazz-tools@0.8.49
- jazz-react-native-media-images@0.8.49
## 1.0.38
### Patch Changes
- Updated dependencies [635e824]
- Updated dependencies [0a85982]
- jazz-tools@0.8.48
- jazz-react-native@0.8.48
- jazz-react-native-auth-clerk@0.8.48
- jazz-react-native-media-images@0.8.48
## 1.0.37
### Patch Changes
- Updated dependencies [33ef9c4]
- jazz-react-native@0.8.47
- jazz-react-native-auth-clerk@0.8.47
## 1.0.36
### Patch Changes
- Updated dependencies [ab4ffbd]
- jazz-react-native@0.8.46
- jazz-react-native-auth-clerk@0.8.46
## 1.0.35
### Patch Changes
- Updated dependencies [7701307]
- Updated dependencies [fa41f8e]
- Updated dependencies [88d7d9a]
- Updated dependencies [60e35ea]
- jazz-react-native@0.8.45
- jazz-tools@0.8.45
- jazz-react-native-auth-clerk@0.8.45
- jazz-react-native-media-images@0.8.45
## 1.0.34
### Patch Changes
- jazz-react-native@0.8.44
- jazz-react-native-auth-clerk@0.8.44
- jazz-tools@0.8.44
- jazz-react-native-media-images@0.8.44
## 1.0.33
### Patch Changes
- cdc7f9f: Fixing react-native examples
- Updated dependencies [cdc7f9f]
- jazz-react-native-auth-clerk@0.8.43
## 1.0.32
### Patch Changes
- jazz-react-native@0.8.41
- jazz-react-native-auth-clerk@0.8.41
- jazz-tools@0.8.41
- jazz-react-native-media-images@0.8.41
## 1.0.31
### Patch Changes
- Updated dependencies [0c6b0f3]
- Updated dependencies [249eecb]
- jazz-react-native@0.8.39
- jazz-tools@0.8.39
- jazz-react-native-auth-clerk@0.8.39
- jazz-react-native-media-images@0.8.39
## 1.0.30
### Patch Changes
- jazz-react-native@0.8.38
- jazz-react-native-auth-clerk@0.8.38
- jazz-tools@0.8.38
- jazz-react-native-media-images@0.8.38
## 1.0.29
### Patch Changes
- jazz-react-native@0.8.37
- jazz-react-native-auth-clerk@0.8.37
- jazz-tools@0.8.37
- jazz-react-native-media-images@0.8.28
## 1.0.28
### Patch Changes
- c84764a: feat: added jazz-react-native-auth-clerk package
- Updated dependencies [c84764a]
- Updated dependencies [441fe27]
- jazz-react-native-auth-clerk@0.8.36
- jazz-react-native@0.8.36
- jazz-tools@0.8.36
- jazz-react-native-media-images@0.8.27
## 1.0.27
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.35
- jazz-react-auth-clerk@0.8.35
- jazz-react-native@0.8.35
- jazz-react-native-media-images@0.8.26
## 1.0.26
### Patch Changes
- Updated dependencies [9ca25d1]
- jazz-react-auth-clerk@0.8.34
- jazz-react-native@0.8.34
- jazz-tools@0.8.34
- jazz-react-native-media-images@0.8.25
## 1.0.25
### Patch Changes
- jazz-react-auth-clerk@0.8.33
## 1.0.24
### Patch Changes
- Updated dependencies [1a4bda0]
- Updated dependencies [df42b2b]
- jazz-react-auth-clerk@0.8.32
- jazz-tools@0.8.32
- jazz-react-native@0.8.32
- jazz-react-native-media-images@0.8.24
## 1.0.23
### Patch Changes
- jazz-react-auth-clerk@0.8.31
- jazz-react-native@0.8.31
- jazz-tools@0.8.31
- jazz-react-native-media-images@0.8.23
## 1.0.22
### Patch Changes
- jazz-react-auth-clerk@0.8.30
- jazz-react-native@0.8.30
- jazz-tools@0.8.30
- jazz-react-native-media-images@0.8.22
## 1.0.21
### Patch Changes
- jazz-react-native@0.8.29
- jazz-react-auth-clerk@0.8.29
- jazz-tools@0.8.29
- jazz-react-native-media-images@0.8.21
## 1.0.20
### Patch Changes
- jazz-react-auth-clerk@0.8.28
- jazz-react-native@0.8.28
- jazz-tools@0.8.28
- jazz-react-native-media-images@0.8.20
## 1.0.19
### Patch Changes
- jazz-react-auth-clerk@0.8.27
- jazz-react-native@0.8.27
- jazz-tools@0.8.27
- jazz-react-native-media-images@0.8.19
## 1.0.18
### Patch Changes
- jazz-react-auth-clerk@0.8.26
## 1.0.17
### Patch Changes
- jazz-react-auth-clerk@0.8.24
## 1.0.16
### Patch Changes
- Updated dependencies [d348c2d]
- Updated dependencies [6902b5b]
- Updated dependencies [1a0cd3d]
- jazz-tools@0.8.23
- jazz-react-auth-clerk@0.8.23
- jazz-react-native@0.8.23
- jazz-react-native-media-images@0.8.18
## 1.0.15
### Patch Changes
- jazz-react-auth-clerk@0.8.22
## 1.0.14
### Patch Changes
- Updated dependencies [149ca97]
- jazz-tools@0.8.21
- jazz-react-auth-clerk@0.8.21
- jazz-react-native@0.8.21
- jazz-react-native-media-images@0.8.17
## 1.0.13
### Patch Changes
- Updated dependencies [3ef3ff3]
- jazz-react-native-media-images@0.8.16
- jazz-react-native@0.8.20
- jazz-react-auth-clerk@0.8.20
## 1.0.12
### Patch Changes
- jazz-react-auth-clerk@0.8.19
- jazz-react-native@0.8.19
- jazz-tools@0.8.19
- jazz-react-native-media-images@0.8.15
## 1.0.11
### Patch Changes
- jazz-react-auth-clerk@0.8.18
- jazz-react-native@0.8.18
- jazz-tools@0.8.18
- jazz-react-native-media-images@0.8.14
## 1.0.10
### Patch Changes
- jazz-react-auth-clerk@0.8.17
- jazz-react-native@0.8.17
- jazz-tools@0.8.17
- jazz-react-native-media-images@0.8.13
## 1.0.9
### Patch Changes
- jazz-react-auth-clerk@0.8.16
- jazz-react-native@0.8.16
- jazz-tools@0.8.16
- jazz-react-native-media-images@0.8.12
## 1.0.8
### Patch Changes
- Updated dependencies [cce679b]
- Updated dependencies [221c58f]
- jazz-tools@0.8.15
- jazz-react-auth-clerk@0.8.15
- jazz-react-native@0.8.15
- jazz-react-native-media-images@0.8.11
## 1.0.7
### Patch Changes
- Updated dependencies [36273b3]
- jazz-tools@0.8.14
- jazz-react-auth-clerk@0.8.14
- jazz-react-native@0.8.14
- jazz-react-native-media-images@0.8.10
## 1.0.6
### Patch Changes
- Updated dependencies [fd011d7]
- jazz-tools@0.8.13
- jazz-react-auth-clerk@0.8.13
- jazz-react-native@0.8.13
- jazz-react-native-media-images@0.8.9
## 1.0.5
### Patch Changes
- jazz-react-auth-clerk@0.8.12
- jazz-react-native@0.8.12
- jazz-tools@0.8.12
- jazz-react-native-media-images@0.8.8
## 1.0.4
### Patch Changes
- jazz-react-auth-clerk@0.8.11
- jazz-react-native@0.8.11
- jazz-tools@0.8.11
- jazz-react-native-media-images@0.8.7
## 1.0.3
### Patch Changes
- b7639cf: feat(react-native): replaced react-native-mmkv with expo-secure-store and initialize it by default as kvStore in createJazzRNApp() (BREAKING)
- Updated dependencies [b7639cf]
- jazz-react-native@0.8.8
## 1.0.2
### Patch Changes
- Updated dependencies [32b05b6]
- jazz-react-native-media-images@0.8.6
- jazz-react-native@0.8.7
- jazz-react-auth-clerk@0.8.7
## 1.0.1
### Patch Changes
- jazz-react-native@0.8.6
- jazz-react-auth-clerk@0.8.6

View File

@@ -0,0 +1,24 @@
# 🎷 Jazz + Expo + `expo-router` + Clerk Auth
## 🚀 How to Run
### 1. Inside the Workspace Root
First, install dependencies and build the project:
```bash
pnpm i
pnpm run build
```
### 2. Inside the `examples/chat-rn-clerk` Directory
Next, navigate to the specific example project and run the following commands:
```bash
pnpm expo prebuild
npx pod-install
pnpm expo run:ios
```
This will set up and launch the app on iOS. For Android, you can replace the last command with `pnpm expo run:android`.

View File

@@ -0,0 +1,54 @@
{
"expo": {
"name": "jazz-chat-rn-clerk",
"scheme": "jazz-chat-rn-clerk",
"slug": "jazz-chat-rn-clerk",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.jazz.chatrnclerk"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.jazz.chatrnclerk"
},
"plugins": [
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": true
},
"android": {
"newArchEnabled": true
}
}
],
"expo-secure-store",
"expo-font",
"expo-router",
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you share them with your friends."
}
]
],
"extra": {
"eas": {
"projectId": "ca3d46e5-a10a-47ec-9d77-3b841e1c62d4"
}
}
}
}

View File

@@ -0,0 +1,15 @@
import { Redirect, Stack } from "expo-router";
import { useIsAuthenticated } from "jazz-react-native";
import React from "react";
export default function HomeLayout() {
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <Redirect href={"/chat"} />;
}
return (
<Stack screenOptions={{ headerShown: false, headerBackVisible: true }} />
);
}

View File

@@ -0,0 +1,33 @@
import { SignedOut } from "@clerk/clerk-expo";
import { Link } from "expo-router";
import React from "react";
import { Text, View } from "react-native";
export default function HomePage() {
return (
<View className="flex-1 justify-center items-center bg-gray-100 p-6">
<SignedOut>
<View className="bg-white p-6 rounded-lg shadow-lg w-11/12 max-w-md">
<Text className="text-2xl font-bold text-center text-gray-900 mb-4">
Jazz 🤝 Clerk 🤝 Expo
</Text>
<Link href="/sign-in" className="mb-4">
<Text className="text-center text-blue-600 underline text-lg">
Sign In
</Text>
</Link>
<Link href="/sign-in-oauth" className="mb-4">
<Text className="text-center text-blue-600 underline text-lg">
Sign In OAuth
</Text>
</Link>
<Link href="/sign-up">
<Text className="text-center text-blue-600 underline text-lg">
Sign Up
</Text>
</Link>
</View>
</SignedOut>
</View>
);
}

View File

@@ -0,0 +1,20 @@
import { Redirect, Stack } from "expo-router";
import { useIsAuthenticated } from "jazz-react-native";
export default function UnAuthenticatedLayout() {
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <Redirect href={"/chat"} />;
}
return (
<Stack
screenOptions={{
headerShown: true,
headerBackVisible: true,
headerTitle: "",
}}
/>
);
}

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