Merge pull request #2617 from garden-co/anselm-gco-634-implement-current-session-logic-in-rust-and-use-as-wasm
Implement current session logic in Rust and use as WASM
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"fixed": [
|
||||
[
|
||||
"cojson",
|
||||
"cojson-core-wasm",
|
||||
"cojson-storage-indexeddb",
|
||||
"cojson-storage-sqlite",
|
||||
"cojson-transport-ws",
|
||||
|
||||
5
.changeset/sour-mice-divide.md
Normal file
5
.changeset/sour-mice-divide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Move the session log management into WASM
|
||||
163
bench/comap.create.bench.ts
Normal file
163
bench/comap.create.bench.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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 (SessionLog)",
|
||||
() => {
|
||||
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: 500 },
|
||||
);
|
||||
|
||||
bench(
|
||||
"latest version (0.17.2)",
|
||||
() => {
|
||||
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: 500 },
|
||||
);
|
||||
});
|
||||
|
||||
describe("Message import", () => {
|
||||
bench("current version (SessionLog)", () => {
|
||||
tools.importContentPieces(content ?? [], schema.account as any);
|
||||
schema.account._raw.core.node.internalDeleteCoValue(message.id as any);
|
||||
});
|
||||
|
||||
bench("latest version (0.17.2)", () => {
|
||||
toolsLatest.importContentPieces(content ?? [], schemaLatest.account);
|
||||
schemaLatest.account._raw.core.node.internalDeleteCoValue(
|
||||
message.id as any,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("import+ decrypt", () => {
|
||||
bench(
|
||||
"current version (SessionLog)",
|
||||
() => {
|
||||
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: 500 },
|
||||
);
|
||||
|
||||
bench(
|
||||
"latest version (0.17.2)",
|
||||
() => {
|
||||
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: 500 },
|
||||
);
|
||||
});
|
||||
14
bench/package.json
Normal file
14
bench/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "jazz-tools-benchmark",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"cojson-latest": "npm:cojson@latest",
|
||||
"jazz-tools-latest": "npm:jazz-tools@latest"
|
||||
},
|
||||
"scripts": {
|
||||
"bench": "vitest bench"
|
||||
}
|
||||
}
|
||||
7
bench/vitest.config.ts
Normal file
7
bench/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineProject } from "vitest/config";
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
name: "bench",
|
||||
},
|
||||
});
|
||||
@@ -9,6 +9,7 @@
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**",
|
||||
"!crates/**",
|
||||
"!**/jazz-tools.json",
|
||||
"!**/ios/**",
|
||||
"!**/android/**",
|
||||
|
||||
9
crates/.gitignore
vendored
Normal file
9
crates/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Rust
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
# Test artifacts
|
||||
lzy/compressed_66k.lzy
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
7
crates/Cargo.toml
Normal file
7
crates/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"lzy",
|
||||
"cojson-core",
|
||||
"cojson-core-wasm",
|
||||
]
|
||||
30
crates/cojson-core-wasm/Cargo.toml
Normal file
30
crates/cojson-core-wasm/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[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"
|
||||
wee_alloc = { version = "0.4.5", optional = true }
|
||||
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"]
|
||||
26
crates/cojson-core-wasm/build.js
Normal file
26
crates/cojson-core-wasm/build.js
Normal file
@@ -0,0 +1,26 @@
|
||||
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
Normal file
3
crates/cojson-core-wasm/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./public/cojson_core_wasm.js";
|
||||
|
||||
export async function initialize(): Promise<void>;
|
||||
8
crates/cojson-core-wasm/index.js
Normal file
8
crates/cojson-core-wasm/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
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 });
|
||||
}
|
||||
22
crates/cojson-core-wasm/package.json
Normal file
22
crates/cojson-core-wasm/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "cojson-core-wasm",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"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
Normal file
291
crates/cojson-core-wasm/public/cojson_core_wasm.d.ts
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
/* 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);
|
||||
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>;
|
||||
1280
crates/cojson-core-wasm/public/cojson_core_wasm.js
Normal file
1280
crates/cojson-core-wasm/public/cojson_core_wasm.js
Normal file
File diff suppressed because it is too large
Load Diff
1
crates/cojson-core-wasm/public/cojson_core_wasm.wasm.d.ts
vendored
Normal file
1
crates/cojson-core-wasm/public/cojson_core_wasm.wasm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const data: string;
|
||||
1
crates/cojson-core-wasm/public/cojson_core_wasm.wasm.js
Normal file
1
crates/cojson-core-wasm/public/cojson_core_wasm.wasm.js
Normal file
File diff suppressed because one or more lines are too long
240
crates/cojson-core-wasm/src/crypto/ed25519.rs
Normal file
240
crates/cojson-core-wasm/src/crypto/ed25519.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
113
crates/cojson-core-wasm/src/crypto/encrypt.rs
Normal file
113
crates/cojson-core-wasm/src/crypto/encrypt.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
200
crates/cojson-core-wasm/src/crypto/seal.rs
Normal file
200
crates/cojson-core-wasm/src/crypto/seal.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
184
crates/cojson-core-wasm/src/crypto/sign.rs
Normal file
184
crates/cojson-core-wasm/src/crypto/sign.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
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(_))));
|
||||
}
|
||||
}
|
||||
168
crates/cojson-core-wasm/src/crypto/x25519.rs
Normal file
168
crates/cojson-core-wasm/src/crypto/x25519.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
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(_))));
|
||||
}
|
||||
}
|
||||
256
crates/cojson-core-wasm/src/crypto/xsalsa20.rs
Normal file
256
crates/cojson-core-wasm/src/crypto/xsalsa20.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
43
crates/cojson-core-wasm/src/error.rs
Normal file
43
crates/cojson-core-wasm/src/error.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
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 {}
|
||||
218
crates/cojson-core-wasm/src/hash/blake3.rs
Normal file
218
crates/cojson-core-wasm/src/hash/blake3.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
171
crates/cojson-core-wasm/src/lib.rs
Normal file
171
crates/cojson-core-wasm/src/lib.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
||||
// allocator.
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[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: String) -> SessionLog {
|
||||
let co_id = CoID(co_id);
|
||||
let session_id = SessionID(session_id);
|
||||
let signer_id = SignerID(signer_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))?)
|
||||
}
|
||||
}
|
||||
18
crates/cojson-core/Cargo.toml
Normal file
18
crates/cojson-core/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[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"] }
|
||||
8
crates/cojson-core/data/multiTxSession.json
Normal file
8
crates/cojson-core/data/multiTxSession.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"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":{}}
|
||||
}
|
||||
}
|
||||
6
crates/cojson-core/data/singleTxSession.json
Normal file
6
crates/cojson-core/data/singleTxSession.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"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":{}}}
|
||||
}
|
||||
680
crates/cojson-core/src/lib.rs
Normal file
680
crates/cojson-core/src/lib.rs
Normal file
@@ -0,0 +1,680 @@
|
||||
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: 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: SignerID) -> Self {
|
||||
let hasher = blake3::Hasher::new();
|
||||
|
||||
let public_key = VerifyingKey::try_from(
|
||||
decode_z(&signer_id.0)
|
||||
.expect("Invalid public key")
|
||||
.as_slice(),
|
||||
)
|
||||
.expect("Invalid public key");
|
||||
|
||||
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()
|
||||
);
|
||||
|
||||
match self.public_key.verify(
|
||||
new_hash_encoded_stringified.as_bytes(),
|
||||
&(new_signature).into(),
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
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"}]"#
|
||||
);
|
||||
}
|
||||
}
|
||||
15
crates/lzy/Cargo.toml
Normal file
15
crates/lzy/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[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
|
||||
36
crates/lzy/benches/compression_benchmark.rs
Normal file
36
crates/lzy/benches/compression_benchmark.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
59
crates/lzy/data/compression_66k_JSON.txt
Normal file
59
crates/lzy/data/compression_66k_JSON.txt
Normal file
File diff suppressed because one or more lines are too long
348
crates/lzy/src/lib.rs
Normal file
348
crates/lzy/src/lib.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
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 ---");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"examples/*",
|
||||
"starters/*"
|
||||
"starters/*",
|
||||
"crates/*"
|
||||
],
|
||||
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
|
||||
"engines": {
|
||||
@@ -31,6 +32,7 @@
|
||||
"vitest": "catalog:default"
|
||||
},
|
||||
"scripts": {
|
||||
"bench": "vitest bench",
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build && cd homepage/homepage && turbo build",
|
||||
"build:packages": "turbo build --filter='./packages/*'",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@scure/base": "1.2.1",
|
||||
"jazz-crypto-rs": "0.0.7",
|
||||
"cojson-core-wasm": "workspace:*",
|
||||
"neverthrow": "^7.0.1",
|
||||
"unicode-segmenter": "^0.12.0"
|
||||
},
|
||||
|
||||
230
packages/cojson/src/coValueCore/SessionMap.ts
Normal file
230
packages/cojson/src/coValueCore/SessionMap.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { Result, err, ok } from "neverthrow";
|
||||
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
||||
import type {
|
||||
CryptoProvider,
|
||||
Hash,
|
||||
KeyID,
|
||||
KeySecret,
|
||||
SessionLogImpl,
|
||||
Signature,
|
||||
SignerID,
|
||||
} from "../crypto/crypto.js";
|
||||
import { RawCoID, SessionID } from "../ids.js";
|
||||
import { parseJSON, stableStringify, Stringified } from "../jsonStringify.js";
|
||||
import { JsonValue } from "../jsonValue.js";
|
||||
import { CoValueKnownState } from "../sync.js";
|
||||
import { TryAddTransactionsError } from "./coValueCore.js";
|
||||
import { Transaction } from "./verifiedState.js";
|
||||
import { exceedsRecommendedSize } from "../coValueContentMessage.js";
|
||||
|
||||
export type SessionLog = {
|
||||
signerID: SignerID;
|
||||
impl: SessionLogImpl;
|
||||
transactions: Transaction[];
|
||||
lastSignature: Signature | undefined;
|
||||
signatureAfter: { [txIdx: number]: Signature | undefined };
|
||||
txSizeSinceLastInbetweenSignature: number;
|
||||
};
|
||||
|
||||
export class SessionMap {
|
||||
sessions: Map<SessionID, SessionLog> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly id: RawCoID,
|
||||
private readonly crypto: CryptoProvider,
|
||||
) {}
|
||||
|
||||
get(sessionID: SessionID): SessionLog | undefined {
|
||||
return this.sessions.get(sessionID);
|
||||
}
|
||||
|
||||
private getOrCreateSessionLog(
|
||||
sessionID: SessionID,
|
||||
signerID: SignerID,
|
||||
): SessionLog {
|
||||
let sessionLog = this.sessions.get(sessionID);
|
||||
if (!sessionLog) {
|
||||
sessionLog = {
|
||||
signerID,
|
||||
impl: this.crypto.createSessionLog(this.id, sessionID, signerID),
|
||||
transactions: [],
|
||||
lastSignature: undefined,
|
||||
signatureAfter: {},
|
||||
txSizeSinceLastInbetweenSignature: 0,
|
||||
};
|
||||
this.sessions.set(sessionID, sessionLog);
|
||||
}
|
||||
|
||||
return sessionLog;
|
||||
}
|
||||
|
||||
addTransaction(
|
||||
sessionID: SessionID,
|
||||
signerID: SignerID,
|
||||
newTransactions: Transaction[],
|
||||
newSignature: Signature,
|
||||
skipVerify: boolean = false,
|
||||
): Result<true, TryAddTransactionsError> {
|
||||
const sessionLog = this.getOrCreateSessionLog(sessionID, signerID);
|
||||
|
||||
try {
|
||||
sessionLog.impl.tryAdd(newTransactions, newSignature, skipVerify);
|
||||
|
||||
this.addTransactionsToJsLog(sessionLog, newTransactions, newSignature);
|
||||
|
||||
return ok(true as const);
|
||||
} catch (e) {
|
||||
return err({
|
||||
type: "InvalidSignature",
|
||||
id: this.id,
|
||||
sessionID,
|
||||
newSignature,
|
||||
signerID,
|
||||
} satisfies TryAddTransactionsError);
|
||||
}
|
||||
}
|
||||
|
||||
makeNewPrivateTransaction(
|
||||
sessionID: SessionID,
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
keyID: KeyID,
|
||||
keySecret: KeySecret,
|
||||
): { signature: Signature; transaction: Transaction } {
|
||||
const sessionLog = this.getOrCreateSessionLog(
|
||||
sessionID,
|
||||
signerAgent.currentSignerID(),
|
||||
);
|
||||
|
||||
const madeAt = Date.now();
|
||||
|
||||
const result = sessionLog.impl.addNewPrivateTransaction(
|
||||
signerAgent,
|
||||
changes,
|
||||
keyID,
|
||||
keySecret,
|
||||
madeAt,
|
||||
);
|
||||
|
||||
this.addTransactionsToJsLog(
|
||||
sessionLog,
|
||||
[result.transaction],
|
||||
result.signature,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
makeNewTrustingTransaction(
|
||||
sessionID: SessionID,
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
): { signature: Signature; transaction: Transaction } {
|
||||
const sessionLog = this.getOrCreateSessionLog(
|
||||
sessionID,
|
||||
signerAgent.currentSignerID(),
|
||||
);
|
||||
|
||||
const madeAt = Date.now();
|
||||
|
||||
const result = sessionLog.impl.addNewTrustingTransaction(
|
||||
signerAgent,
|
||||
changes,
|
||||
madeAt,
|
||||
);
|
||||
|
||||
this.addTransactionsToJsLog(
|
||||
sessionLog,
|
||||
[result.transaction],
|
||||
result.signature,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private addTransactionsToJsLog(
|
||||
sessionLog: SessionLog,
|
||||
newTransactions: Transaction[],
|
||||
signature: Signature,
|
||||
) {
|
||||
for (const tx of newTransactions) {
|
||||
sessionLog.transactions.push(tx);
|
||||
}
|
||||
sessionLog.lastSignature = signature;
|
||||
|
||||
sessionLog.txSizeSinceLastInbetweenSignature += newTransactions.reduce(
|
||||
(sum, tx) =>
|
||||
sum +
|
||||
(tx.privacy === "private"
|
||||
? tx.encryptedChanges.length
|
||||
: tx.changes.length),
|
||||
0,
|
||||
);
|
||||
|
||||
if (exceedsRecommendedSize(sessionLog.txSizeSinceLastInbetweenSignature)) {
|
||||
sessionLog.signatureAfter[sessionLog.transactions.length - 1] = signature;
|
||||
sessionLog.txSizeSinceLastInbetweenSignature = 0;
|
||||
}
|
||||
}
|
||||
|
||||
knownState(): CoValueKnownState {
|
||||
const sessions: CoValueKnownState["sessions"] = {};
|
||||
for (const [sessionID, sessionLog] of this.sessions.entries()) {
|
||||
sessions[sessionID] = sessionLog.transactions.length;
|
||||
}
|
||||
return { id: this.id, header: true, sessions };
|
||||
}
|
||||
|
||||
decryptTransaction(
|
||||
sessionID: SessionID,
|
||||
txIndex: number,
|
||||
keySecret: KeySecret,
|
||||
): JsonValue[] | undefined {
|
||||
const sessionLog = this.sessions.get(sessionID);
|
||||
if (!sessionLog) {
|
||||
return undefined;
|
||||
}
|
||||
const decrypted = sessionLog.impl.decryptNextTransactionChangesJson(
|
||||
txIndex,
|
||||
keySecret,
|
||||
);
|
||||
if (!decrypted) {
|
||||
return undefined;
|
||||
}
|
||||
return parseJSON(decrypted as Stringified<JsonValue[] | undefined>);
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
entries() {
|
||||
return this.sessions.entries();
|
||||
}
|
||||
|
||||
values() {
|
||||
return this.sessions.values();
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this.sessions.keys();
|
||||
}
|
||||
|
||||
clone(): SessionMap {
|
||||
const clone = new SessionMap(this.id, this.crypto);
|
||||
|
||||
for (const [sessionID, sessionLog] of this.sessions) {
|
||||
clone.sessions.set(sessionID, {
|
||||
impl: sessionLog.impl.clone(),
|
||||
transactions: sessionLog.transactions.slice(),
|
||||
lastSignature: sessionLog.lastSignature,
|
||||
signatureAfter: { ...sessionLog.signatureAfter },
|
||||
txSizeSinceLastInbetweenSignature:
|
||||
sessionLog.txSizeSinceLastInbetweenSignature,
|
||||
signerID: sessionLog.signerID,
|
||||
});
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfrom
|
||||
import { expectGroup } from "../typeUtils/expectGroup.js";
|
||||
import { getDependedOnCoValuesFromRawData } from "./utils.js";
|
||||
import { CoValueHeader, Transaction, VerifiedState } from "./verifiedState.js";
|
||||
import { SessionMap } from "./SessionMap.js";
|
||||
|
||||
export function idforHeader(
|
||||
header: CoValueHeader,
|
||||
@@ -95,12 +96,7 @@ export class CoValueCore {
|
||||
this.crypto = node.crypto;
|
||||
if ("header" in init) {
|
||||
this.id = idforHeader(init.header, node.crypto);
|
||||
this._verified = new VerifiedState(
|
||||
this.id,
|
||||
node.crypto,
|
||||
init.header,
|
||||
new Map(),
|
||||
);
|
||||
this._verified = new VerifiedState(this.id, node.crypto, init.header);
|
||||
} else {
|
||||
this.id = init.id;
|
||||
this._verified = null;
|
||||
@@ -298,7 +294,7 @@ export class CoValueCore {
|
||||
this.id,
|
||||
this.node.crypto,
|
||||
header,
|
||||
new Map(),
|
||||
new SessionMap(this.id, this.node.crypto),
|
||||
streamingKnownState,
|
||||
);
|
||||
|
||||
@@ -467,19 +463,7 @@ export class CoValueCore {
|
||||
);
|
||||
|
||||
if (result.isOk()) {
|
||||
if (
|
||||
this._cachedContent &&
|
||||
"processNewTransactions" in this._cachedContent &&
|
||||
typeof this._cachedContent.processNewTransactions === "function"
|
||||
) {
|
||||
this._cachedContent.processNewTransactions();
|
||||
} else {
|
||||
this._cachedContent = undefined;
|
||||
}
|
||||
|
||||
this._cachedDependentOn = undefined;
|
||||
|
||||
this.notifyUpdate(notifyMode);
|
||||
this.updateContentAndNotifyUpdate(notifyMode);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -489,6 +473,22 @@ export class CoValueCore {
|
||||
deferredUpdates = 0;
|
||||
nextDeferredNotify: Promise<void> | undefined;
|
||||
|
||||
updateContentAndNotifyUpdate(notifyMode: "immediate" | "deferred") {
|
||||
if (
|
||||
this._cachedContent &&
|
||||
"processNewTransactions" in this._cachedContent &&
|
||||
typeof this._cachedContent.processNewTransactions === "function"
|
||||
) {
|
||||
this._cachedContent.processNewTransactions();
|
||||
} else {
|
||||
this._cachedContent = undefined;
|
||||
}
|
||||
|
||||
this._cachedDependentOn = undefined;
|
||||
|
||||
this.notifyUpdate(notifyMode);
|
||||
}
|
||||
|
||||
notifyUpdate(notifyMode: "immediate" | "deferred") {
|
||||
if (this.listeners.size === 0) {
|
||||
return;
|
||||
@@ -556,38 +556,6 @@ export class CoValueCore {
|
||||
);
|
||||
}
|
||||
|
||||
const madeAt = Date.now();
|
||||
|
||||
let transaction: Transaction;
|
||||
|
||||
if (privacy === "private") {
|
||||
const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
|
||||
|
||||
if (!keySecret) {
|
||||
throw new Error("Can't make transaction without read key secret");
|
||||
}
|
||||
|
||||
const encrypted = this.crypto.encryptForTransaction(changes, keySecret, {
|
||||
in: this.id,
|
||||
tx: this.nextTransactionID(),
|
||||
});
|
||||
|
||||
this._decryptionCache[encrypted] = changes;
|
||||
|
||||
transaction = {
|
||||
privacy: "private",
|
||||
madeAt,
|
||||
keyUsed: keyID,
|
||||
encryptedChanges: encrypted,
|
||||
};
|
||||
} else {
|
||||
transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt,
|
||||
changes: stableStringify(changes),
|
||||
};
|
||||
}
|
||||
|
||||
// This is an ugly hack to get a unique but stable session ID for editing the current account
|
||||
const sessionID =
|
||||
this.verified.header.meta?.type === "account"
|
||||
@@ -597,39 +565,53 @@ export class CoValueCore {
|
||||
) as SessionID)
|
||||
: this.node.currentSessionID;
|
||||
|
||||
const { expectedNewHash, newStreamingHash } =
|
||||
this.verified.expectedNewHashAfter(sessionID, [transaction]);
|
||||
const signerAgent = this.node.getCurrentAgent();
|
||||
|
||||
const signature = this.crypto.sign(
|
||||
this.node.getCurrentAgent().currentSignerSecret(),
|
||||
expectedNewHash,
|
||||
);
|
||||
let result: { signature: Signature; transaction: Transaction };
|
||||
|
||||
const success = this.tryAddTransactions(
|
||||
sessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
signature,
|
||||
"immediate",
|
||||
true,
|
||||
newStreamingHash,
|
||||
)._unsafeUnwrap({ withStackTrace: true });
|
||||
if (privacy === "private") {
|
||||
const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
|
||||
|
||||
if (success) {
|
||||
const session = this.verified.sessions.get(sessionID);
|
||||
const txIdx = session ? session.transactions.length - 1 : 0;
|
||||
if (!keySecret) {
|
||||
throw new Error("Can't make transaction without read key secret");
|
||||
}
|
||||
|
||||
this.node.syncManager.recordTransactionsSize([transaction], "local");
|
||||
this.node.syncManager.syncLocalTransaction(
|
||||
this.verified,
|
||||
transaction,
|
||||
result = this.verified.makeNewPrivateTransaction(
|
||||
sessionID,
|
||||
signature,
|
||||
txIdx,
|
||||
signerAgent,
|
||||
changes,
|
||||
keyID,
|
||||
keySecret,
|
||||
);
|
||||
|
||||
if (result.transaction.privacy === "private") {
|
||||
this._decryptionCache[result.transaction.encryptedChanges] = changes;
|
||||
}
|
||||
} else {
|
||||
result = this.verified.makeNewTrustingTransaction(
|
||||
sessionID,
|
||||
signerAgent,
|
||||
changes,
|
||||
);
|
||||
}
|
||||
|
||||
return success;
|
||||
const { transaction, signature } = result;
|
||||
|
||||
this.node.syncManager.recordTransactionsSize([transaction], "local");
|
||||
|
||||
const session = this.verified.sessions.get(sessionID);
|
||||
const txIdx = session ? session.transactions.length - 1 : 0;
|
||||
|
||||
this.updateContentAndNotifyUpdate("immediate");
|
||||
this.node.syncManager.syncLocalTransaction(
|
||||
this.verified,
|
||||
transaction,
|
||||
sessionID,
|
||||
signature,
|
||||
txIdx,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getCurrentContent(options?: { ignorePrivateTransactions: true }): RawCoValue {
|
||||
@@ -658,6 +640,12 @@ export class CoValueCore {
|
||||
ignorePrivateTransactions: boolean;
|
||||
knownTransactions?: CoValueKnownState["sessions"];
|
||||
}): DecryptedTransaction[] {
|
||||
if (!this.verified) {
|
||||
throw new Error(
|
||||
"CoValueCore: getValidTransactions called on coValue without verified state",
|
||||
);
|
||||
}
|
||||
|
||||
const validTransactions = determineValidTransactions(
|
||||
this,
|
||||
options?.knownTransactions,
|
||||
@@ -701,25 +689,12 @@ export class CoValueCore {
|
||||
let decryptedChanges = this._decryptionCache[tx.encryptedChanges];
|
||||
|
||||
if (!decryptedChanges) {
|
||||
const decryptedString = this.crypto.decryptRawForTransaction(
|
||||
tx.encryptedChanges,
|
||||
decryptedChanges = this.verified.decryptTransaction(
|
||||
txID.sessionID,
|
||||
txID.txIndex,
|
||||
readKey,
|
||||
{
|
||||
in: this.id,
|
||||
tx: txID,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
decryptedChanges = decryptedString && parseJSON(decryptedString);
|
||||
} catch (e) {
|
||||
logger.error("Failed to parse private transaction on " + this.id, {
|
||||
err: e,
|
||||
txID,
|
||||
changes: decryptedString?.slice(0, 50),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
this._decryptionCache[tx.encryptedChanges] = decryptedChanges;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Encrypted,
|
||||
Hash,
|
||||
KeyID,
|
||||
KeySecret,
|
||||
Signature,
|
||||
SignerID,
|
||||
StreamingHash,
|
||||
@@ -21,6 +22,8 @@ import { PermissionsDef as RulesetDef } from "../permissions.js";
|
||||
import { CoValueKnownState, NewContentMessage } from "../sync.js";
|
||||
import { InvalidHashError, InvalidSignatureError } from "./coValueCore.js";
|
||||
import { TryAddTransactionsError } from "./coValueCore.js";
|
||||
import { SessionLog, SessionMap } from "./SessionMap.js";
|
||||
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
||||
|
||||
export type CoValueHeader = {
|
||||
type: AnyRawCoValue["type"];
|
||||
@@ -48,20 +51,11 @@ export type TrustingTransaction = {
|
||||
|
||||
export type Transaction = PrivateTransaction | TrustingTransaction;
|
||||
|
||||
type SessionLog = {
|
||||
readonly transactions: Transaction[];
|
||||
streamingHash?: StreamingHash;
|
||||
readonly signatureAfter: { [txIdx: number]: Signature | undefined };
|
||||
lastSignature: Signature;
|
||||
};
|
||||
|
||||
export type ValidatedSessions = Map<SessionID, SessionLog>;
|
||||
|
||||
export class VerifiedState {
|
||||
readonly id: RawCoID;
|
||||
readonly crypto: CryptoProvider;
|
||||
readonly header: CoValueHeader;
|
||||
readonly sessions: ValidatedSessions;
|
||||
readonly sessions: SessionMap;
|
||||
private _cachedKnownState?: CoValueKnownState;
|
||||
private _cachedNewContentSinceEmpty: NewContentMessage[] | undefined;
|
||||
private streamingKnownState?: CoValueKnownState["sessions"];
|
||||
@@ -71,35 +65,25 @@ export class VerifiedState {
|
||||
id: RawCoID,
|
||||
crypto: CryptoProvider,
|
||||
header: CoValueHeader,
|
||||
sessions: ValidatedSessions,
|
||||
sessions?: SessionMap,
|
||||
streamingKnownState?: CoValueKnownState["sessions"],
|
||||
) {
|
||||
this.id = id;
|
||||
this.crypto = crypto;
|
||||
this.header = header;
|
||||
this.sessions = sessions;
|
||||
this.sessions = sessions ?? new SessionMap(id, crypto);
|
||||
this.streamingKnownState = streamingKnownState
|
||||
? { ...streamingKnownState }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
clone(): VerifiedState {
|
||||
// do a deep clone, including the sessions
|
||||
const clonedSessions = new Map();
|
||||
for (let [sessionID, sessionLog] of this.sessions) {
|
||||
clonedSessions.set(sessionID, {
|
||||
lastSignature: sessionLog.lastSignature,
|
||||
streamingHash: sessionLog.streamingHash?.clone(),
|
||||
signatureAfter: { ...sessionLog.signatureAfter },
|
||||
transactions: sessionLog.transactions.slice(),
|
||||
} satisfies SessionLog);
|
||||
}
|
||||
return new VerifiedState(
|
||||
this.id,
|
||||
this.crypto,
|
||||
this.header,
|
||||
clonedSessions,
|
||||
this.streamingKnownState,
|
||||
this.sessions.clone(),
|
||||
this.streamingKnownState ? { ...this.streamingKnownState } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,47 +96,58 @@ export class VerifiedState {
|
||||
skipVerify: boolean = false,
|
||||
givenNewStreamingHash?: StreamingHash,
|
||||
): Result<true, TryAddTransactionsError> {
|
||||
if (skipVerify === true) {
|
||||
this.doAddTransactions(
|
||||
sessionID,
|
||||
newTransactions,
|
||||
newSignature,
|
||||
givenNewStreamingHash,
|
||||
);
|
||||
} else {
|
||||
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
||||
sessionID,
|
||||
newTransactions,
|
||||
);
|
||||
const result = this.sessions.addTransaction(
|
||||
sessionID,
|
||||
signerID,
|
||||
newTransactions,
|
||||
newSignature,
|
||||
skipVerify,
|
||||
);
|
||||
|
||||
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
||||
return err({
|
||||
type: "InvalidHash",
|
||||
id: this.id,
|
||||
expectedNewHash,
|
||||
givenExpectedNewHash,
|
||||
} satisfies InvalidHashError);
|
||||
}
|
||||
|
||||
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
|
||||
return err({
|
||||
type: "InvalidSignature",
|
||||
id: this.id,
|
||||
newSignature,
|
||||
sessionID,
|
||||
signerID,
|
||||
} satisfies InvalidSignatureError);
|
||||
}
|
||||
|
||||
this.doAddTransactions(
|
||||
sessionID,
|
||||
newTransactions,
|
||||
newSignature,
|
||||
newStreamingHash,
|
||||
);
|
||||
if (result.isOk()) {
|
||||
this._cachedNewContentSinceEmpty = undefined;
|
||||
this._cachedKnownState = undefined;
|
||||
}
|
||||
|
||||
return ok(true as const);
|
||||
return result;
|
||||
}
|
||||
|
||||
makeNewTrustingTransaction(
|
||||
sessionID: SessionID,
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
) {
|
||||
const result = this.sessions.makeNewTrustingTransaction(
|
||||
sessionID,
|
||||
signerAgent,
|
||||
changes,
|
||||
);
|
||||
|
||||
this._cachedNewContentSinceEmpty = undefined;
|
||||
this._cachedKnownState = undefined;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
makeNewPrivateTransaction(
|
||||
sessionID: SessionID,
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
keyID: KeyID,
|
||||
keySecret: KeySecret,
|
||||
) {
|
||||
const result = this.sessions.makeNewPrivateTransaction(
|
||||
sessionID,
|
||||
signerAgent,
|
||||
changes,
|
||||
keyID,
|
||||
keySecret,
|
||||
);
|
||||
|
||||
this._cachedNewContentSinceEmpty = undefined;
|
||||
this._cachedKnownState = undefined;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getLastSignatureCheckpoint(sessionID: SessionID): number {
|
||||
@@ -166,78 +161,6 @@ export class VerifiedState {
|
||||
);
|
||||
}
|
||||
|
||||
private doAddTransactions(
|
||||
sessionID: SessionID,
|
||||
newTransactions: Transaction[],
|
||||
newSignature: Signature,
|
||||
newStreamingHash?: StreamingHash,
|
||||
) {
|
||||
const sessionLog = this.sessions.get(sessionID);
|
||||
const transactions = sessionLog?.transactions ?? [];
|
||||
|
||||
for (const tx of newTransactions) {
|
||||
transactions.push(tx);
|
||||
}
|
||||
|
||||
const signatureAfter = sessionLog?.signatureAfter ?? {};
|
||||
const lastInbetweenSignatureIdx =
|
||||
this.getLastSignatureCheckpoint(sessionID);
|
||||
|
||||
const sizeOfTxsSinceLastInbetweenSignature = transactions
|
||||
.slice(lastInbetweenSignatureIdx + 1)
|
||||
.reduce((sum, tx) => sum + getTransactionSize(tx), 0);
|
||||
|
||||
if (exceedsRecommendedSize(sizeOfTxsSinceLastInbetweenSignature)) {
|
||||
signatureAfter[transactions.length - 1] = newSignature;
|
||||
}
|
||||
|
||||
this.sessions.set(sessionID, {
|
||||
transactions,
|
||||
streamingHash: newStreamingHash,
|
||||
lastSignature: newSignature,
|
||||
signatureAfter: signatureAfter,
|
||||
});
|
||||
|
||||
this._cachedNewContentSinceEmpty = undefined;
|
||||
this._cachedKnownState = undefined;
|
||||
}
|
||||
|
||||
expectedNewHashAfter(
|
||||
sessionID: SessionID,
|
||||
newTransactions: Transaction[],
|
||||
): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
|
||||
const sessionLog = this.sessions.get(sessionID);
|
||||
|
||||
if (!sessionLog?.streamingHash) {
|
||||
const streamingHash = new StreamingHash(this.crypto);
|
||||
const oldTransactions = sessionLog?.transactions ?? [];
|
||||
|
||||
for (const transaction of oldTransactions) {
|
||||
streamingHash.update(transaction);
|
||||
}
|
||||
|
||||
for (const transaction of newTransactions) {
|
||||
streamingHash.update(transaction);
|
||||
}
|
||||
|
||||
return {
|
||||
expectedNewHash: streamingHash.digest(),
|
||||
newStreamingHash: streamingHash,
|
||||
};
|
||||
}
|
||||
|
||||
const streamingHash = sessionLog.streamingHash.clone();
|
||||
|
||||
for (const transaction of newTransactions) {
|
||||
streamingHash.update(transaction);
|
||||
}
|
||||
|
||||
return {
|
||||
expectedNewHash: streamingHash.digest(),
|
||||
newStreamingHash: streamingHash,
|
||||
};
|
||||
}
|
||||
|
||||
newContentSince(
|
||||
knownState: CoValueKnownState | undefined,
|
||||
): NewContentMessage[] | undefined {
|
||||
@@ -424,6 +347,14 @@ export class VerifiedState {
|
||||
sessions,
|
||||
};
|
||||
}
|
||||
|
||||
decryptTransaction(
|
||||
sessionID: SessionID,
|
||||
txIndex: number,
|
||||
keySecret: KeySecret,
|
||||
): JsonValue[] | undefined {
|
||||
return this.sessions.decryptTransaction(sessionID, txIndex, keySecret);
|
||||
}
|
||||
}
|
||||
|
||||
function getNextKnownSignatureIdx(
|
||||
|
||||
@@ -92,6 +92,10 @@ export class ControlledAccount implements ControlledAccountOrAgent {
|
||||
account: RawAccount<AccountMeta>;
|
||||
agentSecret: AgentSecret;
|
||||
_cachedCurrentAgentID: AgentID | undefined;
|
||||
_cachedCurrentSignerID: SignerID | undefined;
|
||||
_cachedCurrentSignerSecret: SignerSecret | undefined;
|
||||
_cachedCurrentSealerID: SealerID | undefined;
|
||||
_cachedCurrentSealerSecret: SealerSecret | undefined;
|
||||
crypto: CryptoProvider;
|
||||
|
||||
constructor(account: RawAccount<AccountMeta>, agentSecret: AgentSecret) {
|
||||
@@ -114,19 +118,39 @@ export class ControlledAccount implements ControlledAccountOrAgent {
|
||||
}
|
||||
|
||||
currentSignerID() {
|
||||
return this.crypto.getAgentSignerID(this.currentAgentID());
|
||||
if (this._cachedCurrentSignerID) {
|
||||
return this._cachedCurrentSignerID;
|
||||
}
|
||||
const signerID = this.crypto.getAgentSignerID(this.currentAgentID());
|
||||
this._cachedCurrentSignerID = signerID;
|
||||
return signerID;
|
||||
}
|
||||
|
||||
currentSignerSecret(): SignerSecret {
|
||||
return this.crypto.getAgentSignerSecret(this.agentSecret);
|
||||
if (this._cachedCurrentSignerSecret) {
|
||||
return this._cachedCurrentSignerSecret;
|
||||
}
|
||||
const signerSecret = this.crypto.getAgentSignerSecret(this.agentSecret);
|
||||
this._cachedCurrentSignerSecret = signerSecret;
|
||||
return signerSecret;
|
||||
}
|
||||
|
||||
currentSealerID() {
|
||||
return this.crypto.getAgentSealerID(this.currentAgentID());
|
||||
if (this._cachedCurrentSealerID) {
|
||||
return this._cachedCurrentSealerID;
|
||||
}
|
||||
const sealerID = this.crypto.getAgentSealerID(this.currentAgentID());
|
||||
this._cachedCurrentSealerID = sealerID;
|
||||
return sealerID;
|
||||
}
|
||||
|
||||
currentSealerSecret(): SealerSecret {
|
||||
return this.crypto.getAgentSealerSecret(this.agentSecret);
|
||||
if (this._cachedCurrentSealerSecret) {
|
||||
return this._cachedCurrentSealerSecret;
|
||||
}
|
||||
const sealerSecret = this.crypto.getAgentSealerSecret(this.agentSecret);
|
||||
this._cachedCurrentSealerSecret = sealerSecret;
|
||||
return sealerSecret;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,23 +3,32 @@ import { ed25519, x25519 } from "@noble/curves/ed25519";
|
||||
import { blake3 } from "@noble/hashes/blake3";
|
||||
import { base58 } from "@scure/base";
|
||||
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
||||
import { RawCoID, TransactionID } from "../ids.js";
|
||||
import {
|
||||
PrivateTransaction,
|
||||
Transaction,
|
||||
TrustingTransaction,
|
||||
} from "../coValueCore/verifiedState.js";
|
||||
import { RawCoID, SessionID, TransactionID } from "../ids.js";
|
||||
import { Stringified, stableStringify } from "../jsonStringify.js";
|
||||
import { JsonValue } from "../jsonValue.js";
|
||||
import { logger } from "../logger.js";
|
||||
import {
|
||||
CryptoProvider,
|
||||
Encrypted,
|
||||
KeyID,
|
||||
KeySecret,
|
||||
Sealed,
|
||||
SealerID,
|
||||
SealerSecret,
|
||||
SessionLogImpl,
|
||||
Signature,
|
||||
SignerID,
|
||||
SignerSecret,
|
||||
StreamingHash,
|
||||
textDecoder,
|
||||
textEncoder,
|
||||
} from "./crypto.js";
|
||||
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
||||
|
||||
type Blake3State = ReturnType<typeof blake3.create>;
|
||||
|
||||
@@ -67,7 +76,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
||||
return this.blake3HashOnce(input).slice(0, 24);
|
||||
}
|
||||
|
||||
protected generateJsonNonce(material: JsonValue): Uint8Array {
|
||||
generateJsonNonce(material: JsonValue): Uint8Array {
|
||||
return this.generateNonce(textEncoder.encode(stableStringify(material)));
|
||||
}
|
||||
|
||||
@@ -199,4 +208,195 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
createSessionLog(
|
||||
coID: RawCoID,
|
||||
sessionID: SessionID,
|
||||
signerID: SignerID,
|
||||
): SessionLogImpl {
|
||||
return new PureJSSessionLog(coID, sessionID, signerID, this);
|
||||
}
|
||||
}
|
||||
|
||||
export class PureJSSessionLog implements SessionLogImpl {
|
||||
transactions: string[] = [];
|
||||
lastSignature: Signature | undefined;
|
||||
streamingHash: Blake3State;
|
||||
|
||||
constructor(
|
||||
private readonly coID: RawCoID,
|
||||
private readonly sessionID: SessionID,
|
||||
private readonly signerID: SignerID,
|
||||
private readonly crypto: PureJSCrypto,
|
||||
) {
|
||||
this.streamingHash = this.crypto.emptyBlake3State();
|
||||
}
|
||||
|
||||
clone(): SessionLogImpl {
|
||||
const newLog = new PureJSSessionLog(
|
||||
this.coID,
|
||||
this.sessionID,
|
||||
this.signerID,
|
||||
this.crypto,
|
||||
);
|
||||
newLog.transactions = this.transactions.slice();
|
||||
newLog.lastSignature = this.lastSignature;
|
||||
newLog.streamingHash = this.crypto.cloneBlake3State(this.streamingHash);
|
||||
return newLog;
|
||||
}
|
||||
|
||||
tryAdd(
|
||||
transactions: Transaction[],
|
||||
newSignature: Signature,
|
||||
skipVerify: boolean,
|
||||
): void {
|
||||
this.internalTryAdd(
|
||||
transactions.map((tx) => stableStringify(tx)),
|
||||
newSignature,
|
||||
skipVerify,
|
||||
);
|
||||
}
|
||||
|
||||
internalTryAdd(
|
||||
transactions: string[],
|
||||
newSignature: Signature,
|
||||
skipVerify: boolean,
|
||||
) {
|
||||
if (!skipVerify) {
|
||||
const checkHasher = this.crypto.cloneBlake3State(this.streamingHash);
|
||||
|
||||
for (const tx of transactions) {
|
||||
checkHasher.update(textEncoder.encode(tx));
|
||||
}
|
||||
const newHash = checkHasher.digest();
|
||||
const newHashEncoded = `hash_z${base58.encode(newHash)}`;
|
||||
|
||||
if (!this.crypto.verify(newSignature, newHashEncoded, this.signerID)) {
|
||||
throw new Error("Signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
this.crypto.blake3IncrementalUpdate(
|
||||
this.streamingHash,
|
||||
textEncoder.encode(tx),
|
||||
);
|
||||
this.transactions.push(tx);
|
||||
}
|
||||
|
||||
this.lastSignature = newSignature;
|
||||
|
||||
return newSignature;
|
||||
}
|
||||
|
||||
expectedHashAfter(transactionsJson: string[]): string {
|
||||
const hasher = this.crypto.cloneBlake3State(this.streamingHash);
|
||||
for (const tx of transactionsJson) {
|
||||
hasher.update(textEncoder.encode(tx));
|
||||
}
|
||||
const newHash = hasher.digest();
|
||||
return `hash_z${base58.encode(newHash)}`;
|
||||
}
|
||||
|
||||
internalAddNewTransaction(
|
||||
transaction: string,
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
) {
|
||||
this.crypto.blake3IncrementalUpdate(
|
||||
this.streamingHash,
|
||||
textEncoder.encode(transaction),
|
||||
);
|
||||
const newHash = this.crypto.blake3DigestForState(this.streamingHash);
|
||||
const newHashEncoded = `hash_z${base58.encode(newHash)}`;
|
||||
const signature = this.crypto.sign(
|
||||
signerAgent.currentSignerSecret(),
|
||||
newHashEncoded,
|
||||
);
|
||||
this.transactions.push(transaction);
|
||||
this.lastSignature = signature;
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
addNewPrivateTransaction(
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
keyID: KeyID,
|
||||
keySecret: KeySecret,
|
||||
madeAt: number,
|
||||
): { signature: Signature; transaction: PrivateTransaction } {
|
||||
const encryptedChanges = this.crypto.encrypt(changes, keySecret, {
|
||||
in: this.coID,
|
||||
tx: { sessionID: this.sessionID, txIndex: this.transactions.length },
|
||||
});
|
||||
const tx = {
|
||||
encryptedChanges: encryptedChanges,
|
||||
madeAt: madeAt,
|
||||
privacy: "private",
|
||||
keyUsed: keyID,
|
||||
} satisfies Transaction;
|
||||
const signature = this.internalAddNewTransaction(
|
||||
stableStringify(tx),
|
||||
signerAgent,
|
||||
);
|
||||
return {
|
||||
signature: signature as Signature,
|
||||
transaction: tx,
|
||||
};
|
||||
}
|
||||
|
||||
addNewTrustingTransaction(
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
madeAt: number,
|
||||
): { signature: Signature; transaction: TrustingTransaction } {
|
||||
const tx = {
|
||||
changes: stableStringify(changes),
|
||||
madeAt: madeAt,
|
||||
privacy: "trusting",
|
||||
} satisfies Transaction;
|
||||
const signature = this.internalAddNewTransaction(
|
||||
stableStringify(tx),
|
||||
signerAgent,
|
||||
);
|
||||
return {
|
||||
signature: signature as Signature,
|
||||
transaction: tx,
|
||||
};
|
||||
}
|
||||
|
||||
decryptNextTransactionChangesJson(
|
||||
txIndex: number,
|
||||
keySecret: KeySecret,
|
||||
): string {
|
||||
const txJson = this.transactions[txIndex];
|
||||
if (!txJson) {
|
||||
throw new Error("Transaction not found");
|
||||
}
|
||||
const tx = JSON.parse(txJson) as Transaction;
|
||||
if (tx.privacy === "private") {
|
||||
const nOnceMaterial = {
|
||||
in: this.coID,
|
||||
tx: { sessionID: this.sessionID, txIndex: txIndex },
|
||||
};
|
||||
|
||||
const nOnce = this.crypto.generateJsonNonce(nOnceMaterial);
|
||||
|
||||
const ciphertext = base64URLtoBytes(
|
||||
tx.encryptedChanges.substring("encrypted_U".length),
|
||||
);
|
||||
const keySecretBytes = base58.decode(
|
||||
keySecret.substring("keySecret_z".length),
|
||||
);
|
||||
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
||||
|
||||
return textDecoder.decode(plaintext);
|
||||
} else {
|
||||
return tx.changes;
|
||||
}
|
||||
}
|
||||
|
||||
free(): void {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {
|
||||
SessionLog,
|
||||
initialize,
|
||||
Blake3Hasher,
|
||||
blake3_empty_state,
|
||||
blake3_hash_once,
|
||||
@@ -7,16 +9,15 @@ import {
|
||||
encrypt,
|
||||
get_sealer_id,
|
||||
get_signer_id,
|
||||
initialize,
|
||||
new_ed25519_signing_key,
|
||||
new_x25519_private_key,
|
||||
seal,
|
||||
sign,
|
||||
unseal,
|
||||
verify,
|
||||
} from "jazz-crypto-rs";
|
||||
} from "cojson-core-wasm";
|
||||
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
||||
import { RawCoID, TransactionID } from "../ids.js";
|
||||
import { RawCoID, SessionID, TransactionID } from "../ids.js";
|
||||
import { Stringified, stableStringify } from "../jsonStringify.js";
|
||||
import { JsonValue } from "../jsonValue.js";
|
||||
import { logger } from "../logger.js";
|
||||
@@ -24,6 +25,8 @@ import { PureJSCrypto } from "./PureJSCrypto.js";
|
||||
import {
|
||||
CryptoProvider,
|
||||
Encrypted,
|
||||
Hash,
|
||||
KeyID,
|
||||
KeySecret,
|
||||
Sealed,
|
||||
SealerID,
|
||||
@@ -34,11 +37,17 @@ import {
|
||||
textDecoder,
|
||||
textEncoder,
|
||||
} from "./crypto.js";
|
||||
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
||||
import {
|
||||
PrivateTransaction,
|
||||
Transaction,
|
||||
TrustingTransaction,
|
||||
} from "../coValueCore/verifiedState.js";
|
||||
|
||||
type Blake3State = Blake3Hasher;
|
||||
|
||||
/**
|
||||
* WebAssembly implementation of the CryptoProvider interface using jazz-crypto-rs.
|
||||
* WebAssembly implementation of the CryptoProvider interface using cojson-core-wasm.
|
||||
* This provides the primary implementation using WebAssembly for optimal performance, offering:
|
||||
* - Signing/verifying (Ed25519)
|
||||
* - Encryption/decryption (XSalsa20)
|
||||
@@ -195,4 +204,86 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
createSessionLog(coID: RawCoID, sessionID: SessionID, signerID: SignerID) {
|
||||
return new SessionLogAdapter(new SessionLog(coID, sessionID, signerID));
|
||||
}
|
||||
}
|
||||
|
||||
class SessionLogAdapter {
|
||||
constructor(private readonly sessionLog: SessionLog) {}
|
||||
|
||||
tryAdd(
|
||||
transactions: Transaction[],
|
||||
newSignature: Signature,
|
||||
skipVerify: boolean,
|
||||
): void {
|
||||
this.sessionLog.tryAdd(
|
||||
transactions.map((tx) => stableStringify(tx)),
|
||||
newSignature,
|
||||
skipVerify,
|
||||
);
|
||||
}
|
||||
|
||||
addNewPrivateTransaction(
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
keyID: KeyID,
|
||||
keySecret: KeySecret,
|
||||
madeAt: number,
|
||||
): { signature: Signature; transaction: PrivateTransaction } {
|
||||
const output = this.sessionLog.addNewPrivateTransaction(
|
||||
stableStringify(changes),
|
||||
signerAgent.currentSignerSecret(),
|
||||
keySecret,
|
||||
keyID,
|
||||
madeAt,
|
||||
);
|
||||
const parsedOutput = JSON.parse(output);
|
||||
const transaction: PrivateTransaction = {
|
||||
privacy: "private",
|
||||
madeAt,
|
||||
encryptedChanges: parsedOutput.encrypted_changes,
|
||||
keyUsed: keyID,
|
||||
};
|
||||
return { signature: parsedOutput.signature, transaction };
|
||||
}
|
||||
|
||||
addNewTrustingTransaction(
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
madeAt: number,
|
||||
): { signature: Signature; transaction: TrustingTransaction } {
|
||||
const stringifiedChanges = stableStringify(changes);
|
||||
const output = this.sessionLog.addNewTrustingTransaction(
|
||||
stringifiedChanges,
|
||||
signerAgent.currentSignerSecret(),
|
||||
madeAt,
|
||||
);
|
||||
const transaction: TrustingTransaction = {
|
||||
privacy: "trusting",
|
||||
madeAt,
|
||||
changes: stringifiedChanges,
|
||||
};
|
||||
return { signature: output as Signature, transaction };
|
||||
}
|
||||
|
||||
decryptNextTransactionChangesJson(
|
||||
txIndex: number,
|
||||
keySecret: KeySecret,
|
||||
): string {
|
||||
const output = this.sessionLog.decryptNextTransactionChangesJson(
|
||||
txIndex,
|
||||
keySecret,
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
free() {
|
||||
this.sessionLog.free();
|
||||
}
|
||||
|
||||
clone(): SessionLogAdapter {
|
||||
return new SessionLogAdapter(this.sessionLog.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { base58 } from "@scure/base";
|
||||
import { RawAccountID } from "../coValues/account.js";
|
||||
import { ControlledAccountOrAgent, RawAccountID } from "../coValues/account.js";
|
||||
import { AgentID, RawCoID, TransactionID } from "../ids.js";
|
||||
import { SessionID } from "../ids.js";
|
||||
import { Stringified, parseJSON, stableStringify } from "../jsonStringify.js";
|
||||
import { JsonValue } from "../jsonValue.js";
|
||||
import { logger } from "../logger.js";
|
||||
import {
|
||||
PrivateTransaction,
|
||||
Transaction,
|
||||
TrustingTransaction,
|
||||
} from "../coValueCore/verifiedState.js";
|
||||
|
||||
function randomBytes(bytesLength = 32): Uint8Array {
|
||||
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
||||
@@ -297,6 +302,12 @@ export abstract class CryptoProvider<Blake3State = any> {
|
||||
newRandomSessionID(accountID: RawAccountID | AgentID): SessionID {
|
||||
return `${accountID}_session_z${base58.encode(this.randomBytes(8))}`;
|
||||
}
|
||||
|
||||
abstract createSessionLog(
|
||||
coID: RawCoID,
|
||||
sessionID: SessionID,
|
||||
signerID: SignerID,
|
||||
): SessionLogImpl;
|
||||
}
|
||||
|
||||
export type Hash = `hash_z${string}`;
|
||||
@@ -341,3 +352,29 @@ export type KeySecret = `keySecret_z${string}`;
|
||||
export type KeyID = `key_z${string}`;
|
||||
|
||||
export const secretSeedLength = 32;
|
||||
|
||||
export interface SessionLogImpl {
|
||||
clone(): SessionLogImpl;
|
||||
tryAdd(
|
||||
transactions: Transaction[],
|
||||
newSignature: Signature,
|
||||
skipVerify: boolean,
|
||||
): void;
|
||||
addNewPrivateTransaction(
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
keyID: KeyID,
|
||||
keySecret: KeySecret,
|
||||
madeAt: number,
|
||||
): { signature: Signature; transaction: PrivateTransaction };
|
||||
addNewTrustingTransaction(
|
||||
signerAgent: ControlledAccountOrAgent,
|
||||
changes: JsonValue[],
|
||||
madeAt: number,
|
||||
): { signature: Signature; transaction: TrustingTransaction };
|
||||
decryptNextTransactionChangesJson(
|
||||
tx_index: number,
|
||||
key_secret: KeySecret,
|
||||
): string;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
@@ -132,17 +132,25 @@ export class LocalNode {
|
||||
return accountOrAgentIDfromSessionID(this.currentSessionID);
|
||||
}
|
||||
|
||||
_cachedCurrentAgent: ControlledAccountOrAgent | undefined;
|
||||
getCurrentAgent(): ControlledAccountOrAgent {
|
||||
const accountOrAgent = this.getCurrentAccountOrAgentID();
|
||||
if (isAgentID(accountOrAgent)) {
|
||||
return new ControlledAgent(this.agentSecret, this.crypto);
|
||||
if (!this._cachedCurrentAgent) {
|
||||
const accountOrAgent = this.getCurrentAccountOrAgentID();
|
||||
if (isAgentID(accountOrAgent)) {
|
||||
this._cachedCurrentAgent = new ControlledAgent(
|
||||
this.agentSecret,
|
||||
this.crypto,
|
||||
);
|
||||
} else {
|
||||
this._cachedCurrentAgent = new ControlledAccount(
|
||||
expectAccount(
|
||||
this.expectCoValueLoaded(accountOrAgent).getCurrentContent(),
|
||||
),
|
||||
this.agentSecret,
|
||||
);
|
||||
}
|
||||
}
|
||||
return new ControlledAccount(
|
||||
expectAccount(
|
||||
this.expectCoValueLoaded(accountOrAgent).getCurrentContent(),
|
||||
),
|
||||
this.agentSecret,
|
||||
);
|
||||
return this._cachedCurrentAgent;
|
||||
}
|
||||
|
||||
expectCurrentAccountID(reason: string): RawAccountID {
|
||||
@@ -360,7 +368,7 @@ export class LocalNode {
|
||||
|
||||
const coValue = this.putCoValue(
|
||||
id,
|
||||
new VerifiedState(id, this.crypto, header, new Map()),
|
||||
new VerifiedState(id, this.crypto, header),
|
||||
);
|
||||
|
||||
this.garbageCollector?.trackCoValueAccess(coValue);
|
||||
|
||||
130
packages/cojson/src/tests/PureJSCrypto.test.ts
Normal file
130
packages/cojson/src/tests/PureJSCrypto.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { assert, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
loadCoValueOrFail,
|
||||
setCurrentTestCryptoProvider,
|
||||
setupTestNode,
|
||||
setupTestAccount,
|
||||
} from "./testUtils";
|
||||
import { PureJSCrypto } from "../crypto/PureJSCrypto";
|
||||
import { stableStringify } from "../jsonStringify";
|
||||
|
||||
const jsCrypto = await PureJSCrypto.create();
|
||||
setCurrentTestCryptoProvider(jsCrypto);
|
||||
|
||||
let syncServer: ReturnType<typeof setupTestNode>;
|
||||
|
||||
beforeEach(() => {
|
||||
syncServer = setupTestNode({ isSyncServer: true });
|
||||
});
|
||||
|
||||
// A suite of tests focused on high-level tests that verify:
|
||||
// - Keys creation and unsealing
|
||||
// - Signature creation and verification
|
||||
// - Encryption and decryption of values
|
||||
describe("PureJSCrypto", () => {
|
||||
it("successfully creates a private CoValue and reads it in another session", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("count", 0, "private");
|
||||
map.set("count", 1, "private");
|
||||
map.set("count", 2, "private");
|
||||
|
||||
const client2 = client.spawnNewSession();
|
||||
|
||||
const mapInTheOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
||||
|
||||
expect(mapInTheOtherSession.get("count")).toEqual(2);
|
||||
});
|
||||
|
||||
it("successfully updates a private CoValue and reads it in another session", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("count", 0, "private");
|
||||
map.set("count", 1, "private");
|
||||
map.set("count", 2, "private");
|
||||
|
||||
const client2 = client.spawnNewSession();
|
||||
|
||||
const mapInTheOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
||||
mapInTheOtherSession.set("count", 3, "private");
|
||||
|
||||
await mapInTheOtherSession.core.waitForSync();
|
||||
|
||||
expect(mapInTheOtherSession.get("count")).toEqual(3);
|
||||
});
|
||||
|
||||
it("can invite another account to a group and share a private CoValue", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
const account = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const invite = group.createInvite("admin");
|
||||
|
||||
await account.node.acceptInvite(group.id, invite);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("secret", "private-data", "private");
|
||||
|
||||
// The other account should be able to read the private value
|
||||
const mapInOtherSession = await loadCoValueOrFail(account.node, map.id);
|
||||
expect(mapInOtherSession.get("secret")).toEqual("private-data");
|
||||
|
||||
mapInOtherSession.set("secret", "modified", "private");
|
||||
|
||||
await mapInOtherSession.core.waitForSync();
|
||||
|
||||
expect(map.get("secret")).toEqual("modified");
|
||||
});
|
||||
|
||||
it("rejects sessions with invalid signatures", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("count", 0, "trusting");
|
||||
|
||||
// Create a new session with the same agent
|
||||
const client2 = client.spawnNewSession();
|
||||
|
||||
// This should work normally
|
||||
const mapInOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
||||
expect(mapInOtherSession.get("count")).toEqual(0);
|
||||
|
||||
mapInOtherSession.core.tryAddTransactions(
|
||||
client2.node.currentSessionID,
|
||||
[
|
||||
{
|
||||
privacy: "trusting",
|
||||
changes: stableStringify([{ op: "set", key: "count", value: 1 }]),
|
||||
madeAt: Date.now(),
|
||||
},
|
||||
],
|
||||
"hash_z12345678",
|
||||
"signature_z12345678",
|
||||
"immediate",
|
||||
true,
|
||||
);
|
||||
|
||||
const content =
|
||||
mapInOtherSession.core.verified.newContentSince(undefined)?.[0];
|
||||
assert(content);
|
||||
|
||||
client.node.syncManager.handleNewContent(content, "storage");
|
||||
|
||||
expect(map.get("count")).toEqual(0);
|
||||
});
|
||||
});
|
||||
130
packages/cojson/src/tests/WasmCrypto.test.ts
Normal file
130
packages/cojson/src/tests/WasmCrypto.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { assert, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
loadCoValueOrFail,
|
||||
setCurrentTestCryptoProvider,
|
||||
setupTestNode,
|
||||
setupTestAccount,
|
||||
} from "./testUtils";
|
||||
import { stableStringify } from "../jsonStringify";
|
||||
import { WasmCrypto } from "../crypto/WasmCrypto";
|
||||
|
||||
const wasmCrypto = await WasmCrypto.create();
|
||||
setCurrentTestCryptoProvider(wasmCrypto);
|
||||
|
||||
let syncServer: ReturnType<typeof setupTestNode>;
|
||||
|
||||
beforeEach(() => {
|
||||
syncServer = setupTestNode({ isSyncServer: true });
|
||||
});
|
||||
|
||||
// A suite of tests focused on high-level tests that verify:
|
||||
// - Keys creation and unsealing
|
||||
// - Signature creation and verification
|
||||
// - Encryption and decryption of values
|
||||
describe("WasmCrypto", () => {
|
||||
it("successfully creates a private CoValue and reads it in another session", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("count", 0, "private");
|
||||
map.set("count", 1, "private");
|
||||
map.set("count", 2, "private");
|
||||
|
||||
const client2 = client.spawnNewSession();
|
||||
|
||||
const mapInTheOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
||||
|
||||
expect(mapInTheOtherSession.get("count")).toEqual(2);
|
||||
});
|
||||
|
||||
it("successfully updates a private CoValue and reads it in another session", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("count", 0, "private");
|
||||
map.set("count", 1, "private");
|
||||
map.set("count", 2, "private");
|
||||
|
||||
const client2 = client.spawnNewSession();
|
||||
|
||||
const mapInTheOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
||||
mapInTheOtherSession.set("count", 3, "private");
|
||||
|
||||
await mapInTheOtherSession.core.waitForSync();
|
||||
|
||||
expect(mapInTheOtherSession.get("count")).toEqual(3);
|
||||
});
|
||||
|
||||
it("can invite another account to a group and share a private CoValue", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
const account = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const invite = group.createInvite("admin");
|
||||
|
||||
await account.node.acceptInvite(group.id, invite);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("secret", "private-data", "private");
|
||||
|
||||
// The other account should be able to read the private value
|
||||
const mapInOtherSession = await loadCoValueOrFail(account.node, map.id);
|
||||
expect(mapInOtherSession.get("secret")).toEqual("private-data");
|
||||
|
||||
mapInOtherSession.set("secret", "modified", "private");
|
||||
|
||||
await mapInOtherSession.core.waitForSync();
|
||||
|
||||
expect(map.get("secret")).toEqual("modified");
|
||||
});
|
||||
|
||||
it("rejects sessions with invalid signatures", async () => {
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("count", 0, "trusting");
|
||||
|
||||
// Create a new session with the same agent
|
||||
const client2 = client.spawnNewSession();
|
||||
|
||||
// This should work normally
|
||||
const mapInOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
||||
expect(mapInOtherSession.get("count")).toEqual(0);
|
||||
|
||||
mapInOtherSession.core.tryAddTransactions(
|
||||
client2.node.currentSessionID,
|
||||
[
|
||||
{
|
||||
privacy: "trusting",
|
||||
changes: stableStringify([{ op: "set", key: "count", value: 1 }]),
|
||||
madeAt: Date.now(),
|
||||
},
|
||||
],
|
||||
"hash_z12345678",
|
||||
"signature_z12345678",
|
||||
"immediate",
|
||||
true,
|
||||
);
|
||||
|
||||
const content =
|
||||
mapInOtherSession.core.verified.newContentSince(undefined)?.[0];
|
||||
assert(content);
|
||||
|
||||
client.node.syncManager.handleNewContent(content, "storage");
|
||||
|
||||
expect(map.get("count")).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { encrypt } from "jazz-crypto-rs";
|
||||
import {
|
||||
assert,
|
||||
afterEach,
|
||||
@@ -8,15 +7,13 @@ import {
|
||||
test,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { bytesToBase64url } from "../base64url.js";
|
||||
import { CoValueCore } from "../coValueCore/coValueCore.js";
|
||||
import { Transaction } from "../coValueCore/verifiedState.js";
|
||||
import { MapOpPayload } from "../coValues/coMap.js";
|
||||
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
||||
import { stableStringify } from "../jsonStringify.js";
|
||||
import { LocalNode } from "../localNode.js";
|
||||
import { Role } from "../permissions.js";
|
||||
import {
|
||||
agentAndSessionIDFromSecret,
|
||||
createTestMetricReader,
|
||||
createTestNode,
|
||||
createTwoConnectedNodes,
|
||||
@@ -25,10 +22,13 @@ import {
|
||||
randomAgentAndSessionID,
|
||||
tearDownTestMetricReader,
|
||||
} from "./testUtils.js";
|
||||
import { CO_VALUE_PRIORITY } from "../priority.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
let metricReader: ReturnType<typeof createTestMetricReader>;
|
||||
const agentSecret =
|
||||
"sealerSecret_zE3Nr7YFr1KkVbJSx4JDCzYn4ApYdm8kJ5ghNBxREHQya/signerSecret_z9fEu4eNG1eXHMak3YSzY7uLdoG8HESSJ8YW4xWdNNDSP";
|
||||
|
||||
beforeEach(() => {
|
||||
metricReader = createTestMetricReader();
|
||||
@@ -38,47 +38,7 @@ afterEach(() => {
|
||||
tearDownTestMetricReader();
|
||||
});
|
||||
|
||||
test("Can create coValue with new agent credentials and add transaction to it", () => {
|
||||
const [agent, sessionID] = randomAgentAndSessionID();
|
||||
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "costream",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const transaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "world",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.verified.expectedNewHashAfter(
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
);
|
||||
|
||||
expect(
|
||||
coValue
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
Crypto.sign(agent.currentSignerSecret(), expectedNewHash),
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrap(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("transactions with wrong signature are rejected", () => {
|
||||
const wrongAgent = Crypto.newRandomAgentSecret();
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
@@ -88,88 +48,53 @@ test("transactions with wrong signature are rejected", () => {
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const transaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "world",
|
||||
},
|
||||
]),
|
||||
};
|
||||
const { transaction, signature } =
|
||||
coValue.verified.makeNewTrustingTransaction(
|
||||
node.currentSessionID,
|
||||
node.getCurrentAgent(),
|
||||
[{ hello: "world" }],
|
||||
);
|
||||
|
||||
const { expectedNewHash } = coValue.verified.expectedNewHashAfter(
|
||||
transaction.madeAt = Date.now() + 1000;
|
||||
|
||||
// Delete the transaction from the coValue
|
||||
node.internalDeleteCoValue(coValue.id);
|
||||
node.syncManager.handleNewContent(
|
||||
{
|
||||
action: "content",
|
||||
id: coValue.id,
|
||||
header: coValue.verified.header,
|
||||
priority: CO_VALUE_PRIORITY.LOW,
|
||||
new: {},
|
||||
},
|
||||
"import",
|
||||
);
|
||||
|
||||
const newEntry = node.getCoValue(coValue.id);
|
||||
|
||||
// eslint-disable-next-line neverthrow/must-use-result
|
||||
const result = newEntry.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
undefined,
|
||||
signature,
|
||||
"immediate",
|
||||
);
|
||||
|
||||
// eslint-disable-next-line neverthrow/must-use-result
|
||||
coValue
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
Crypto.sign(Crypto.getAgentSignerSecret(wrongAgent), expectedNewHash),
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrapErr({ withStackTrace: true });
|
||||
});
|
||||
|
||||
test("transactions with correctly signed, but wrong hash are rejected", () => {
|
||||
const [agent, sessionID] = randomAgentAndSessionID();
|
||||
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "costream",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const transaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "world",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.verified.expectedNewHashAfter(
|
||||
node.currentSessionID,
|
||||
[
|
||||
{
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "wrong",
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line neverthrow/must-use-result
|
||||
coValue
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
Crypto.sign(agent.currentSignerSecret(), expectedNewHash),
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrapErr({ withStackTrace: true });
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(newEntry.getValidSortedTransactions().length).toBe(0);
|
||||
});
|
||||
|
||||
test("New transactions in a group correctly update owned values, including subscriptions", async () => {
|
||||
const [agent, sessionID] = randomAgentAndSessionID();
|
||||
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
||||
|
||||
const group = node.createGroup();
|
||||
const timeBeforeEdit = Date.now() - 1000;
|
||||
const dateNowMock = vi
|
||||
.spyOn(Date, "now")
|
||||
.mockImplementation(() => timeBeforeEdit);
|
||||
|
||||
const timeBeforeEdit = Date.now();
|
||||
const group = node.createGroup();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
@@ -179,49 +104,31 @@ test("New transactions in a group correctly update owned values, including subsc
|
||||
|
||||
const listener = vi.fn();
|
||||
|
||||
map.subscribe(listener);
|
||||
map.subscribe((map) => {
|
||||
listener(map.get("hello"));
|
||||
});
|
||||
|
||||
expect(listener.mock.calls[0]?.[0].get("hello")).toBe("world");
|
||||
expect(listener).toHaveBeenLastCalledWith("world");
|
||||
|
||||
const resignationThatWeJustLearnedAbout = {
|
||||
privacy: "trusting",
|
||||
madeAt: timeBeforeEdit,
|
||||
changes: stableStringify([
|
||||
expect(map.core.getValidSortedTransactions().length).toBe(1);
|
||||
expect(group.get(agent.id)).toBe("admin");
|
||||
|
||||
group.core.makeTransaction(
|
||||
[
|
||||
{
|
||||
op: "set",
|
||||
key: agent.id,
|
||||
value: "revoked",
|
||||
} satisfies MapOpPayload<typeof agent.id, Role>,
|
||||
]),
|
||||
} satisfies Transaction;
|
||||
|
||||
const { expectedNewHash } = group.core.verified.expectedNewHashAfter(
|
||||
sessionID,
|
||||
[resignationThatWeJustLearnedAbout],
|
||||
},
|
||||
],
|
||||
"trusting",
|
||||
);
|
||||
|
||||
const signature = Crypto.sign(
|
||||
node.getCurrentAgent().currentSignerSecret(),
|
||||
expectedNewHash,
|
||||
);
|
||||
|
||||
expect(map.core.getValidSortedTransactions().length).toBe(1);
|
||||
|
||||
const manuallyAdddedTxSuccess = group.core
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[resignationThatWeJustLearnedAbout],
|
||||
expectedNewHash,
|
||||
signature,
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrap({ withStackTrace: true });
|
||||
|
||||
expect(manuallyAdddedTxSuccess).toBe(true);
|
||||
|
||||
expect(listener.mock.calls.length).toBe(2);
|
||||
expect(listener.mock.calls[1]?.[0].get("hello")).toBe(undefined);
|
||||
expect(group.get(agent.id)).toBe("revoked");
|
||||
dateNowMock.mockReset();
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
expect(listener).toHaveBeenLastCalledWith(undefined);
|
||||
expect(map.core.getValidSortedTransactions().length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -359,166 +266,51 @@ test("listeners are notified even if the previous listener threw an error", asyn
|
||||
errorLog.mockRestore();
|
||||
});
|
||||
|
||||
test("getValidTransactions should skip trusting transactions with invalid JSON", () => {
|
||||
const [agent, sessionID] = randomAgentAndSessionID();
|
||||
test("getValidTransactions should skip private transactions with invalid JSON", () => {
|
||||
const [agent, sessionID] = agentAndSessionIDFromSecret(agentSecret);
|
||||
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "costream",
|
||||
ruleset: { type: "unsafeAllowAll" },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
const fixtures = {
|
||||
id: "co_zWwrEiushQLvbkWd6Z3L8WxTU1r",
|
||||
signature:
|
||||
"signature_z3ktW7wxMnW7VYExCGZv4Ug2UJSW3ag6zLDiP8GpZThzif6veJt7JipYpUgshhuGbgHtLcWywWSWysV7hChxFypDt",
|
||||
decrypted:
|
||||
'[{"after":"start","op":"app","value":"co_zMphsnYN6GU8nn2HDY5suvyGufY"}]',
|
||||
key: {
|
||||
secret: "keySecret_z3dU66SsyQkkGKpNCJW6NX74MnfVGHUyY7r85b4M8X88L",
|
||||
id: "key_z5XUAHyoqUV9zXWvMK",
|
||||
},
|
||||
transaction: {
|
||||
privacy: "private",
|
||||
madeAt: 0,
|
||||
encryptedChanges:
|
||||
"encrypted_UNAxqdUSGRZ2rzuLU99AFPKCe2C0HwsTzMWQreXZqLr6RpWrSMa-5lwgwIev7xPHTgZFq5UyUgMFrO9zlHJHJGgjJcDzFihY=" as any,
|
||||
keyUsed: "key_z5XUAHyoqUV9zXWvMK",
|
||||
},
|
||||
session:
|
||||
"sealer_z5yhsCCe2XwLTZC4254mUoMASshm3Diq49JrefPpjTktp/signer_z7gVGDpNz9qUtsRxAkHMuu4DYdtVVCG4XELTKPYdoYLPr_session_z9mDP8FoonSA",
|
||||
} as const;
|
||||
|
||||
// Create a valid transaction first
|
||||
const validTransaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: stableStringify([{ hello: "world" }]),
|
||||
};
|
||||
const group = node.createGroup();
|
||||
const map = group.createMap();
|
||||
|
||||
const { expectedNewHash: expectedNewHash1 } =
|
||||
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
|
||||
validTransaction,
|
||||
]);
|
||||
map.set("hello", "world");
|
||||
|
||||
coValue
|
||||
// This should fail silently, because the encryptedChanges will be outputted as gibberish
|
||||
map.core
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[validTransaction],
|
||||
expectedNewHash1,
|
||||
Crypto.sign(agent.currentSignerSecret(), expectedNewHash1),
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrap();
|
||||
|
||||
// Create an invalid transaction with malformed JSON
|
||||
const invalidTransaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now() + 1,
|
||||
changes: '{"invalid": json}' as any, // Invalid JSON string
|
||||
};
|
||||
|
||||
const { expectedNewHash: expectedNewHash2 } =
|
||||
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
|
||||
invalidTransaction,
|
||||
]);
|
||||
|
||||
coValue
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[invalidTransaction],
|
||||
expectedNewHash2,
|
||||
Crypto.sign(agent.currentSignerSecret(), expectedNewHash2),
|
||||
fixtures.session,
|
||||
[fixtures.transaction],
|
||||
undefined,
|
||||
fixtures.signature,
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrap();
|
||||
|
||||
// Get valid transactions - should only include the valid one
|
||||
const validTransactions = coValue.getValidTransactions();
|
||||
const validTransactions = map.core.getValidTransactions();
|
||||
|
||||
expect(validTransactions).toHaveLength(1);
|
||||
expect(validTransactions[0]?.changes).toEqual([{ hello: "world" }]);
|
||||
});
|
||||
|
||||
test("getValidTransactions should skip private transactions with invalid JSON", () => {
|
||||
const [agent, sessionID] = randomAgentAndSessionID();
|
||||
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
||||
|
||||
const group = node.createGroup();
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
const coValue = node.createCoValue({
|
||||
type: "costream",
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const { secret: keySecret, id: keyID } = coValue.getCurrentReadKey();
|
||||
|
||||
assert(keySecret);
|
||||
|
||||
const encrypted = Crypto.encryptForTransaction(
|
||||
[{ hello: "world" }],
|
||||
keySecret,
|
||||
{
|
||||
in: coValue.id,
|
||||
tx: coValue.nextTransactionID(),
|
||||
},
|
||||
);
|
||||
|
||||
// Create a valid private transaction first
|
||||
const validTransaction: Transaction = {
|
||||
privacy: "private",
|
||||
madeAt: Date.now(),
|
||||
keyUsed: keyID,
|
||||
encryptedChanges: encrypted as any,
|
||||
};
|
||||
|
||||
const { expectedNewHash: expectedNewHash1 } =
|
||||
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
|
||||
validTransaction,
|
||||
]);
|
||||
|
||||
coValue
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[validTransaction],
|
||||
expectedNewHash1,
|
||||
Crypto.sign(agent.currentSignerSecret(), expectedNewHash1),
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrap();
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const brokenChange = `encrypted_U${bytesToBase64url(
|
||||
encrypt(
|
||||
textEncoder.encode('{"invalid": json}'),
|
||||
keySecret,
|
||||
textEncoder.encode(
|
||||
stableStringify({
|
||||
in: coValue.id,
|
||||
tx: coValue.nextTransactionID(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)}`;
|
||||
|
||||
// Create an invalid private transaction with malformed JSON after decryption
|
||||
const invalidTransaction: Transaction = {
|
||||
privacy: "private",
|
||||
madeAt: Date.now() + 1,
|
||||
keyUsed: keyID,
|
||||
encryptedChanges: brokenChange as any,
|
||||
};
|
||||
|
||||
const { expectedNewHash: expectedNewHash2 } =
|
||||
coValue.verified.expectedNewHashAfter(node.currentSessionID, [
|
||||
invalidTransaction,
|
||||
]);
|
||||
|
||||
coValue
|
||||
.tryAddTransactions(
|
||||
node.currentSessionID,
|
||||
[invalidTransaction],
|
||||
expectedNewHash2,
|
||||
Crypto.sign(agent.currentSignerSecret(), expectedNewHash2),
|
||||
"immediate",
|
||||
)
|
||||
._unsafeUnwrap();
|
||||
|
||||
// Get valid transactions - should skip the invalid one
|
||||
const validTransactions = coValue.getValidTransactions({
|
||||
ignorePrivateTransactions: false,
|
||||
});
|
||||
|
||||
// Since we can't easily create valid private transactions in this test setup,
|
||||
// we just verify that the method doesn't crash and handles the invalid JSON gracefully
|
||||
expect(validTransactions).toBeDefined();
|
||||
expect(Array.isArray(validTransactions)).toBe(true);
|
||||
expect(validTransactions.length).toBe(1);
|
||||
expect(validTransactions[0]?.changes).toEqual([{ hello: "world" }]);
|
||||
});
|
||||
|
||||
describe("markErrored and isErroredInPeer", () => {
|
||||
|
||||
142
packages/cojson/src/tests/coreWasm.test.ts
Normal file
142
packages/cojson/src/tests/coreWasm.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { assert, describe, expect, it } from "vitest";
|
||||
import { WasmCrypto } from "../crypto/WasmCrypto";
|
||||
import { JsonValue, LocalNode, SessionID } from "../exports";
|
||||
import {
|
||||
agentAndSessionIDFromSecret,
|
||||
randomAgentAndSessionID,
|
||||
} from "./testUtils";
|
||||
import { PureJSCrypto } from "../crypto/PureJSCrypto";
|
||||
import { Encrypted } from "../crypto/crypto";
|
||||
import { PrivateTransaction } from "../coValueCore/verifiedState";
|
||||
|
||||
const wasmCrypto = await WasmCrypto.create();
|
||||
const jsCrypto = await PureJSCrypto.create();
|
||||
|
||||
const agentSecret =
|
||||
"sealerSecret_zE3Nr7YFr1KkVbJSx4JDCzYn4ApYdm8kJ5ghNBxREHQya/signerSecret_z9fEu4eNG1eXHMak3YSzY7uLdoG8HESSJ8YW4xWdNNDSP";
|
||||
|
||||
function createTestNode() {
|
||||
const [agent, session] = agentAndSessionIDFromSecret(agentSecret);
|
||||
return {
|
||||
agent,
|
||||
session,
|
||||
node: new LocalNode(agent.agentSecret, session, jsCrypto),
|
||||
};
|
||||
}
|
||||
|
||||
describe("SessionLog WASM", () => {
|
||||
it("it works", () => {
|
||||
const [agent, sessionId] = agentAndSessionIDFromSecret(agentSecret);
|
||||
|
||||
const session = wasmCrypto.createSessionLog(
|
||||
"co_test1" as any,
|
||||
sessionId,
|
||||
agent.currentSignerID(),
|
||||
);
|
||||
|
||||
expect(session).toBeDefined();
|
||||
});
|
||||
|
||||
it("test_add_from_example_json", () => {
|
||||
const { agent, session, node } = createTestNode();
|
||||
|
||||
const group = node.createGroup();
|
||||
const sessionContent =
|
||||
group.core.verified.newContentSince(undefined)?.[0]?.new[session];
|
||||
assert(sessionContent);
|
||||
|
||||
let log = wasmCrypto.createSessionLog(
|
||||
group.id,
|
||||
session,
|
||||
agent.currentSignerID(),
|
||||
);
|
||||
|
||||
log.tryAdd(
|
||||
sessionContent.newTransactions,
|
||||
sessionContent.lastSignature,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("test_add_new_transaction", () => {
|
||||
const { agent, session, node } = createTestNode();
|
||||
|
||||
const group = node.createGroup();
|
||||
const sessionContent =
|
||||
group.core.verified.newContentSince(undefined)?.[0]?.new[session];
|
||||
assert(sessionContent);
|
||||
|
||||
let log = wasmCrypto.createSessionLog(
|
||||
group.id,
|
||||
session,
|
||||
agent.currentSignerID(),
|
||||
);
|
||||
|
||||
const changesJson = [
|
||||
{ after: "start", op: "app", value: "co_zMphsnYN6GU8nn2HDY5suvyGufY" },
|
||||
];
|
||||
const key = group.getCurrentReadKey();
|
||||
assert(key);
|
||||
assert(key.secret);
|
||||
|
||||
const { signature, transaction } = log.addNewPrivateTransaction(
|
||||
agent,
|
||||
changesJson,
|
||||
key.id,
|
||||
key.secret,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(signature).toMatch(/^signature_z[a-zA-Z0-9]+$/);
|
||||
expect(transaction).toEqual({
|
||||
encryptedChanges: expect.stringMatching(/^encrypted_U/),
|
||||
keyUsed: expect.stringMatching(/^key_z/),
|
||||
madeAt: 0,
|
||||
privacy: "private",
|
||||
});
|
||||
|
||||
const decrypted = log.decryptNextTransactionChangesJson(0, key.secret);
|
||||
|
||||
expect(decrypted).toEqual(
|
||||
'[{"after":"start","op":"app","value":"co_zMphsnYN6GU8nn2HDY5suvyGufY"}]',
|
||||
);
|
||||
});
|
||||
|
||||
it("test_decrypt + clone", () => {
|
||||
const [agent] = agentAndSessionIDFromSecret(agentSecret);
|
||||
const fixtures = {
|
||||
id: "co_zWwrEiushQLvbkWd6Z3L8WxTU1r",
|
||||
signature:
|
||||
"signature_z3ktW7wxMnW7VYExCGZv4Ug2UJSW3ag6zLDiP8GpZThzif6veJt7JipYpUgshhuGbgHtLcWywWSWysV7hChxFypDt",
|
||||
decrypted:
|
||||
'[{"after":"start","op":"app","value":"co_zMphsnYN6GU8nn2HDY5suvyGufY"}]',
|
||||
key: {
|
||||
secret: "keySecret_z3dU66SsyQkkGKpNCJW6NX74MnfVGHUyY7r85b4M8X88L",
|
||||
id: "key_z5XUAHyoqUV9zXWvMK",
|
||||
},
|
||||
transaction: {
|
||||
privacy: "private",
|
||||
madeAt: 0,
|
||||
encryptedChanges:
|
||||
"encrypted_UNAxqdUSGRZ2rzuLU99AFPKCe2C0HwsTzMWQreXZqLr6RpWrSMa-5lwgwIev7xPHTgZFq5UyUgMFrO9zlHJHJGgjJcDzFihY=" as any,
|
||||
keyUsed: "key_z5XUAHyoqUV9zXWvMK",
|
||||
},
|
||||
session:
|
||||
"sealer_z5yhsCCe2XwLTZC4254mUoMASshm3Diq49JrefPpjTktp/signer_z7gVGDpNz9qUtsRxAkHMuu4DYdtVVCG4XELTKPYdoYLPr_session_z9mDP8FoonSA",
|
||||
} as const;
|
||||
|
||||
let log = wasmCrypto.createSessionLog(
|
||||
fixtures.id,
|
||||
fixtures.session,
|
||||
agent.currentSignerID(),
|
||||
);
|
||||
|
||||
log.tryAdd([fixtures.transaction], fixtures.signature, true);
|
||||
|
||||
const decrypted = log
|
||||
.clone()
|
||||
.decryptNextTransactionChangesJson(0, fixtures.key.secret);
|
||||
|
||||
expect(decrypted).toEqual(fixtures.decrypted);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type AgentSecret,
|
||||
type CoID,
|
||||
type CoValueCore,
|
||||
CryptoProvider,
|
||||
type RawAccount,
|
||||
type RawCoValue,
|
||||
StorageAPI,
|
||||
@@ -23,8 +24,15 @@ import type { Peer, SyncMessage } from "../sync.js";
|
||||
import { expectGroup } from "../typeUtils/expectGroup.js";
|
||||
import { toSimplifiedMessages } from "./messagesTestUtils.js";
|
||||
import { createAsyncStorage, createSyncStorage } from "./testStorage.js";
|
||||
import { PureJSCrypto } from "../crypto/PureJSCrypto.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
let Crypto = await WasmCrypto.create();
|
||||
|
||||
export function setCurrentTestCryptoProvider(
|
||||
crypto: WasmCrypto | PureJSCrypto,
|
||||
) {
|
||||
Crypto = crypto;
|
||||
}
|
||||
|
||||
const syncServer: {
|
||||
current: undefined | LocalNode;
|
||||
|
||||
85
packages/jazz-tools/src/tools/tests/patterns/quest.test.ts
Normal file
85
packages/jazz-tools/src/tools/tests/patterns/quest.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { assert, beforeEach, describe, expect, test } from "vitest";
|
||||
import { co, z } from "../../exports.js";
|
||||
import { createJazzTestAccount, setupJazzTestSync } from "../../testing.js";
|
||||
|
||||
// Define the difficulty levels enum
|
||||
const difficultyLevels = ["easy", "medium", "hard"] as const;
|
||||
|
||||
// Define the Quest schema with migration
|
||||
const QuestSchema = co
|
||||
.map({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
imageUrl: z.string(),
|
||||
twigs: z.number(),
|
||||
difficulty: z.enum(difficultyLevels),
|
||||
category: z.string(),
|
||||
categories: z.array(z.string()),
|
||||
completed: z.boolean(),
|
||||
})
|
||||
.withMigration((quest) => {
|
||||
if (quest.categories === undefined) {
|
||||
quest.categories = [quest.category];
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupJazzTestSync();
|
||||
|
||||
await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
creationProps: { name: "Quest Tester" },
|
||||
});
|
||||
});
|
||||
|
||||
describe("QuestSchema", () => {
|
||||
test("should fill categories array with category when categories is undefined", async () => {
|
||||
// Create a quest without categories
|
||||
// @ts-expect-error - (simulating old data)
|
||||
const quest = QuestSchema.create({
|
||||
title: "Test Quest",
|
||||
description: "A test quest description",
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
twigs: 100,
|
||||
difficulty: "medium",
|
||||
category: "adventure",
|
||||
completed: false,
|
||||
});
|
||||
|
||||
// Initially categories should be undefined
|
||||
expect(quest.categories).toBeUndefined();
|
||||
|
||||
// Load the quest to trigger the migration
|
||||
const loadedQuest = await QuestSchema.load(quest.id);
|
||||
assert(loadedQuest);
|
||||
|
||||
// After loading, the migration should have run and filled categories
|
||||
expect(loadedQuest.categories).toEqual(["adventure"]);
|
||||
expect(loadedQuest.category).toEqual("adventure");
|
||||
});
|
||||
|
||||
test("should preserve existing categories when they are already defined", async () => {
|
||||
// Create a quest with categories already defined
|
||||
const quest = QuestSchema.create({
|
||||
title: "Test Quest",
|
||||
description: "A test quest description",
|
||||
imageUrl: "https://example.com/image.jpg",
|
||||
twigs: 150,
|
||||
difficulty: "hard",
|
||||
category: "combat",
|
||||
categories: ["combat", "boss-fight", "endgame"],
|
||||
completed: false,
|
||||
});
|
||||
|
||||
// Categories should be defined initially
|
||||
expect(quest.categories).toEqual(["combat", "boss-fight", "endgame"]);
|
||||
|
||||
// Load the quest to ensure migration doesn't change existing categories
|
||||
const loadedQuest = await QuestSchema.load(quest.id);
|
||||
assert(loadedQuest);
|
||||
|
||||
// Categories should remain unchanged after migration
|
||||
expect(loadedQuest.categories).toEqual(["combat", "boss-fight", "endgame"]);
|
||||
expect(loadedQuest.category).toEqual("combat");
|
||||
});
|
||||
});
|
||||
461
pnpm-lock.yaml
generated
461
pnpm-lock.yaml
generated
@@ -142,6 +142,27 @@ importers:
|
||||
specifier: catalog:default
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.5)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
|
||||
|
||||
bench:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:*
|
||||
version: link:../packages/cojson
|
||||
cojson-latest:
|
||||
specifier: npm:cojson@latest
|
||||
version: cojson@0.17.5
|
||||
jazz-tools:
|
||||
specifier: workspace:*
|
||||
version: link:../packages/jazz-tools
|
||||
jazz-tools-latest:
|
||||
specifier: npm:jazz-tools@latest
|
||||
version: jazz-tools@0.17.5(fcnzwos7wezy7dy4sqlslwz4mm)
|
||||
|
||||
crates/cojson-core-wasm:
|
||||
devDependencies:
|
||||
wasm-pack:
|
||||
specifier: ^0.13.1
|
||||
version: 0.13.1
|
||||
|
||||
examples/betterauth:
|
||||
dependencies:
|
||||
'@icons-pack/react-simple-icons':
|
||||
@@ -1847,9 +1868,9 @@ importers:
|
||||
'@scure/base':
|
||||
specifier: 1.2.1
|
||||
version: 1.2.1
|
||||
jazz-crypto-rs:
|
||||
specifier: 0.0.7
|
||||
version: 0.0.7
|
||||
cojson-core-wasm:
|
||||
specifier: workspace:*
|
||||
version: link:../../crates/cojson-core-wasm
|
||||
neverthrow:
|
||||
specifier: ^7.0.1
|
||||
version: 7.2.0
|
||||
@@ -7860,6 +7881,9 @@ packages:
|
||||
await-lock@2.2.2:
|
||||
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
|
||||
|
||||
axios@0.26.1:
|
||||
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7994,6 +8018,11 @@ packages:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
binary-install@1.1.2:
|
||||
resolution: {integrity: sha512-ZS2cqFHPZOy4wLxvzqfQvDjCOifn+7uCPqNmYRIBM/03+yllON+4fNnsD0VJdW0p97y+E+dTRNPStWNqMBq+9g==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
binary@0.3.0:
|
||||
resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
|
||||
|
||||
@@ -8216,6 +8245,10 @@ packages:
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
chownr@2.0.0:
|
||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -8296,6 +8329,15 @@ packages:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
|
||||
cojson-storage-indexeddb@0.17.5:
|
||||
resolution: {integrity: sha512-Q0VOvqhPvBD18gf0YMXEjq/XzGg3b7B0zGl8DiqtWbCLtei5fzCyCDxcEUSODjBFEC8hYuJTLnbNBdWSnNR+Ug==}
|
||||
|
||||
cojson-transport-ws@0.17.5:
|
||||
resolution: {integrity: sha512-F8z2EhJwYnLiejxwsfrfk6MmhiBcj/jz6qEH0ME4gFy3SUx0QtXeiZI9BwuL/DufoTHB6lNH1xPivu3OdVJjAw==}
|
||||
|
||||
cojson@0.17.5:
|
||||
resolution: {integrity: sha512-GBWySEsURzFakyIhQHSBTxcNQDDmnYVlP802m0jYbSW5aK6PdT1H53QOIzBSq0sm0eEiN8DDP3u+w1TfVJydcQ==}
|
||||
|
||||
collect-v8-coverage@1.0.2:
|
||||
resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==}
|
||||
|
||||
@@ -9509,6 +9551,15 @@ packages:
|
||||
flow-enums-runtime@0.0.6:
|
||||
resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
fontfaceobserver@2.3.0:
|
||||
resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
|
||||
|
||||
@@ -9562,6 +9613,10 @@ packages:
|
||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
@@ -10255,6 +10310,53 @@ packages:
|
||||
jazz-crypto-rs@0.0.7:
|
||||
resolution: {integrity: sha512-Pzs8Zu1zgKVURkBVvqqF1B2r78FaSZdwPbmeCWx0Nb3nibzPuB/kSKEO7LJsKnb3P5HO0v+lTIJ53mGC5H1urQ==}
|
||||
|
||||
jazz-tools@0.17.5:
|
||||
resolution: {integrity: sha512-3Kvad7APpnMpYDTX6zZPcyD9vwDeQoa52g809CZ+F0mC9GQZDWTWaY7jYJd+aJbH2K/TLkIcTLDFX8OF6LnNCQ==}
|
||||
peerDependencies:
|
||||
'@bam.tech/react-native-image-resizer': '*'
|
||||
'@op-engineering/op-sqlite': ^11.4.8
|
||||
'@react-native-community/netinfo': '*'
|
||||
expo-file-system: '*'
|
||||
expo-secure-store: '*'
|
||||
expo-sqlite: 15.2.9
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0
|
||||
react-native: '*'
|
||||
react-native-fast-encoder: ^0.2.0
|
||||
react-native-mmkv: ^3.2.0
|
||||
react-native-nitro-modules: 0.25.2
|
||||
react-native-quick-crypto: 1.0.0-beta.16
|
||||
svelte: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
'@bam.tech/react-native-image-resizer':
|
||||
optional: true
|
||||
'@op-engineering/op-sqlite':
|
||||
optional: true
|
||||
'@react-native-community/netinfo':
|
||||
optional: true
|
||||
expo-file-system:
|
||||
optional: true
|
||||
expo-secure-store:
|
||||
optional: true
|
||||
expo-sqlite:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
react-native-fast-encoder:
|
||||
optional: true
|
||||
react-native-mmkv:
|
||||
optional: true
|
||||
react-native-nitro-modules:
|
||||
optional: true
|
||||
react-native-quick-crypto:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
|
||||
jest-changed-files@29.7.0:
|
||||
resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -11151,10 +11253,22 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@3.3.6:
|
||||
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minipass@5.0.0:
|
||||
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minizlib@2.1.2:
|
||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
minizlib@3.0.1:
|
||||
resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -13427,6 +13541,10 @@ packages:
|
||||
tar-stream@3.1.7:
|
||||
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
||||
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
tar@7.4.3:
|
||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -14195,6 +14313,10 @@ packages:
|
||||
warn-once@0.1.1:
|
||||
resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==}
|
||||
|
||||
wasm-pack@0.13.1:
|
||||
resolution: {integrity: sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==}
|
||||
hasBin: true
|
||||
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
|
||||
@@ -16527,6 +16649,12 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
'@bam.tech/react-native-image-resizer@3.0.11(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
'@base-ui-components/react@1.0.0-beta.1(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
@@ -16963,6 +17091,15 @@ snapshots:
|
||||
- react
|
||||
- react-native
|
||||
|
||||
'@craftzdog/react-native-buffer@6.0.5(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
ieee754: 1.2.1
|
||||
react-native-quick-base64: 2.1.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
- react-native
|
||||
optional: true
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
@@ -17631,6 +17768,13 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
'@expo/vector-icons@14.1.0(expo-font@13.3.3-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
expo-font: 13.3.3-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
'@expo/ws-tunnel@1.0.6': {}
|
||||
|
||||
'@expo/xcpretty@4.3.2':
|
||||
@@ -18541,6 +18685,12 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
'@op-engineering/op-sqlite@11.4.8(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
'@op-engineering/op-sqlite@14.1.0(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -19536,6 +19686,11 @@ snapshots:
|
||||
dependencies:
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
'@react-native-community/netinfo@11.4.1(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))':
|
||||
dependencies:
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
'@react-native/assets-registry@0.80.0': {}
|
||||
|
||||
'@react-native/babel-plugin-codegen@0.80.0(@babel/core@7.27.1)':
|
||||
@@ -19903,6 +20058,16 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@react-native/virtualized-lists@0.80.0(@types/react@19.1.0)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
invariant: 2.2.4
|
||||
nullthrows: 1.1.1
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
optional: true
|
||||
|
||||
'@react-navigation/core@7.12.1(react@19.1.0)':
|
||||
dependencies:
|
||||
'@react-navigation/routers': 7.4.1
|
||||
@@ -22201,6 +22366,12 @@ snapshots:
|
||||
|
||||
await-lock@2.2.2: {}
|
||||
|
||||
axios@0.26.1:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
b4a@1.6.7: {}
|
||||
@@ -22513,6 +22684,14 @@ snapshots:
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
binary-install@1.1.2:
|
||||
dependencies:
|
||||
axios: 0.26.1
|
||||
rimraf: 3.0.2
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
binary@0.3.0:
|
||||
dependencies:
|
||||
buffers: 0.1.1
|
||||
@@ -22757,6 +22936,8 @@ snapshots:
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
chownr@2.0.0: {}
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
chrome-launcher@0.15.2:
|
||||
@@ -22839,6 +23020,26 @@ snapshots:
|
||||
co@4.6.0:
|
||||
optional: true
|
||||
|
||||
cojson-storage-indexeddb@0.17.5:
|
||||
dependencies:
|
||||
cojson: 0.17.5
|
||||
|
||||
cojson-transport-ws@0.17.5:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
cojson: 0.17.5
|
||||
|
||||
cojson@0.17.5:
|
||||
dependencies:
|
||||
'@noble/ciphers': 1.3.0
|
||||
'@noble/curves': 1.9.1
|
||||
'@noble/hashes': 1.8.0
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@scure/base': 1.2.1
|
||||
jazz-crypto-rs: 0.0.7
|
||||
neverthrow: 7.2.0
|
||||
unicode-segmenter: 0.12.0
|
||||
|
||||
collect-v8-coverage@1.0.2:
|
||||
optional: true
|
||||
|
||||
@@ -23920,6 +24121,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
expo-asset@11.2.0-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@expo/image-utils': 0.7.6-canary-20250701-6a945c5
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
expo-constants: 17.1.7-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
expo-auth-session@6.0.1(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo-application: 6.0.2(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))
|
||||
@@ -23976,6 +24188,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
expo-constants@17.1.7-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
'@expo/config': 11.0.12-canary-20250701-6a945c5
|
||||
'@expo/env': 1.0.7-canary-20250701-6a945c5
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
expo-crypto@14.0.2(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
@@ -23991,6 +24213,12 @@ snapshots:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
expo-file-system@18.1.10(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
expo-file-system@18.2.0-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
@@ -24001,6 +24229,12 @@ snapshots:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
expo-file-system@18.2.0-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
expo-font@13.3.3-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
@@ -24013,6 +24247,13 @@ snapshots:
|
||||
fontfaceobserver: 2.3.0
|
||||
react: 19.1.0
|
||||
|
||||
expo-font@13.3.3-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
fontfaceobserver: 2.3.0
|
||||
react: 19.1.0
|
||||
optional: true
|
||||
|
||||
expo-keep-awake@14.1.5-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
@@ -24023,6 +24264,12 @@ snapshots:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
|
||||
expo-keep-awake@14.1.5-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
optional: true
|
||||
|
||||
expo-linking@7.0.5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo-constants: 17.0.8(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
|
||||
@@ -24065,6 +24312,11 @@ snapshots:
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
|
||||
expo-secure-store@14.2.3(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
expo-sqlite@15.2.10(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
await-lock: 2.2.2
|
||||
@@ -24078,6 +24330,13 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
expo-sqlite@15.2.9(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
expo-web-browser@14.0.2(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
expo: 54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
|
||||
@@ -24152,6 +24411,39 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
'@expo/cli': 1.0.0-canary-20250701-6a945c5(graphql@16.11.0)
|
||||
'@expo/config': 11.0.12-canary-20250701-6a945c5
|
||||
'@expo/config-plugins': 10.2.0-canary-20250701-6a945c5
|
||||
'@expo/fingerprint': 0.13.4-canary-20250701-6a945c5
|
||||
'@expo/metro-config': 0.21.0-canary-20250701-6a945c5
|
||||
'@expo/vector-icons': 14.1.0(expo-font@13.3.3-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
babel-preset-expo: 13.3.0-canary-20250701-6a945c5(@babel/core@7.28.0)
|
||||
expo-asset: 11.2.0-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
expo-constants: 17.1.7-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
|
||||
expo-file-system: 18.2.0-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
|
||||
expo-font: 13.3.3-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0)
|
||||
expo-keep-awake: 14.1.5-canary-20250701-6a945c5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react@19.1.0)
|
||||
expo-modules-autolinking: 2.1.14-canary-20250701-6a945c5
|
||||
expo-modules-core: 2.5.0-canary-20250701-6a945c5
|
||||
metro-runtime: 0.82.3
|
||||
pretty-format: 29.7.0
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
react-native-edge-to-edge: 1.6.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
react-refresh: 0.17.0
|
||||
whatwg-url-without-unicode: 8.0.0-3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-react-compiler
|
||||
- bufferutil
|
||||
- graphql
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
exponential-backoff@3.1.1: {}
|
||||
|
||||
exsolve@1.0.5: {}
|
||||
@@ -24320,6 +24612,8 @@ snapshots:
|
||||
|
||||
flow-enums-runtime@0.0.6: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
fontfaceobserver@2.3.0: {}
|
||||
|
||||
for-each@0.3.3:
|
||||
@@ -24378,6 +24672,10 @@ snapshots:
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
@@ -25075,6 +25373,44 @@ snapshots:
|
||||
|
||||
jazz-crypto-rs@0.0.7: {}
|
||||
|
||||
jazz-tools@0.17.5(fcnzwos7wezy7dy4sqlslwz4mm):
|
||||
dependencies:
|
||||
'@manuscripts/prosemirror-recreate-steps': 0.1.4
|
||||
'@scure/base': 1.2.1
|
||||
'@scure/bip39': 1.5.0
|
||||
'@tiptap/core': 2.12.0(@tiptap/pm@2.12.0)
|
||||
clsx: 2.1.1
|
||||
cojson: 0.17.5
|
||||
cojson-storage-indexeddb: 0.17.5
|
||||
cojson-transport-ws: 0.17.5
|
||||
fast-myers-diff: 3.2.0
|
||||
goober: 2.1.16(csstype@3.1.3)
|
||||
prosemirror-example-setup: 1.2.3
|
||||
prosemirror-menu: 1.2.5
|
||||
prosemirror-model: 1.25.1
|
||||
prosemirror-schema-basic: 1.2.4
|
||||
prosemirror-state: 1.4.3
|
||||
prosemirror-transform: 1.10.4
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
'@bam.tech/react-native-image-resizer': 3.0.11(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
'@op-engineering/op-sqlite': 11.4.8(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
'@react-native-community/netinfo': 11.4.1(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
|
||||
expo-file-system: 18.1.10(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
|
||||
expo-secure-store: 14.2.3(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))
|
||||
expo-sqlite: 15.2.9(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.28.0)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
react-native-fast-encoder: 0.2.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
react-native-mmkv: 3.3.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
react-native-nitro-modules: 0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
react-native-quick-crypto: 1.0.0-beta.16(react-native-nitro-modules@0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
svelte: 5.34.6
|
||||
transitivePeerDependencies:
|
||||
- '@tiptap/pm'
|
||||
- csstype
|
||||
|
||||
jest-changed-files@29.7.0:
|
||||
dependencies:
|
||||
execa: 5.1.1
|
||||
@@ -26206,8 +26542,19 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@3.3.6:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
minipass@5.0.0: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@2.1.2:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
yallist: 4.0.0
|
||||
|
||||
minizlib@3.0.1:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
@@ -27450,6 +27797,12 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
react-native-edge-to-edge@1.6.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
react-native-fast-encoder@0.2.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
big-integer: 1.6.52
|
||||
@@ -27457,6 +27810,14 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
react-native-fast-encoder@0.2.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
big-integer: 1.6.52
|
||||
flatbuffers: 2.0.6
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
react-native-get-random-values@1.11.0(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)):
|
||||
dependencies:
|
||||
fast-base64-decode: 1.0.0
|
||||
@@ -27482,17 +27843,36 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
react-native-mmkv@3.3.0(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
react-native-nitro-modules@0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
react-native-nitro-modules@0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
react-native-quick-base64@2.1.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
|
||||
|
||||
react-native-quick-base64@2.1.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
optional: true
|
||||
|
||||
react-native-quick-crypto@1.0.0-beta.16(react-native-nitro-modules@0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@craftzdog/react-native-buffer': 6.0.5(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
@@ -27504,6 +27884,18 @@ snapshots:
|
||||
readable-stream: 4.5.2
|
||||
util: 0.12.5
|
||||
|
||||
react-native-quick-crypto@1.0.0-beta.16(react-native-nitro-modules@0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@craftzdog/react-native-buffer': 6.0.5(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
events: 3.3.0
|
||||
react: 19.1.0
|
||||
react-native: 0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
|
||||
react-native-nitro-modules: 0.25.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
react-native-quick-base64: 2.1.2(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
readable-stream: 4.5.2
|
||||
util: 0.12.5
|
||||
optional: true
|
||||
|
||||
react-native-safe-area-context@5.5.0(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -27663,6 +28055,54 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0):
|
||||
dependencies:
|
||||
'@jest/create-cache-key-function': 29.7.0
|
||||
'@react-native/assets-registry': 0.80.0
|
||||
'@react-native/codegen': 0.80.0(@babel/core@7.28.0)
|
||||
'@react-native/community-cli-plugin': 0.80.0(@react-native-community/cli@19.0.0(typescript@5.8.3))
|
||||
'@react-native/gradle-plugin': 0.80.0
|
||||
'@react-native/js-polyfills': 0.80.0
|
||||
'@react-native/normalize-colors': 0.80.0
|
||||
'@react-native/virtualized-lists': 0.80.0(@types/react@19.1.0)(react-native@0.80.0(@babel/core@7.28.0)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
|
||||
abort-controller: 3.0.0
|
||||
anser: 1.4.10
|
||||
ansi-regex: 5.0.1
|
||||
babel-jest: 29.7.0(@babel/core@7.28.0)
|
||||
babel-plugin-syntax-hermes-parser: 0.28.1
|
||||
base64-js: 1.5.1
|
||||
chalk: 4.1.2
|
||||
commander: 12.1.0
|
||||
flow-enums-runtime: 0.0.6
|
||||
glob: 7.2.3
|
||||
invariant: 2.2.4
|
||||
jest-environment-node: 29.7.0
|
||||
memoize-one: 5.2.1
|
||||
metro-runtime: 0.82.3
|
||||
metro-source-map: 0.82.3
|
||||
nullthrows: 1.1.1
|
||||
pretty-format: 29.7.0
|
||||
promise: 8.3.0
|
||||
react: 19.1.0
|
||||
react-devtools-core: 6.1.1
|
||||
react-refresh: 0.14.2
|
||||
regenerator-runtime: 0.13.11
|
||||
scheduler: 0.26.0
|
||||
semver: 7.7.2
|
||||
stacktrace-parser: 0.1.10
|
||||
whatwg-fetch: 3.6.20
|
||||
ws: 6.2.3
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@react-native-community/cli'
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-refresh@0.17.0: {}
|
||||
@@ -28943,6 +29383,15 @@ snapshots:
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.21.1
|
||||
|
||||
tar@6.2.1:
|
||||
dependencies:
|
||||
chownr: 2.0.0
|
||||
fs-minipass: 2.1.0
|
||||
minipass: 5.0.0
|
||||
minizlib: 2.1.2
|
||||
mkdirp: 1.0.4
|
||||
yallist: 4.0.0
|
||||
|
||||
tar@7.4.3:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
@@ -29863,6 +30312,12 @@ snapshots:
|
||||
|
||||
warn-once@0.1.1: {}
|
||||
|
||||
wasm-pack@0.13.1:
|
||||
dependencies:
|
||||
binary-install: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
wcwidth@1.0.1:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "examples/*"
|
||||
- "starters/*"
|
||||
- "tests/*"
|
||||
- 'packages/*'
|
||||
- 'examples/*'
|
||||
- 'starters/*'
|
||||
- 'tests/*'
|
||||
- 'crates/*'
|
||||
- 'bench'
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
|
||||
@@ -5,6 +5,7 @@ export default defineConfig({
|
||||
root: "./",
|
||||
test: {
|
||||
projects: [
|
||||
"bench",
|
||||
"packages/*",
|
||||
"tests/browser-integration",
|
||||
"tests/cloudflare-workers",
|
||||
|
||||
Reference in New Issue
Block a user