Compare commits
8 Commits
changeset-
...
feat/form-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7077d97812 | ||
|
|
25d5fa415b | ||
|
|
9cd8c407d2 | ||
|
|
88d393ac91 | ||
|
|
f04b11572d | ||
|
|
3cbd4eae3b | ||
|
|
63ab6abd4f | ||
|
|
52243161a3 |
@@ -6,7 +6,7 @@
|
||||
"fixed": [
|
||||
[
|
||||
"cojson",
|
||||
"cojson-core-wasm",
|
||||
"cojson-storage",
|
||||
"cojson-storage-indexeddb",
|
||||
"cojson-storage-sqlite",
|
||||
"cojson-transport-ws",
|
||||
@@ -15,8 +15,7 @@
|
||||
"jazz-betterauth-server-plugin",
|
||||
"jazz-react-auth-betterauth",
|
||||
"jazz-run",
|
||||
"jazz-tools",
|
||||
"community-jazz-vue"
|
||||
"jazz-tools"
|
||||
]
|
||||
],
|
||||
"access": "public",
|
||||
|
||||
5
.changeset/hungry-spiders-turn.md
Normal file
5
.changeset/hungry-spiders-turn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
Fix type error with `RNQuickCrypto` that prevented using it as a `CryptoProvider`
|
||||
5
.changeset/nasty-apples-bow.md
Normal file
5
.changeset/nasty-apples-bow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-jazz-app": patch
|
||||
---
|
||||
|
||||
Add default project name
|
||||
7
.changeset/ten-jeans-live.md
Normal file
7
.changeset/ten-jeans-live.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"jazz-react-auth-betterauth": patch
|
||||
"jazz-auth-betterauth": patch
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
Changes that verify the BetterAuth package for React also works with React Native and Expo apps
|
||||
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -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
|
||||
35
.github/pull_request_template.md
vendored
35
.github/pull_request_template.md
vendored
@@ -1,23 +1,24 @@
|
||||
# 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 RFC’s 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 -->
|
||||
### What this Does
|
||||
Brief summary of the change, ideally framed in user or product terms.
|
||||
|
||||
## Manual testing instructions
|
||||
### Why Are We Doing This?
|
||||
Link to the shaped pitch or explain what problem it solves.
|
||||
|
||||
<!-- Add any actions required to manually test the changes -->
|
||||
### Scope / Boundaries
|
||||
Includes:
|
||||
- [x] Core feature functionality
|
||||
- [x] Tests or validation steps
|
||||
|
||||
## Tests
|
||||
Do NOT include:
|
||||
- [ ] Related stretch features or follow-ups
|
||||
|
||||
- [ ] 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
|
||||
### Testing Instructions
|
||||
How a reviewer or QA can verify behavior, offer step-by-step instructions if possible. Screenshots or recordings are welcome.
|
||||
|
||||
### Known Issues / Open Questions (if any)
|
||||
- [ ] Note anything you’d like review on or decided async
|
||||
|
||||
## 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
|
||||
### Related Links
|
||||
- GitHub issue
|
||||
- Linear pitch
|
||||
- Design links or references
|
||||
|
||||
11
.github/workflows/code-quality.yml
vendored
11
.github/workflows/code-quality.yml
vendored
@@ -1,11 +1,5 @@
|
||||
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:
|
||||
@@ -22,6 +16,9 @@ jobs:
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@v2
|
||||
with:
|
||||
version: 2.1.3
|
||||
version: 1.9.4
|
||||
- name: Run Biome
|
||||
run: biome ci .
|
||||
|
||||
- name: Check Catalog Dependencies
|
||||
run: node scripts/check-catalog-deps.js
|
||||
|
||||
77
.github/workflows/create-jazz-app.yml
vendored
77
.github/workflows/create-jazz-app.yml
vendored
@@ -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"
|
||||
6
.github/workflows/e2e-rn-test.yml
vendored
6
.github/workflows/e2e-rn-test.yml
vendored
@@ -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]
|
||||
|
||||
6
.github/workflows/jazz-run.yml
vendored
6
.github/workflows/jazz-run.yml
vendored
@@ -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"]
|
||||
|
||||
46
.github/workflows/playwright-homepage.yml
vendored
Normal file
46
.github/workflows/playwright-homepage.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Source Code
|
||||
uses: ./.github/actions/source-code/
|
||||
|
||||
- name: Install root dependencies
|
||||
run: pnpm install && pnpm exec turbo build --filter="./packages/*"
|
||||
|
||||
- name: Install project dependencies
|
||||
run: pnpm install
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- name: Pnpm Build
|
||||
run: pnpm exec turbo build
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm exec playwright test
|
||||
working-directory: ./homepage/homepage
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: homepage-playwright-report
|
||||
path: ./homepage/homepage/playwright-report/
|
||||
retention-days: 30
|
||||
158
.github/workflows/playwright.yml
vendored
158
.github/workflows/playwright.yml
vendored
@@ -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"]
|
||||
@@ -19,7 +13,21 @@ jobs:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
shard: ["1/2", "2/2"]
|
||||
project: [
|
||||
"tests/e2e",
|
||||
"examples/chat",
|
||||
"examples/chat-svelte",
|
||||
"examples/clerk",
|
||||
"examples/betterauth",
|
||||
"examples/file-share-svelte",
|
||||
"examples/form",
|
||||
"examples/inspector",
|
||||
"examples/music-player",
|
||||
"examples/organization",
|
||||
"starters/react-passkey-auth",
|
||||
"starters/svelte-passkey-auth",
|
||||
"tests/jazz-svelte"
|
||||
]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -29,131 +37,25 @@ jobs:
|
||||
- name: Setup Source Code
|
||||
uses: ./.github/actions/source-code/
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install
|
||||
|
||||
- name: Run Playwright tests for shard ${{ matrix.shard }}
|
||||
- name: Pnpm Build
|
||||
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
|
||||
pnpm turbo build
|
||||
working-directory: ./${{ matrix.project }}
|
||||
|
||||
if ! pnpm turbo build; then
|
||||
echo "❌ BUILD FAILED: $project"
|
||||
failed_projects+=("$project (build failed)")
|
||||
overall_exit_code=1
|
||||
cd - > /dev/null
|
||||
continue
|
||||
fi
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install
|
||||
working-directory: ./${{ matrix.project }}
|
||||
|
||||
# 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
|
||||
- name: Run Playwright tests
|
||||
run: pnpm exec playwright test
|
||||
working-directory: ./${{ matrix.project }}
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
7
.github/workflows/pre-release.yml
vendored
7
.github/workflows/pre-release.yml
vendored
@@ -1,11 +1,4 @@
|
||||
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]
|
||||
|
||||
6
.github/workflows/unit-test.yml
vendored
6
.github/workflows/unit-test.yml
vendored
@@ -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]
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineProject } from "vitest/config";
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: "bench",
|
||||
},
|
||||
});
|
||||
57
biome.json
57
biome.json
@@ -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,39 @@
|
||||
},
|
||||
"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/**",
|
||||
"tests/jazz-svelte/src/**",
|
||||
"examples/*svelte*/**",
|
||||
"starters/*svelte*/**",
|
||||
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
|
||||
"homepage/homepage/**",
|
||||
"**/package.json"
|
||||
]
|
||||
},
|
||||
"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 +47,7 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["packages/community-jazz-vue/src/**"],
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/packages/**/src/**"],
|
||||
"include": ["packages/**/src/**"],
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
@@ -62,10 +56,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"**/packages/cojson/src/storage/**/*/**",
|
||||
"**/cojson-transport-ws/**"
|
||||
],
|
||||
"include": ["packages/cojson-storage*/**", "cojson-transport-ws/**"],
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
@@ -74,7 +65,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/tests/**"],
|
||||
"include": ["**/tests/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
@@ -84,7 +75,7 @@
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
"noExplicitAny": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
crates/.gitignore
vendored
8
crates/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
# Rust
|
||||
/target
|
||||
|
||||
# Test artifacts
|
||||
lzy/compressed_66k.lzy
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
1164
crates/Cargo.lock
generated
1164
crates/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"lzy",
|
||||
"cojson-core",
|
||||
"cojson-core-wasm",
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
# cojson-core-wasm
|
||||
|
||||
## 0.17.11
|
||||
|
||||
## 0.17.10
|
||||
@@ -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"]
|
||||
@@ -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"),
|
||||
);
|
||||
3
crates/cojson-core-wasm/index.d.ts
vendored
3
crates/cojson-core-wasm/index.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
export * from "./public/cojson_core_wasm.js";
|
||||
|
||||
export async function initialize(): Promise<void>;
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
291
crates/cojson-core-wasm/public/cojson_core_wasm.d.ts
vendored
291
crates/cojson-core-wasm/public/cojson_core_wasm.d.ts
vendored
@@ -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
@@ -1 +0,0 @@
|
||||
export const data: string;
|
||||
File diff suppressed because one or more lines are too long
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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(_))));
|
||||
}
|
||||
}
|
||||
@@ -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(_))));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))?)
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
@@ -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":{}}
|
||||
}
|
||||
}
|
||||
@@ -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":{}}}
|
||||
}
|
||||
@@ -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"}]"#
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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 ---");
|
||||
}
|
||||
}
|
||||
@@ -27,22 +27,22 @@
|
||||
"jazz-tools": "workspace:*",
|
||||
"lucide-react": "^0.510.0",
|
||||
"next": "15.3.2",
|
||||
"react": "catalog:react",
|
||||
"react-dom": "catalog:react",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tw-animate-css": "^1.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "catalog:default",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@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",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"react-email": "^4.0.11",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "catalog:default"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
"@bacons/text-decoder": "^0.0.0",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"expo": "catalog:expo",
|
||||
"expo-clipboard": "catalog:expo",
|
||||
"expo-secure-store": "catalog:expo",
|
||||
"expo-sqlite": "catalog:expo",
|
||||
"expo": "~53.0.9",
|
||||
"expo-clipboard": "^7.1.4",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
"expo-sqlite": "~15.2.10",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "catalog:expo",
|
||||
"react-native": "catalog:expo",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"readable-stream": "^4.7.0"
|
||||
},
|
||||
|
||||
@@ -11,11 +11,11 @@ react {
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
reactNativeDir = file("../../../../node_modules/react-native")
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
codegenDir = file("../../../../node_modules/@react-native/codegen")
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||
cliFile = file("../../../../node_modules/react-native/cli.js")
|
||||
// cliFile = file("../../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") }
|
||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||
rootProject.name = 'ChatRN'
|
||||
include ':app'
|
||||
includeBuild('../../../node_modules/@react-native/gradle-plugin')
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
USE_HERMES = true;
|
||||
@@ -452,7 +452,7 @@
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
||||
@@ -2370,87 +2370,87 @@ PODS:
|
||||
- Yoga (0.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
||||
- FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||
- glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- "op-sqlite (from `../../../node_modules/@op-engineering/op-sqlite`)"
|
||||
- RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||
- RCTRequired (from `../../../node_modules/react-native/Libraries/Required`)
|
||||
- RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`)
|
||||
- React (from `../../../node_modules/react-native/`)
|
||||
- React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`)
|
||||
- React-Core (from `../../../node_modules/react-native/`)
|
||||
- React-Core/RCTWebSocket (from `../../../node_modules/react-native/`)
|
||||
- React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`)
|
||||
- React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`)
|
||||
- React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`)
|
||||
- React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
|
||||
- React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
|
||||
- React-Fabric (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- React-FabricImage (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`)
|
||||
- React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
|
||||
- React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`)
|
||||
- React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`)
|
||||
- React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
|
||||
- React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
|
||||
- React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`)
|
||||
- React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`)
|
||||
- React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||
- React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`)
|
||||
- React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
|
||||
- React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
|
||||
- React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
|
||||
- React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`)
|
||||
- React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`)
|
||||
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
||||
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
|
||||
- React (from `../node_modules/react-native/`)
|
||||
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
|
||||
- React-Core (from `../node_modules/react-native/`)
|
||||
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
|
||||
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
|
||||
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
|
||||
- React-debug (from `../node_modules/react-native/ReactCommon/react/debug`)
|
||||
- React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
|
||||
- React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
|
||||
- React-Fabric (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-FabricComponents (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-FabricImage (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`)
|
||||
- React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
|
||||
- React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`)
|
||||
- React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
|
||||
- React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
|
||||
- React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
|
||||
- React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
|
||||
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`)
|
||||
- React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
|
||||
- React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
|
||||
- React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
|
||||
- React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`)
|
||||
- React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`)
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||
- react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`)
|
||||
- react-native-mmkv (from `../../../node_modules/react-native-mmkv`)
|
||||
- "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
|
||||
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`)
|
||||
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
- React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`)
|
||||
- React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||
- React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`)
|
||||
- React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`)
|
||||
- React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`)
|
||||
- React-RCTFabric (from `../../../node_modules/react-native/React`)
|
||||
- React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`)
|
||||
- React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`)
|
||||
- React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`)
|
||||
- React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`)
|
||||
- React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`)
|
||||
- React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`)
|
||||
- React-RCTText (from `../../../node_modules/react-native/Libraries/Text`)
|
||||
- React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`)
|
||||
- React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`)
|
||||
- React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`)
|
||||
- React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`)
|
||||
- React-rncore (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
|
||||
- React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
|
||||
- React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
|
||||
- React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
|
||||
- React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`)
|
||||
- React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`)
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
- React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`)
|
||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
|
||||
- React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`)
|
||||
- React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
|
||||
- React-RCTFabric (from `../node_modules/react-native/React`)
|
||||
- React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`)
|
||||
- React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
|
||||
- React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
|
||||
- React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`)
|
||||
- React-RCTRuntime (from `../node_modules/react-native/React/Runtime`)
|
||||
- React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
|
||||
- React-RCTText (from `../node_modules/react-native/Libraries/Text`)
|
||||
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
|
||||
- React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`)
|
||||
- React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`)
|
||||
- React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`)
|
||||
- React-rncore (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
|
||||
- React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`)
|
||||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`)
|
||||
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
|
||||
- React-timing (from `../node_modules/react-native/ReactCommon/react/timing`)
|
||||
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
|
||||
- ReactAppDependencyProvider (from `build/generated/ios`)
|
||||
- ReactCodegen (from `build/generated/ios`)
|
||||
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- "RNCClipboard (from `../../../node_modules/@react-native-clipboard/clipboard`)"
|
||||
- RNScreens (from `../../../node_modules/react-native-screens`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- SocketRocket (~> 0.7.1)
|
||||
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -2458,88 +2458,88 @@ SPEC REPOS:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
DoubleConversion:
|
||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
fast_float:
|
||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
||||
FBLazyVector:
|
||||
:path: "../../../node_modules/react-native/Libraries/FBLazyVector"
|
||||
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
||||
fmt:
|
||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/fmt.podspec"
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
|
||||
glog:
|
||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec"
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
|
||||
hermes-engine:
|
||||
:podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:tag: hermes-2025-05-06-RNv0.80.0-4eb6132a5bf0450bf4c6c91987675381d7ac8bca
|
||||
op-sqlite:
|
||||
:path: "../../../node_modules/@op-engineering/op-sqlite"
|
||||
RCT-Folly:
|
||||
:podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||
RCTDeprecation:
|
||||
:path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
|
||||
:path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
|
||||
RCTRequired:
|
||||
:path: "../../../node_modules/react-native/Libraries/Required"
|
||||
:path: "../node_modules/react-native/Libraries/Required"
|
||||
RCTTypeSafety:
|
||||
:path: "../../../node_modules/react-native/Libraries/TypeSafety"
|
||||
:path: "../node_modules/react-native/Libraries/TypeSafety"
|
||||
React:
|
||||
:path: "../../../node_modules/react-native/"
|
||||
:path: "../node_modules/react-native/"
|
||||
React-callinvoker:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/callinvoker"
|
||||
:path: "../node_modules/react-native/ReactCommon/callinvoker"
|
||||
React-Core:
|
||||
:path: "../../../node_modules/react-native/"
|
||||
:path: "../node_modules/react-native/"
|
||||
React-CoreModules:
|
||||
:path: "../../../node_modules/react-native/React/CoreModules"
|
||||
:path: "../node_modules/react-native/React/CoreModules"
|
||||
React-cxxreact:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/cxxreact"
|
||||
:path: "../node_modules/react-native/ReactCommon/cxxreact"
|
||||
React-debug:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/debug"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/debug"
|
||||
React-defaultsnativemodule:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
|
||||
React-domnativemodule:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom"
|
||||
React-Fabric:
|
||||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-FabricComponents:
|
||||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-FabricImage:
|
||||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-featureflags:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/featureflags"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/featureflags"
|
||||
React-featureflagsnativemodule:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
|
||||
React-graphics:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/graphics"
|
||||
React-hermes:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/hermes"
|
||||
:path: "../node_modules/react-native/ReactCommon/hermes"
|
||||
React-idlecallbacksnativemodule:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
|
||||
React-ImageManager:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
|
||||
React-jserrorhandler:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jserrorhandler"
|
||||
:path: "../node_modules/react-native/ReactCommon/jserrorhandler"
|
||||
React-jsi:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsi"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsi"
|
||||
React-jsiexecutor:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsiexecutor"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
|
||||
React-jsinspector:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern"
|
||||
React-jsinspectorcdp:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
|
||||
React-jsinspectornetwork:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network"
|
||||
React-jsinspectortracing:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
|
||||
React-jsitooling:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/jsitooling"
|
||||
:path: "../node_modules/react-native/ReactCommon/jsitooling"
|
||||
React-jsitracing:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/hermes/executor/"
|
||||
:path: "../node_modules/react-native/ReactCommon/hermes/executor/"
|
||||
React-logger:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/logger"
|
||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||
React-Mapbuffer:
|
||||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-microtasksnativemodule:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
react-native-get-random-values:
|
||||
:path: "../../../node_modules/react-native-get-random-values"
|
||||
react-native-mmkv:
|
||||
@@ -2547,75 +2547,75 @@ EXTERNAL SOURCES:
|
||||
react-native-netinfo:
|
||||
:path: "../../../node_modules/@react-native-community/netinfo"
|
||||
react-native-safe-area-context:
|
||||
:path: "../../../node_modules/react-native-safe-area-context"
|
||||
:path: "../node_modules/react-native-safe-area-context"
|
||||
React-NativeModulesApple:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||
React-oscompat:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/oscompat"
|
||||
:path: "../node_modules/react-native/ReactCommon/oscompat"
|
||||
React-perflogger:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/reactperflogger"
|
||||
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
|
||||
React-performancetimeline:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/performance/timeline"
|
||||
React-RCTActionSheet:
|
||||
:path: "../../../node_modules/react-native/Libraries/ActionSheetIOS"
|
||||
:path: "../node_modules/react-native/Libraries/ActionSheetIOS"
|
||||
React-RCTAnimation:
|
||||
:path: "../../../node_modules/react-native/Libraries/NativeAnimation"
|
||||
:path: "../node_modules/react-native/Libraries/NativeAnimation"
|
||||
React-RCTAppDelegate:
|
||||
:path: "../../../node_modules/react-native/Libraries/AppDelegate"
|
||||
:path: "../node_modules/react-native/Libraries/AppDelegate"
|
||||
React-RCTBlob:
|
||||
:path: "../../../node_modules/react-native/Libraries/Blob"
|
||||
:path: "../node_modules/react-native/Libraries/Blob"
|
||||
React-RCTFabric:
|
||||
:path: "../../../node_modules/react-native/React"
|
||||
:path: "../node_modules/react-native/React"
|
||||
React-RCTFBReactNativeSpec:
|
||||
:path: "../../../node_modules/react-native/React"
|
||||
:path: "../node_modules/react-native/React"
|
||||
React-RCTImage:
|
||||
:path: "../../../node_modules/react-native/Libraries/Image"
|
||||
:path: "../node_modules/react-native/Libraries/Image"
|
||||
React-RCTLinking:
|
||||
:path: "../../../node_modules/react-native/Libraries/LinkingIOS"
|
||||
:path: "../node_modules/react-native/Libraries/LinkingIOS"
|
||||
React-RCTNetwork:
|
||||
:path: "../../../node_modules/react-native/Libraries/Network"
|
||||
:path: "../node_modules/react-native/Libraries/Network"
|
||||
React-RCTRuntime:
|
||||
:path: "../../../node_modules/react-native/React/Runtime"
|
||||
:path: "../node_modules/react-native/React/Runtime"
|
||||
React-RCTSettings:
|
||||
:path: "../../../node_modules/react-native/Libraries/Settings"
|
||||
:path: "../node_modules/react-native/Libraries/Settings"
|
||||
React-RCTText:
|
||||
:path: "../../../node_modules/react-native/Libraries/Text"
|
||||
:path: "../node_modules/react-native/Libraries/Text"
|
||||
React-RCTVibration:
|
||||
:path: "../../../node_modules/react-native/Libraries/Vibration"
|
||||
:path: "../node_modules/react-native/Libraries/Vibration"
|
||||
React-rendererconsistency:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/consistency"
|
||||
React-renderercss:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/css"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/css"
|
||||
React-rendererdebug:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/debug"
|
||||
React-rncore:
|
||||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-RuntimeApple:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
|
||||
React-RuntimeCore:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/runtime"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/runtime"
|
||||
React-runtimeexecutor:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||
React-RuntimeHermes:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/runtime"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/runtime"
|
||||
React-runtimescheduler:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
|
||||
React-timing:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/timing"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/timing"
|
||||
React-utils:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/react/utils"
|
||||
:path: "../node_modules/react-native/ReactCommon/react/utils"
|
||||
ReactAppDependencyProvider:
|
||||
:path: build/generated/ios
|
||||
ReactCodegen:
|
||||
:path: build/generated/ios
|
||||
ReactCommon:
|
||||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
RNCClipboard:
|
||||
:path: "../../../node_modules/@react-native-clipboard/clipboard"
|
||||
RNScreens:
|
||||
:path: "../../../node_modules/react-native-screens"
|
||||
:path: "../node_modules/react-native-screens"
|
||||
Yoga:
|
||||
:path: "../../../node_modules/react-native/ReactCommon/yoga"
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||
@@ -2692,7 +2692,7 @@ SPEC CHECKSUMS:
|
||||
React-timing: a275a1c2e6112dba17f8f7dd496d439213bbea0d
|
||||
React-utils: 449a6e1fd53886510e284e80bdbb1b1c6db29452
|
||||
ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0
|
||||
ReactCodegen: d82f538f70f00484d418803f74b5a0ea09cc8689
|
||||
ReactCodegen: 5d41e1df061200130dd326e55cdfdf94b0289c6e
|
||||
ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147
|
||||
RNCClipboard: 54ff19965d7c816febbafe5f520c2c3e7b677a49
|
||||
RNScreens: ee2abe7e0c548eed14e92742e81ed991165c56aa
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bacons/text-decoder": "0.0.0",
|
||||
"@op-engineering/op-sqlite": "14.1.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-clipboard/clipboard": "1.16.2",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-navigation/native": "7.1.14",
|
||||
"@react-navigation/native-stack": "7.3.19",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "catalog:rn",
|
||||
"react-native": "catalog:rn",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.80.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-mmkv": "3.3.0",
|
||||
"react-native-safe-area-context": "5.5.0",
|
||||
@@ -31,16 +31,16 @@
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/preset-env": "^7.25.3",
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"@react-native-community/cli": "catalog:rn",
|
||||
"@react-native-community/cli-platform-android": "catalog:rn",
|
||||
"@react-native-community/cli-platform-ios": "catalog:rn",
|
||||
"@react-native/babel-preset": "catalog:rn",
|
||||
"@react-native/eslint-config": "catalog:rn",
|
||||
"@react-native/metro-config": "catalog:rn",
|
||||
"@react-native/typescript-config": "catalog:rn",
|
||||
"@react-native-community/cli": "19.0.0",
|
||||
"@react-native-community/cli-platform-android": "19.0.0",
|
||||
"@react-native-community/cli-platform-ios": "19.0.0",
|
||||
"@react-native/babel-preset": "0.80.0",
|
||||
"@react-native/eslint-config": "0.80.0",
|
||||
"@react-native/metro-config": "0.80.0",
|
||||
"@react-native/typescript-config": "0.80.0",
|
||||
"@rnx-kit/metro-config": "^2.0.1",
|
||||
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
|
||||
"@types/react": "catalog:rn",
|
||||
"@types/react": "19.1.0",
|
||||
"eslint": "^8.19.0",
|
||||
"pod-install": "^0.3.5",
|
||||
"prettier": "2.8.8",
|
||||
|
||||
@@ -76,9 +76,7 @@ export function ChatScreen({ navigation }: { navigation: any }) {
|
||||
|
||||
const renderMessageItem = ({
|
||||
item,
|
||||
}: {
|
||||
item: Loaded<typeof Message, { text: true }>;
|
||||
}) => {
|
||||
}: { item: Loaded<typeof Message, { text: true }> }) => {
|
||||
const isMe = item._edits?.text?.by?.isMe;
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -3,7 +3,11 @@ import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import { Chat } from "./schema";
|
||||
|
||||
export function HandleInviteScreen({ navigation }: { navigation: any }) {
|
||||
export function HandleInviteScreen({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: any;
|
||||
}) {
|
||||
useAcceptInviteNative({
|
||||
invitedObjectSchema: Chat,
|
||||
onAccept: async (chatId) => {
|
||||
|
||||
@@ -1,242 +1,5 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.124
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bb9d837]
|
||||
- jazz-tools@0.17.11
|
||||
|
||||
## 0.0.123
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.17.10
|
||||
|
||||
## 0.0.122
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [52ea0c7]
|
||||
- jazz-tools@0.17.9
|
||||
|
||||
## 0.0.121
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ac3e694]
|
||||
- Updated dependencies [6dbb053]
|
||||
- Updated dependencies [1a182f0]
|
||||
- jazz-tools@0.17.8
|
||||
|
||||
## 0.0.120
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.17.7
|
||||
|
||||
## 0.0.119
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [82de51c]
|
||||
- Updated dependencies [694b168]
|
||||
- jazz-tools@0.17.6
|
||||
|
||||
## 0.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [5963658]
|
||||
- jazz-tools@0.17.5
|
||||
|
||||
## 0.0.117
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [7dd3d00]
|
||||
- jazz-tools@0.17.4
|
||||
|
||||
## 0.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.17.3
|
||||
|
||||
## 0.0.115
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [794681a]
|
||||
- Updated dependencies [83fc22f]
|
||||
- jazz-tools@0.17.2
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0bcbf55]
|
||||
- Updated dependencies [d1bdbf5]
|
||||
- Updated dependencies [4b73834]
|
||||
- jazz-tools@0.17.1
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [fcaf4b9]
|
||||
- jazz-tools@0.17.0
|
||||
|
||||
## 0.0.112
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [67e0968]
|
||||
- Updated dependencies [2c8120d]
|
||||
- jazz-tools@0.16.6
|
||||
|
||||
## 0.0.111
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3cd1586]
|
||||
- Updated dependencies [33ebbf0]
|
||||
- jazz-tools@0.16.5
|
||||
|
||||
## 0.0.110
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [16764f6]
|
||||
- jazz-tools@0.16.4
|
||||
|
||||
## 0.0.109
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [43d3511]
|
||||
- jazz-tools@0.16.3
|
||||
|
||||
## 0.0.108
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.16.2
|
||||
|
||||
## 0.0.107
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c62abef]
|
||||
- jazz-tools@0.16.1
|
||||
|
||||
## 0.0.106
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2bbb07b: Introduce a cleaner separation between Zod and CoValue schemas:
|
||||
- Zod schemas and CoValue schemas are fully separated. Zod schemas can only be composed with other Zod schemas. CoValue schemas can be composed with either Zod or other CoValue schemas.
|
||||
- `z.optional()` and `z.discriminatedUnion()` no longer work with CoValue schemas. Use `co.optional()` and `co.discriminatedUnion()` instead.
|
||||
- Internal schema access is now simpler. You no longer need to use Zod’s `.def` to access internals. Use properties like `CoMapSchema.shape`, `CoListSchema.element`, and `CoOptionalSchema.innerType` directly.
|
||||
- CoValue schema types are now namespaced under `co.`. Non-namespaced exports have been removed
|
||||
- CoMap schemas no longer incorrectly inherit from Zod. Previously, methods like `.extend()` and `.partial()` appeared available but could cause unexpected behavior. These methods are now disabled. In their place, `.optional()` has been added, and more Zod-like methods will be introduced in future releases.
|
||||
- Upgraded Zod from `3.25.28` to `3.25.76`.
|
||||
- Removed deprecated `withHelpers` method from CoValue schemas
|
||||
- Removed deprecated `createCoValueObservable` function
|
||||
- Updated dependencies [c09dcdf]
|
||||
- Updated dependencies [2bbb07b]
|
||||
- jazz-tools@0.16.0
|
||||
|
||||
## 0.0.105
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9633d01]
|
||||
- Updated dependencies [4beafb7]
|
||||
- jazz-tools@0.15.16
|
||||
|
||||
## 0.0.104
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3fe53a3]
|
||||
- jazz-tools@0.15.15
|
||||
|
||||
## 0.0.103
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [a584590]
|
||||
- Updated dependencies [9acccb5]
|
||||
- jazz-tools@0.15.14
|
||||
|
||||
## 0.0.102
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6c76ff8]
|
||||
- jazz-tools@0.15.13
|
||||
|
||||
## 0.0.101
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d1c1b0c]
|
||||
- Updated dependencies [cf4ad72]
|
||||
- jazz-tools@0.15.12
|
||||
|
||||
## 0.0.100
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bdc9aee]
|
||||
- jazz-tools@0.15.11
|
||||
|
||||
## 0.0.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9815ec6]
|
||||
- Updated dependencies [b4fdab4]
|
||||
- jazz-tools@0.15.10
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [27b4837]
|
||||
- jazz-tools@0.15.9
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3844666]
|
||||
- jazz-tools@0.15.8
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c09b636]
|
||||
- jazz-tools@0.15.7
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [a5ceaff]
|
||||
- jazz-tools@0.15.6
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [23bfea5]
|
||||
- Updated dependencies [e4ba23c]
|
||||
- Updated dependencies [4b89838]
|
||||
- jazz-tools@0.15.5
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-svelte",
|
||||
"version": "0.0.124",
|
||||
"version": "0.0.93",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ImageDefinition, type Loaded } from 'jazz-tools';
|
||||
import { Image } from 'jazz-tools/svelte';
|
||||
import { useProgressiveImg } from '$lib/utils/useProgressiveImage.svelte';
|
||||
let { image }: { image: Loaded<typeof ImageDefinition> } = $props();
|
||||
const { src } = $derived(
|
||||
useProgressiveImg({
|
||||
image
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<Image
|
||||
imageId={image.id}
|
||||
alt=""
|
||||
class="h-auto max-h-[20rem] max-w-full rounded-t-xl mb-1"
|
||||
/>
|
||||
<img class="h-auto max-h-[20rem] max-w-full rounded-t-xl mb-1" {src} alt="" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { co } from 'jazz-tools';
|
||||
import { co, z } from 'jazz-tools';
|
||||
|
||||
export const Message = co.map({
|
||||
text: co.plainText(),
|
||||
image: co.optional(co.image())
|
||||
image: z.optional(co.image())
|
||||
});
|
||||
|
||||
export const Chat = co.list(Message);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ImageDefinition, type Loaded } from 'jazz-tools';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export function useProgressiveImg({
|
||||
image,
|
||||
maxWidth,
|
||||
targetWidth
|
||||
}: {
|
||||
image: Loaded<typeof ImageDefinition> | null | undefined;
|
||||
maxWidth?: number;
|
||||
targetWidth?: number;
|
||||
}) {
|
||||
let current = $state<{
|
||||
src?: string;
|
||||
res?: `${number}x${number}` | 'placeholder';
|
||||
}>();
|
||||
const originalSize = $state(image?.originalSize);
|
||||
|
||||
const unsubscribe = image?.subscribe({}, (update: Loaded<typeof ImageDefinition>) => {
|
||||
const highestRes = ImageDefinition.highestResAvailable(update, { maxWidth, targetWidth });
|
||||
if (highestRes) {
|
||||
if (highestRes.res !== current?.res) {
|
||||
const blob = highestRes.stream.toBlob();
|
||||
if (blob) {
|
||||
const blobURI = URL.createObjectURL(blob);
|
||||
current = { src: blobURI, res: highestRes.res };
|
||||
|
||||
setTimeout(() => URL.revokeObjectURL(blobURI), 200);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current = {
|
||||
src: update?.placeholderDataURL,
|
||||
res: 'placeholder'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => () => {
|
||||
unsubscribe?.();
|
||||
});
|
||||
|
||||
return {
|
||||
get src() {
|
||||
return current?.src;
|
||||
},
|
||||
get res() {
|
||||
return current?.res;
|
||||
},
|
||||
|
||||
originalSize
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createImage } from 'jazz-tools/media';
|
||||
import { createImage } from 'jazz-tools/browser-media-images';
|
||||
import { AccountCoState, CoState } from 'jazz-tools/svelte';
|
||||
import { Account, CoPlainText, type ID } from 'jazz-tools';
|
||||
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
"clsx": "^2.0.0",
|
||||
"hash-slash": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"lucide-react": "^0.536.0",
|
||||
"react": "catalog:react",
|
||||
"react-dom": "catalog:react",
|
||||
"zod": "3.25.76"
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"zod": "3.25.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@types/react": "catalog:react",
|
||||
"@types/react-dom": "catalog:react",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||
"is-ci": "^3.0.1",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"typescript": "catalog:default",
|
||||
"vite": "catalog:default"
|
||||
"typescript": "5.6.2",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Account } from "jazz-tools";
|
||||
import { createImage } from "jazz-tools/media";
|
||||
import { useAccount, useCoState } from "jazz-tools/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Account, co } from "jazz-tools";
|
||||
import { createImage, useAccount, useCoState } from "jazz-tools/react";
|
||||
import { useState } from "react";
|
||||
import { Chat, Message } from "./schema.ts";
|
||||
import {
|
||||
BubbleBody,
|
||||
@@ -16,17 +15,14 @@ import {
|
||||
TextInput,
|
||||
} from "./ui.tsx";
|
||||
|
||||
const INITIAL_MESSAGES_TO_SHOW = 30;
|
||||
|
||||
export function ChatScreen(props: { chatID: string }) {
|
||||
const chat = useCoState(Chat, props.chatID);
|
||||
const chat = useCoState(Chat, props.chatID, {
|
||||
resolve: { $each: { text: true } },
|
||||
});
|
||||
const { me } = useAccount();
|
||||
const [showNLastMessages, setShowNLastMessages] = useState(
|
||||
INITIAL_MESSAGES_TO_SHOW,
|
||||
);
|
||||
const isLoading = useMessagesPreload(props.chatID);
|
||||
const [showNLastMessages, setShowNLastMessages] = useState(30);
|
||||
|
||||
if (!chat || isLoading)
|
||||
if (!chat)
|
||||
return (
|
||||
<div className="flex-1 flex justify-center items-center">Loading...</div>
|
||||
);
|
||||
@@ -41,15 +37,11 @@ export function ChatScreen(props: { chatID: string }) {
|
||||
return;
|
||||
}
|
||||
|
||||
createImage(file, {
|
||||
owner: chat._owner,
|
||||
progressive: true,
|
||||
placeholder: "blur",
|
||||
}).then((image) => {
|
||||
createImage(file, { owner: chat._owner }).then((image) => {
|
||||
chat.push(
|
||||
Message.create(
|
||||
{
|
||||
text: file.name,
|
||||
text: co.plainText().create(file.name, chat._owner),
|
||||
image: image,
|
||||
},
|
||||
chat._owner,
|
||||
@@ -67,14 +59,9 @@ export function ChatScreen(props: { chatID: string }) {
|
||||
<ChatBody>
|
||||
{chat.length > 0 ? (
|
||||
chat
|
||||
// We call slice before reverse to avoid mutating the original array
|
||||
.slice(-showNLastMessages)
|
||||
// Reverse plus flex-col-reverse on ChatBody gives us scroll-to-bottom behavior
|
||||
.reverse()
|
||||
.map(
|
||||
(msg) =>
|
||||
msg?.text && <ChatBubble me={me} msg={msg} key={msg.id} />,
|
||||
)
|
||||
.reverse() // this plus flex-col-reverse on ChatBody gives us scroll-to-bottom behavior
|
||||
.map((msg) => <ChatBubble me={me} msg={msg} key={msg.id} />)
|
||||
) : (
|
||||
<EmptyChatMessage />
|
||||
)}
|
||||
@@ -93,7 +80,12 @@ export function ChatScreen(props: { chatID: string }) {
|
||||
|
||||
<TextInput
|
||||
onSubmit={(text) => {
|
||||
chat.push(Message.create({ text }, chat._owner));
|
||||
chat.push(
|
||||
Message.create(
|
||||
{ text: co.plainText().create(text, chat._owner) },
|
||||
chat._owner,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</InputBar>
|
||||
@@ -101,7 +93,10 @@ export function ChatScreen(props: { chatID: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatBubble(props: { me: Account; msg: Message }) {
|
||||
function ChatBubble(props: {
|
||||
me: Account;
|
||||
msg: co.loaded<typeof Message, { text: true }>;
|
||||
}) {
|
||||
if (!props.me.canRead(props.msg) || !props.msg.text?.toString()) {
|
||||
return (
|
||||
<BubbleContainer fromMe={false}>
|
||||
@@ -131,35 +126,3 @@ function ChatBubble(props: { me: Account; msg: Message }) {
|
||||
</BubbleContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warms the local cache with the initial messages to load only the initial messages
|
||||
* and avoid flickering
|
||||
*/
|
||||
function useMessagesPreload(chatID: string) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
preloadChatMessages(chatID).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [chatID]);
|
||||
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
async function preloadChatMessages(chatID: string) {
|
||||
const chat = await Chat.load(chatID);
|
||||
|
||||
if (!chat?._refs) return;
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const msg of Array.from(chat._refs)
|
||||
.reverse()
|
||||
.slice(0, INITIAL_MESSAGES_TO_SHOW)) {
|
||||
promises.push(Message.load(msg.id, { resolve: { text: true } }));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { co } from "jazz-tools";
|
||||
import { co, z } from "jazz-tools";
|
||||
|
||||
export const Message = co.map({
|
||||
text: co.plainText(),
|
||||
image: co.optional(co.image()),
|
||||
image: z.optional(co.image()),
|
||||
});
|
||||
export type Message = co.loaded<typeof Message>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { CoPlainText, ImageDefinition } from "jazz-tools";
|
||||
import { Image } from "jazz-tools/react";
|
||||
import { ProgressiveImg } from "jazz-tools/react";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import { useId, useRef } from "react";
|
||||
|
||||
@@ -83,12 +83,14 @@ export function BubbleText(props: {
|
||||
|
||||
export function BubbleImage(props: { image: ImageDefinition }) {
|
||||
return (
|
||||
<Image
|
||||
imageId={props.image.id}
|
||||
<ProgressiveImg image={props.image}>
|
||||
{({ src }) => (
|
||||
<img
|
||||
className="h-auto max-h-80 max-w-full rounded-t-xl mb-1"
|
||||
height="original"
|
||||
width="original"
|
||||
src={src}
|
||||
/>
|
||||
)}
|
||||
</ProgressiveImg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,9 +112,7 @@ export function InputBar(props: { children: React.ReactNode }) {
|
||||
|
||||
export function ImageInput({
|
||||
onImageChange,
|
||||
}: {
|
||||
onImageChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
}: { onImageChange?: (event: React.ChangeEvent<HTMLInputElement>) => void }) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onUploadClick = () => {
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||
"@clerk/clerk-expo": "^2.13.1",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"expo": "catalog:expo",
|
||||
"expo-crypto": "catalog:expo",
|
||||
"expo-linking": "catalog:expo",
|
||||
"expo-secure-store": "catalog:expo",
|
||||
"expo-sqlite": "catalog:expo",
|
||||
"expo-web-browser": "catalog:expo",
|
||||
"expo": "~53.0.9",
|
||||
"expo-crypto": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
"expo-sqlite": "~15.2.10",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "catalog:expo",
|
||||
"react-native": "catalog:expo",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"readable-stream": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "catalog:expo",
|
||||
"@types/react": "~19.0.10",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"private": true
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
|
||||
export function SignInScreen({
|
||||
setPage,
|
||||
}: {
|
||||
setPage: (page: "sign-in" | "sign-up") => void;
|
||||
}) {
|
||||
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
|
||||
const { signIn, setActive, isLoaded } = useSignIn();
|
||||
|
||||
const [emailAddress, setEmailAddress] = useState("");
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
|
||||
export function SignUpScreen({
|
||||
setPage,
|
||||
}: {
|
||||
setPage: (page: "sign-in" | "sign-up") => void;
|
||||
}) {
|
||||
}: { setPage: (page: "sign-in" | "sign-up") => void }) {
|
||||
const { isLoaded, signUp, setActive } = useSignUp();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.4.1",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "catalog:react",
|
||||
"react-dom": "catalog:react"
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@biomejs/biome": "catalog:default",
|
||||
"@types/react": "catalog:react",
|
||||
"@types/react-dom": "catalog:react",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"globals": "^15.11.0",
|
||||
"typescript": "catalog:default",
|
||||
"vite": "catalog:default"
|
||||
"typescript": "5.6.2",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ function JazzProvider({ children }: { children: ReactNode }) {
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
fallback={<p>Loading...</p>}
|
||||
>
|
||||
{children}
|
||||
</JazzReactProviderWithClerk>
|
||||
|
||||
6
examples/community-chat-vue/.gitignore
vendored
6
examples/community-chat-vue/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
dist
|
||||
# env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
@@ -1,7 +0,0 @@
|
||||
# community-chat-vue
|
||||
|
||||
## 0.15.4
|
||||
|
||||
- rewrite from React to Vue
|
||||
- jazz-tools@0.15.4
|
||||
- community-jazz-vue@0.15.4
|
||||
@@ -1,60 +0,0 @@
|
||||
# Chat example with Jazz and Vue
|
||||
|
||||
## Getting started
|
||||
|
||||
You can either
|
||||
1. Clone the jazz repository, and run the app within the monorepo.
|
||||
2. Or create a new Jazz project using this example as a template.
|
||||
|
||||
### Using the example as a template
|
||||
|
||||
Create a new Jazz project, and use this example as a template.
|
||||
```bash
|
||||
npx create-jazz-app@latest chat-vue-app --example community-chat-vue
|
||||
```
|
||||
|
||||
Go to the new project directory.
|
||||
```bash
|
||||
cd chat-vue-app
|
||||
```
|
||||
|
||||
Run the dev server.
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 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/community-chat-vue/
|
||||
```
|
||||
|
||||
Start the dev server.
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||
|
||||
## Questions / problems / feedback
|
||||
|
||||
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
|
||||
|
||||
## Configuration: sync server
|
||||
|
||||
By default, the example app uses [Jazz Cloud](https://jazz.tools/cloud) (`wss://cloud.jazz.tools`) - so cross-device use, invites and collaboration should just work.
|
||||
|
||||
You can also run a local sync server by running `npx jazz-run sync`, and setting the `sync` parameter of `JazzProvider` in [./src/main.ts](./src/main.ts) to `{ peer: "ws://localhost:4200" }`.
|
||||
1
examples/community-chat-vue/env.d.ts
vendored
1
examples/community-chat-vue/env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jazz Chat Vue Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "community-chat-vue",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build-type-check": "run-p type-check \"build {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-tools": "workspace:*",
|
||||
"community-jazz-vue": "workspace:*",
|
||||
"vue": "^3.5.11",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^22.5.1",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"postcss": "^8.4.40",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"typescript": "5.6.2",
|
||||
"vite": "6.3.5",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vue-tsc": "^2.1.6"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<AppContainer>
|
||||
<TopBar v-if="me">
|
||||
<p>{{ me.profile?.name }}</p>
|
||||
<button @click="logoutHandler">Log out</button>
|
||||
</TopBar>
|
||||
<router-view />
|
||||
</AppContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from "community-jazz-vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import AppContainer from "./components/AppContainer.vue";
|
||||
import TopBar from "./components/TopBar.vue";
|
||||
|
||||
const { me, logOut } = useAccount();
|
||||
const router = useRouter();
|
||||
|
||||
async function logoutHandler() {
|
||||
await logOut();
|
||||
router.push("/");
|
||||
}
|
||||
</script>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { JazzVueProvider, PasskeyAuthBasicUI } from "community-jazz-vue";
|
||||
import { h } from "vue";
|
||||
import "jazz-tools/inspector/register-custom-element";
|
||||
import App from "./App.vue";
|
||||
import { apiKey } from "./apiKey";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<JazzVueProvider
|
||||
:sync="{
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}"
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="Jazz Vue Chat">
|
||||
<App />
|
||||
</PasskeyAuthBasicUI>
|
||||
<component
|
||||
:is="h('jazz-inspector', {
|
||||
style: { position: 'fixed', left: '20px', bottom: '20px', zIndex: 9999 }
|
||||
})"
|
||||
/>
|
||||
</JazzVueProvider>
|
||||
</template>
|
||||
@@ -1 +0,0 @@
|
||||
export const apiKey = "chat-example-jazz@garden.co";
|
||||
@@ -1,76 +0,0 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
Before Width: | Height: | Size: 276 B |
@@ -1,35 +0,0 @@
|
||||
@import "./base.css";
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between w-screen h-screen bg-stone-50 dark:bg-black dark:text-white"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "AppContainer",
|
||||
};
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-2xl text-sm line-clamp-10 text-ellipsis bg-white max-w-full whitespace-pre-wrap dark:bg-stone-700 dark:text-white py-1 px-3 shadow-sm"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "BubbleBody",
|
||||
};
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<div :class="[alignClass, 'flex flex-col m-2']" role="row">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "BubbleContainer",
|
||||
props: {
|
||||
fromMe: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
alignClass() {
|
||||
return this.fromMe ? "items-end" : "items-start";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="h-auto max-w-full rounded-t-xl mb-1">
|
||||
<Image
|
||||
:image-id="image.id"
|
||||
alt="Uploaded image"
|
||||
class-names="h-full rounded-t-xl"
|
||||
width="original"
|
||||
height="original"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { type ImageDefinition } from "jazz-tools";
|
||||
import { Image } from "community-jazz-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BubbleImage",
|
||||
components: {
|
||||
Image,
|
||||
},
|
||||
props: {
|
||||
image: {
|
||||
type: Object as () => ImageDefinition,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div class="text-xs text-neutral-500 mt-1.5">
|
||||
{{ by }} · {{ formattedTime }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BubbleInfo",
|
||||
props: {
|
||||
by: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
madeAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const formattedTime = computed(() => props.madeAt.toLocaleTimeString());
|
||||
return {
|
||||
formattedTime,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-y-auto flex flex-col-reverse" role="application">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "ChatBody",
|
||||
};
|
||||
</script>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<BubbleContainer :fromMe="lastEdit.by?.isMe">
|
||||
<BubbleBody>
|
||||
<BubbleImage v-if="msg.image" :image="msg.image" />
|
||||
{{ msg.text }}
|
||||
</BubbleBody>
|
||||
<BubbleInfo :by="lastEdit.by?.profile?.name" :madeAt="lastEdit.madeAt" />
|
||||
</BubbleContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "vue";
|
||||
import BubbleBody from "./BubbleBody.vue";
|
||||
import BubbleContainer from "./BubbleContainer.vue";
|
||||
import BubbleInfo from "./BubbleInfo.vue";
|
||||
import BubbleImage from "./BubbleImage.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChatBubble",
|
||||
components: {
|
||||
BubbleContainer,
|
||||
BubbleBody,
|
||||
BubbleInfo,
|
||||
BubbleImage,
|
||||
},
|
||||
props: {
|
||||
msg: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const lastEdit = computed(() => props.msg._edits.text);
|
||||
return {
|
||||
lastEdit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="p-3 bg-white border-t shadow-2xl mt-auto dark:bg-transparent dark:border-stone-800 flex gap-1"
|
||||
>
|
||||
<ImageInput @image-change="handleImageChange" />
|
||||
|
||||
<div class="flex-1">
|
||||
<label class="sr-only" :for="inputId">Type a message and press Enter</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
v-model="inputValue"
|
||||
class="rounded-full py-2 px-4 text-sm border block w-full dark:bg-black dark:text-white dark:border-stone-700"
|
||||
placeholder="Type a message and press Enter"
|
||||
maxlength="2048"
|
||||
@keydown.enter.prevent="submitMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
import ImageInput from "./ImageInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChatInput",
|
||||
components: {
|
||||
ImageInput,
|
||||
},
|
||||
emits: ["submit", "imageSubmit"],
|
||||
setup(_, { emit }) {
|
||||
const inputId = `input-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const inputValue = ref("");
|
||||
|
||||
function submitMessage() {
|
||||
if (!inputValue.value) return;
|
||||
emit("submit", inputValue.value);
|
||||
inputValue.value = "";
|
||||
}
|
||||
|
||||
function handleImageChange(file: File | undefined) {
|
||||
if (file) {
|
||||
emit("imageSubmit", file);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
inputId,
|
||||
inputValue,
|
||||
submitMessage,
|
||||
handleImageChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-full text-base text-stone-500 flex items-center justify-center px-3 md:text-xl"
|
||||
>
|
||||
Start a conversation below.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "EmptyChatMessage",
|
||||
};
|
||||
</script>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Send image"
|
||||
title="Send image"
|
||||
@click="onUploadClick"
|
||||
class="text-stone-500 p-1.5 rounded-full hover:bg-stone-100 hover:text-stone-800 dark:hover:bg-stone-800 dark:hover:text-stone-200 transition-colors"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<label class="sr-only">
|
||||
Image
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
@change="onImageChange"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageInput",
|
||||
emits: ["imageChange"],
|
||||
setup(_, { emit }) {
|
||||
const inputRef = ref<HTMLInputElement>();
|
||||
|
||||
function onUploadClick() {
|
||||
inputRef.value?.click();
|
||||
}
|
||||
|
||||
function onImageChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
emit("imageChange", target.files?.[0]);
|
||||
}
|
||||
|
||||
return {
|
||||
inputRef,
|
||||
onUploadClick,
|
||||
onImageChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="p-3 bg-white w-full flex justify-end gap-1 text-xs border-b dark:bg-transparent dark:border-stone-800"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "TopBar",
|
||||
};
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import RootApp from "./RootApp.vue";
|
||||
import "./index.css";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(RootApp);
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user