Compare commits
48 Commits
feat/quick
...
fix/loggin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d20c81ed5 | ||
|
|
3accbc06e2 | ||
|
|
39ae4fc8c1 | ||
|
|
ae60eb0d01 | ||
|
|
05eb330173 | ||
|
|
5d1e192245 | ||
|
|
c5e059b3a9 | ||
|
|
d1785c0178 | ||
|
|
9b95c32edb | ||
|
|
d87781d33f | ||
|
|
ad50ec2d9d | ||
|
|
fef055b9fb | ||
|
|
cdc7f9f841 | ||
|
|
ff6940de56 | ||
|
|
cad54e4018 | ||
|
|
c657880a22 | ||
|
|
79b02dee3c | ||
|
|
ee29cb300c | ||
|
|
36f0ab7571 | ||
|
|
829be3cafa | ||
|
|
7c322796aa | ||
|
|
36d50d96dd | ||
|
|
1fd26b9d05 | ||
|
|
40f161d4d0 | ||
|
|
655a601a8e | ||
|
|
c7a28e5003 | ||
|
|
ae429d0a1c | ||
|
|
32834ef9e3 | ||
|
|
13be2a3235 | ||
|
|
7fa6e4333d | ||
|
|
77dbd4e205 | ||
|
|
325250272b | ||
|
|
219ba975a5 | ||
|
|
f5a3394d40 | ||
|
|
4a5209237f | ||
|
|
d7eb50abc1 | ||
|
|
b097c38617 | ||
|
|
385dfc89ff | ||
|
|
d89e4c3412 | ||
|
|
bbeee086ce | ||
|
|
718e9418e2 | ||
|
|
ac216b9f2e | ||
|
|
93230df2cb | ||
|
|
c13daa140b | ||
|
|
3c9439d0cc | ||
|
|
d38d4928c7 | ||
|
|
596901cba6 | ||
|
|
2a1fda3758 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Remove @opentelemetry/api as a peer dependency and add it as a dependency
|
||||
5
.changeset/thin-bulldogs-march.md
Normal file
5
.changeset/thin-bulldogs-march.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Add an internal API to disable the permission errors logs
|
||||
6
.github/workflows/playwright.yml
vendored
6
.github/workflows/playwright.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
project: ["tests/e2e", "examples/chat", "examples/file-share-svelte", "examples/music-player", "examples/pets", "examples/onboarding"]
|
||||
project: ["tests/e2e", "examples/chat", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "examples/onboarding"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -45,10 +45,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup .env
|
||||
run: echo "VITE_WS_PEER=ws://localhost:4200/" >> .env
|
||||
working-directory: ./${{ matrix.project }}
|
||||
|
||||
- name: Pnpm Build
|
||||
run: pnpm turbo build
|
||||
working-directory: ./${{ matrix.project }}
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -4,6 +4,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
type: boolean
|
||||
description: "Run tmate session for debugging"
|
||||
required: false
|
||||
default: false
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -48,4 +55,11 @@ jobs:
|
||||
publish: pnpm release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# Enable tmate debugging only if the workflow is manually triggered, debug_enabled is true, and the workflow failed
|
||||
- name: Setup tmate session for debugging
|
||||
if: ${{ failure() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
with:
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-book-shelf
|
||||
|
||||
## 0.1.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.1.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-example-book-shelf",
|
||||
"version": "0.1.32",
|
||||
"version": "0.1.33",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -11,9 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.8.40",
|
||||
"jazz-react": "workspace:0.8.40",
|
||||
"jazz-tools": "workspace:0.8.39",
|
||||
"jazz-browser-media-images": "workspace:0.8.41",
|
||||
"jazz-react": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:0.8.41",
|
||||
"next": "14.2.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cdc7f9f: Fixing react-native examples
|
||||
- Updated dependencies [cdc7f9f]
|
||||
- jazz-react-native-auth-clerk@0.8.43
|
||||
|
||||
## 1.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.8.41
|
||||
- jazz-react-native-auth-clerk@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-react-native-media-images@0.8.41
|
||||
|
||||
## 1.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "../global.css";
|
||||
import { ClerkLoaded, ClerkProvider } from "@clerk/clerk-expo";
|
||||
import { useFonts } from "expo-font";
|
||||
import { Slot } from "expo-router";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ["babel-preset-expo"],
|
||||
plugins: ["nativewind/babel"],
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
"nativewind/babel",
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
3
examples/chat-rn-clerk/global.css
Normal file
3
examples/chat-rn-clerk/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,5 +1,6 @@
|
||||
// Learn more https://docs.expo.dev/guides/monorepos
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
const { FileStore } = require("metro-cache");
|
||||
const path = require("path");
|
||||
|
||||
@@ -31,4 +32,5 @@ config.cacheStores = [
|
||||
}),
|
||||
];
|
||||
|
||||
module.exports = config;
|
||||
// module.exports = config;
|
||||
module.exports = withNativeWind(config, { input: "./global.css" });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.31",
|
||||
"version": "1.0.33",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
@@ -45,7 +45,7 @@
|
||||
"jazz-react-native-auth-clerk": "workspace:*",
|
||||
"jazz-react-native-media-images": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"nativewind": "^2.0.11",
|
||||
"nativewind": "^4.1.21",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-native": "~0.76.3",
|
||||
@@ -70,7 +70,7 @@
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~52.0.2",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"private": true
|
||||
|
||||
14
examples/chat-rn-clerk/tailwind.config.js
Normal file
14
examples/chat-rn-clerk/tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// NOTE: Update this to include the paths to all of your component files.
|
||||
content: [
|
||||
"./app/**/*.{js,jsx,ts,tsx}",
|
||||
"./components/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
presets: [require("nativewind/preset")],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./app/**/*.{js,jsx,ts,tsx}",
|
||||
"./components/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -7,5 +7,5 @@
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
|
||||
## 1.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.29",
|
||||
"version": "1.0.30",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ac216b9]
|
||||
- jazz-browser@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-vue@0.8.41
|
||||
|
||||
## 0.0.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.23",
|
||||
"version": "0.0.24",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.119
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3252502]
|
||||
- Updated dependencies [6370348]
|
||||
- Updated dependencies [ac216b9]
|
||||
- cojson@0.8.41
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.118",
|
||||
"version": "0.0.119",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -18,11 +18,11 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.39",
|
||||
"cojson": "workspace:0.8.41",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"jazz-browser-media-images": "workspace:0.8.40",
|
||||
"jazz-react": "workspace:0.8.40",
|
||||
"jazz-tools": "workspace:0.8.39",
|
||||
"jazz-browser-media-images": "workspace:0.8.41",
|
||||
"jazz-react": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:0.8.41",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-react-auth-clerk@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.4.1",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-clerk": "workspace:0.8.40",
|
||||
"jazz-react-auth-clerk": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-svelte@0.8.41
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
3
examples/form/.gitignore
vendored
3
examples/form/.gitignore
vendored
@@ -22,3 +22,6 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# form
|
||||
|
||||
## 0.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.14",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -21,11 +21,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"is-ci": "^3.0.1",
|
||||
"globals": "^15.11.0",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.9",
|
||||
|
||||
46
examples/form/playwright.config.ts
Normal file
46
examples/form/playwright.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import isCI from "is-ci";
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: isCI,
|
||||
/* Retry on CI only */
|
||||
retries: isCI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: isCI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://localhost:5173/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: "pnpm preview --port 5173",
|
||||
url: "http://localhost:5173/",
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export function Orders() {
|
||||
<h1 className="text-lg pb-2 border-b mb-3">
|
||||
<strong>Your orders 🧋</strong>
|
||||
</h1>
|
||||
|
||||
{me?.profile?.orders?.length ? (
|
||||
me?.profile?.orders.map((order) =>
|
||||
order ? <OrderThumbnail key={order.id} order={order} /> : null,
|
||||
|
||||
56
examples/form/tests/form.spec.ts
Normal file
56
examples/form/tests/form.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
|
||||
test("create and edit an order", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.fillUsername("Alice");
|
||||
await loginPage.signup();
|
||||
|
||||
// start an order
|
||||
await page.getByRole("link", { name: "Add new order" }).click();
|
||||
await page.getByLabel("Base tea").selectOption("Oolong");
|
||||
|
||||
// test draft indicator
|
||||
await page.getByRole("link", { name: /Back to all orders/ }).click();
|
||||
await expect(page.getByText("You have a draft")).toBeVisible();
|
||||
|
||||
// fill out the rest of order form
|
||||
await page.getByRole("link", { name: "Add new order" }).click();
|
||||
await page.getByLabel("Pearl").check();
|
||||
await page.getByLabel("Taro").check();
|
||||
await page.getByLabel("Delivery date").fill("2024-12-21");
|
||||
await page.getByLabel("With milk?").check();
|
||||
await page.getByLabel("Special instructions").fill("25% sugar");
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
|
||||
await page.waitForURL("/");
|
||||
|
||||
// the draft indicator should be gone because the order was submitted
|
||||
await expect(page.getByText("You have a draft")).toHaveCount(0);
|
||||
|
||||
// check if order was created correctly
|
||||
const firstOrder = page.getByRole("link", { name: "Oolong milk tea" });
|
||||
await expect(firstOrder).toHaveText(/25% sugar/);
|
||||
await expect(firstOrder).toHaveText(/12\/21\/2024/);
|
||||
await expect(firstOrder).toHaveText(/with pearl, taro/);
|
||||
|
||||
// edit order
|
||||
await firstOrder.click();
|
||||
await page.getByLabel("Base tea").selectOption("Jasmine");
|
||||
await page.getByLabel("Red bean").check();
|
||||
await page.getByLabel("Brown sugar").check();
|
||||
await page.getByLabel("Delivery date").fill("2024-12-25");
|
||||
await page.getByLabel("With milk?").uncheck();
|
||||
await page.getByLabel("Special instructions").fill("10% sugar");
|
||||
await page.getByRole("link", { name: /Back to all orders/ }).click();
|
||||
|
||||
// check if order was edited correctly
|
||||
const editedOrder = page.getByRole("link", { name: "Jasmine tea" });
|
||||
await expect(editedOrder).toHaveText(/10% sugar/);
|
||||
await expect(editedOrder).toHaveText(/12\/25\/2024/);
|
||||
await expect(editedOrder).toHaveText(
|
||||
/with pearl, taro, red bean, brown sugar/,
|
||||
);
|
||||
});
|
||||
40
examples/form/tests/pages/LoginPage.ts
Normal file
40
examples/form/tests/pages/LoginPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly usernameInput: Locator;
|
||||
readonly signupButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.usernameInput = page.getByRole("textbox");
|
||||
this.signupButton = page.getByRole("button", {
|
||||
name: "Sign up",
|
||||
});
|
||||
}
|
||||
|
||||
async goto() {
|
||||
this.page.goto("/");
|
||||
}
|
||||
|
||||
async fillUsername(value: string) {
|
||||
await this.usernameInput.clear();
|
||||
await this.usernameInput.fill(value);
|
||||
}
|
||||
|
||||
async loginAs(value: string) {
|
||||
await this.page
|
||||
.getByRole("button", {
|
||||
name: value,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async signup() {
|
||||
await this.signupButton.click();
|
||||
}
|
||||
|
||||
async expectLoaded() {
|
||||
await expect(this.signupButton).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.16",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3252502]
|
||||
- Updated dependencies [6370348]
|
||||
- Updated dependencies [ac216b9]
|
||||
- cojson@0.8.41
|
||||
- cojson-transport-ws@0.8.41
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"private": true,
|
||||
"version": "0.0.86",
|
||||
"version": "0.0.87",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.39",
|
||||
"cojson-transport-ws": "workspace:0.8.39",
|
||||
"cojson": "workspace:0.8.41",
|
||||
"cojson-transport-ws": "workspace:0.8.41",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.38",
|
||||
"version": "0.0.39",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -18,8 +18,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.8.40",
|
||||
"jazz-tools": "workspace:0.8.39",
|
||||
"jazz-react": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:0.8.41",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-onboarding
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.0.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-onboarding",
|
||||
"private": true,
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.20",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.8.41
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.17",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.37",
|
||||
"version": "0.0.38",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.8.40",
|
||||
"jazz-tools": "workspace:0.8.39",
|
||||
"jazz-react": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:0.8.41",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.136
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.0.135
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.135",
|
||||
"version": "0.0.136",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,9 +19,9 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.8.40",
|
||||
"jazz-react": "workspace:0.8.40",
|
||||
"jazz-tools": "workspace:0.8.39",
|
||||
"jazz-browser-media-images": "workspace:0.8.41",
|
||||
"jazz-react": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:0.8.41",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.8.40",
|
||||
"jazz-run": "workspace:0.8.41",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-browser-media-images@0.8.41
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.16",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ac216b9]
|
||||
- jazz-browser@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
- jazz-vue@0.8.41
|
||||
|
||||
## 0.0.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.135
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.41
|
||||
- jazz-tools@0.8.41
|
||||
|
||||
## 0.0.134
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.134",
|
||||
"version": "0.0.135",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.8.40",
|
||||
"jazz-tools": "workspace:0.8.39",
|
||||
"jazz-react": "workspace:0.8.41",
|
||||
"jazz-tools": "workspace:0.8.41",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { clsx } from "clsx";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { forwardRef } from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -9,7 +9,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
href?: string;
|
||||
newTab?: boolean;
|
||||
icon?: LucideIcon;
|
||||
icon?: string;
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -17,14 +17,16 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function ButtonIcon({ icon: Icon, loading }: ButtonProps) {
|
||||
function ButtonIcon({ icon, loading }: ButtonProps) {
|
||||
if (!Icon) return null;
|
||||
|
||||
const className = "size-5";
|
||||
|
||||
if (loading) return <Spinner className={className} />;
|
||||
|
||||
return <Icon strokeWidth={1.5} className={className} />;
|
||||
if (icon) {
|
||||
return <Icon name={icon} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
||||
138
homepage/design-system/src/app/components/atoms/Icon.tsx
Normal file
138
homepage/design-system/src/app/components/atoms/Icon.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowRightIcon,
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CodeIcon,
|
||||
CopyIcon,
|
||||
FileLock2Icon,
|
||||
FileTextIcon,
|
||||
FingerprintIcon,
|
||||
FolderArchiveIcon,
|
||||
GaugeIcon,
|
||||
GlobeIcon,
|
||||
ImageIcon,
|
||||
LinkIcon,
|
||||
LockKeyholeIcon,
|
||||
LucideIcon,
|
||||
MailIcon,
|
||||
MenuIcon,
|
||||
MessageCircleQuestionIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
MoonIcon,
|
||||
MousePointerSquareDashedIcon,
|
||||
PencilLineIcon,
|
||||
ScanFace,
|
||||
SunIcon,
|
||||
TrashIcon,
|
||||
UploadCloudIcon,
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
UsersIcon,
|
||||
WifiOffIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
const icons = {
|
||||
addUser: UserPlusIcon,
|
||||
arrowDown: ArrowDownIcon,
|
||||
arrowRight: ArrowRightIcon,
|
||||
auth: UserIcon,
|
||||
browser: GlobeIcon,
|
||||
check: CheckIcon,
|
||||
chevronRight: ChevronRight,
|
||||
chevronDown: ChevronDown,
|
||||
close: XIcon,
|
||||
code: CodeIcon,
|
||||
copy: CopyIcon,
|
||||
darkTheme: MoonIcon,
|
||||
delete: TrashIcon,
|
||||
devices: MonitorSmartphoneIcon,
|
||||
docs: BookTextIcon,
|
||||
encryption: LockKeyholeIcon,
|
||||
faceId: ScanFace,
|
||||
file: FileTextIcon,
|
||||
help: MessageCircleQuestionIcon,
|
||||
image: ImageIcon,
|
||||
instant: GaugeIcon,
|
||||
lightTheme: SunIcon,
|
||||
link: LinkIcon,
|
||||
menu: MenuIcon,
|
||||
newsletter: MailIcon,
|
||||
offline: WifiOffIcon,
|
||||
package: BoxIcon,
|
||||
permissions: FileLock2Icon,
|
||||
social: UsersIcon,
|
||||
spatialPresence: MousePointerSquareDashedIcon,
|
||||
touchId: FingerprintIcon,
|
||||
upload: UploadCloudIcon,
|
||||
write: PencilLineIcon,
|
||||
zip: FolderArchiveIcon,
|
||||
};
|
||||
|
||||
// copied from tailwind line height https://tailwindcss.com/docs/font-size
|
||||
const sizes = {
|
||||
xs: 16,
|
||||
sm: 20,
|
||||
md: 24,
|
||||
lg: 28,
|
||||
xl: 28,
|
||||
"2xl": 32,
|
||||
"3xl": 36,
|
||||
"4xl": 40,
|
||||
"5xl": 48,
|
||||
"6xl": 60,
|
||||
"7xl": 72,
|
||||
"8xl": 96,
|
||||
"9xl": 128,
|
||||
};
|
||||
|
||||
const strokeWidths = {
|
||||
xs: 2,
|
||||
sm: 2,
|
||||
md: 1.5,
|
||||
lg: 1.5,
|
||||
xl: 1.5,
|
||||
"2xl": 1.25,
|
||||
"3xl": 1.25,
|
||||
"4xl": 1.25,
|
||||
"5xl": 1,
|
||||
"6xl": 1,
|
||||
"7xl": 1,
|
||||
"8xl": 1,
|
||||
"9xl": 1,
|
||||
};
|
||||
|
||||
export function Icon({
|
||||
name,
|
||||
icon,
|
||||
size = "md",
|
||||
className,
|
||||
...svgProps
|
||||
}: {
|
||||
name?: string;
|
||||
icon?: LucideIcon;
|
||||
size?: keyof typeof sizes;
|
||||
className?: string;
|
||||
} & React.SVGProps<SVGSVGElement>) {
|
||||
if (!icon && (!name || !icons.hasOwnProperty(name))) {
|
||||
throw new Error(`Icon not found`);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const IconComponent = icons?.hasOwnProperty(name) ? icons[name] : icon;
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
aria-hidden="true"
|
||||
size={sizes[size]}
|
||||
strokeWidth={strokeWidths[size]}
|
||||
strokeLinecap="butt"
|
||||
className={className}
|
||||
{...svgProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { Clipboard } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
|
||||
// TODO: add tabs feature, and remove CodeExampleTabs
|
||||
|
||||
@@ -44,11 +44,12 @@ function CopyButton({ code, size }: { code: string; size?: "sm" | "md" }) {
|
||||
copied && "-translate-y-1.5 opacity-0",
|
||||
)}
|
||||
>
|
||||
<Clipboard
|
||||
strokeWidth={1}
|
||||
<Icon
|
||||
name="copy"
|
||||
size="xs"
|
||||
className={clsx(
|
||||
size === "sm" ? "h-3 w-3" : "h-4 w-4",
|
||||
"fill-stone-500/20 stroke-stone-500 transition-colors group-hover/button:stroke-stone-600 dark:group-hover/button:stroke-stone-400",
|
||||
size === "sm" ? "size-2" : "size-3",
|
||||
"stroke-stone-500 transition-colors group-hover/button:stroke-stone-600 dark:group-hover/button:stroke-stone-400",
|
||||
)}
|
||||
/>
|
||||
Copy
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import clsx from "clsx";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { Card } from "../atoms/Card";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import { Prose } from "./Prose";
|
||||
|
||||
export function FeatureCard({
|
||||
label,
|
||||
icon: Icon,
|
||||
icon,
|
||||
explanation,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
icon?: LucideIcon;
|
||||
icon?: string;
|
||||
explanation?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className={clsx(className, "p-4")}>
|
||||
{Icon && (
|
||||
{icon && (
|
||||
<Icon
|
||||
className="size-8 text-blue p-1.5 rounded-lg bg-blue-50 dark:text-blue-500 dark:bg-stone-900 mb-2.5 md:size-10"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="butt"
|
||||
size={80}
|
||||
name={icon}
|
||||
className="text-blue p-1.5 rounded-lg bg-blue-50 dark:text-blue-500 dark:bg-stone-900 mb-2.5"
|
||||
size="3xl"
|
||||
/>
|
||||
)}
|
||||
<div className="text-stone-900 font-medium md:text-base dark:text-stone-100 mb-2">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { useId } from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
|
||||
export function Select(
|
||||
props: React.SelectHTMLAttributes<HTMLSelectElement> & { label: string },
|
||||
@@ -32,9 +32,10 @@ export function Select(
|
||||
{props.children}
|
||||
</select>
|
||||
|
||||
<ChevronDownIcon
|
||||
<Icon
|
||||
name="chevronDown"
|
||||
className="absolute right-[0.5em] text-stone-400 dark:text-stone-600"
|
||||
size={16}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { UseThemeProps } from "next-themes/dist/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
|
||||
export function ThemeToggle({
|
||||
className,
|
||||
@@ -31,14 +30,14 @@ export function ThemeToggle({
|
||||
aria-label={mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"}
|
||||
onClick={() => setTheme(otherTheme)}
|
||||
>
|
||||
<MoonIcon
|
||||
size={24}
|
||||
strokeWidth={2}
|
||||
<Icon
|
||||
name="darkTheme"
|
||||
size="lg"
|
||||
className="size-5 stroke-stone-900 dark:hidden"
|
||||
/>
|
||||
<SunIcon
|
||||
size={24}
|
||||
strokeWidth={2}
|
||||
<Icon
|
||||
name="lightTheme"
|
||||
size="lg"
|
||||
className="size-5 hidden stroke-white dark:block"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
PopoverPanel,
|
||||
} from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
@@ -19,12 +18,13 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import { BreadCrumb } from "../molecules/Breadcrumb";
|
||||
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
|
||||
|
||||
type NavItemProps = {
|
||||
href: string;
|
||||
icon?: ReactNode;
|
||||
icon?: string;
|
||||
title: string;
|
||||
firstOnRight?: boolean;
|
||||
newTab?: boolean;
|
||||
@@ -56,7 +56,7 @@ function NavItem({
|
||||
if (item.icon) {
|
||||
return (
|
||||
<NavLinkLogo className="px-3" {...item}>
|
||||
{icon}
|
||||
<Icon name={item.icon} />
|
||||
<span className="sr-only">{title}</span>
|
||||
</NavLinkLogo>
|
||||
);
|
||||
@@ -86,7 +86,7 @@ function NavItem({
|
||||
)}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<ChevronDownIcon aria-hidden="true" className="size-4" />
|
||||
<Icon name="chevronDown" size="xs" />
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel
|
||||
@@ -103,7 +103,13 @@ function NavItem({
|
||||
as={Link}
|
||||
key={href}
|
||||
>
|
||||
{icon}
|
||||
{icon && (
|
||||
<Icon
|
||||
className="stroke-blue dark:stroke-blue-500 shrink-0"
|
||||
size="sm"
|
||||
name={icon}
|
||||
/>
|
||||
)}
|
||||
<div className="grid gap-1.5 mt-px">
|
||||
<p className="text-sm font-medium text-stone-900 dark:text-white">
|
||||
{title}
|
||||
@@ -155,7 +161,7 @@ export function MobileNav({
|
||||
}}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<MenuIcon />
|
||||
<Icon name="menu" />
|
||||
<BreadCrumb items={items} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -223,10 +229,10 @@ export function MobileNav({
|
||||
aria-label="Close menu"
|
||||
>
|
||||
{menuOpen || searchOpen ? (
|
||||
<XIcon />
|
||||
<Icon name="close" />
|
||||
) : (
|
||||
<>
|
||||
<MenuIcon />
|
||||
<Icon name="menu" />
|
||||
<BreadCrumb items={items} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, MailIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ErrorResponse } from "resend";
|
||||
import { subscribe } from "../../../actions/resend";
|
||||
import { Button } from "../atoms/Button";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import { Input } from "../molecules/Input";
|
||||
|
||||
export function NewsletterForm() {
|
||||
@@ -34,7 +34,7 @@ export function NewsletterForm() {
|
||||
if (state === "success") {
|
||||
return (
|
||||
<div className="flex gap-3 items-center">
|
||||
<CheckIcon className="text-green-500" size={16} />
|
||||
<Icon name="check" className="text-green-500" />
|
||||
<p>Thanks for subscribing!</p>
|
||||
</div>
|
||||
);
|
||||
@@ -63,7 +63,7 @@ export function NewsletterForm() {
|
||||
variant="secondary"
|
||||
loadingText="Subscribing..."
|
||||
loading={state === "loading"}
|
||||
icon={MailIcon}
|
||||
icon="newsletter"
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { packages } from "@/lib/packages";
|
||||
import { clsx } from "clsx";
|
||||
import { MessageCircleQuestionIcon, PackageIcon } from "lucide-react";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import Link from "next/link";
|
||||
|
||||
const CardHeading = ({
|
||||
@@ -64,9 +64,8 @@ export default function Page() {
|
||||
key={name}
|
||||
>
|
||||
<Card className="border shadow-sm">
|
||||
<PackageIcon
|
||||
size={25}
|
||||
strokeWidth={1.5}
|
||||
<Icon
|
||||
name="package"
|
||||
className="text-stone-500 dark:text-stone-400"
|
||||
/>
|
||||
<CardHeading className="group-hover:text-blue dark:group-hover:text-blue-600">
|
||||
@@ -78,9 +77,9 @@ export default function Page() {
|
||||
))}
|
||||
|
||||
<Card className="bg-stone-50 dark:bg-stone-925">
|
||||
<MessageCircleQuestionIcon
|
||||
size={25}
|
||||
strokeWidth={1.5}
|
||||
<Icon
|
||||
name="help"
|
||||
size="md"
|
||||
className="text-stone-500 dark:text-stone-400"
|
||||
/>
|
||||
<CardHeading>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
# Sharing data through Organizations
|
||||
|
||||
Organizations are a way to share a set of data between users.
|
||||
Different apps have different names for this concept, such as "teams" or "workspaces".
|
||||
|
||||
We'll use the term Organization.
|
||||
|
||||
## Defining the schema for an Organization
|
||||
|
||||
Create a CoMap shared by the users of the same organization to act as a root (or "main database") for the shared data within an organization.
|
||||
|
||||
For this example, users within an `Organization` will be sharing `Project`s.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// schema.ts
|
||||
export class Project extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
export class ListOfProjects extends CoList.Of(co.ref(Project)) {}
|
||||
|
||||
export class Organization extends CoMap {
|
||||
name = co.string;
|
||||
|
||||
// shared data between users of each organization
|
||||
projects = co.ref(ListOfProjects);
|
||||
}
|
||||
|
||||
export class ListOfOrganizations extends CoList.Of(co.ref(Organization)) {}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Learn more about [defining schemas](/docs/schemas/covalues).
|
||||
|
||||
## Adding a list of Organizations to the user's Account
|
||||
|
||||
Let's add the list of `Organization`s to the user's Account `root` so they can access them.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// schema.ts
|
||||
export class JazzAccountRoot extends CoMap {
|
||||
organizations = co.ref(ListOfOrganizations);
|
||||
}
|
||||
|
||||
export class JazzAccount extends Account {
|
||||
root = co.ref(JazzAccountRoot);
|
||||
|
||||
async migrate(creationProps?: { name: string }) {
|
||||
super.migrate(creationProps);
|
||||
|
||||
if (!this._refs.root) {
|
||||
// Using a Group as an owner allows you to give access to other users
|
||||
const initialOrganizationOwnership = {
|
||||
owner: Group.create({ owner: this }),
|
||||
};
|
||||
|
||||
const organizations = ListOfOrganizations.create(
|
||||
[
|
||||
// Create the first Organization so users can start right away
|
||||
Organization.create(
|
||||
{
|
||||
name: "My organization",
|
||||
projects: ListOfProjects.create([], initialOrganizationOwnership),
|
||||
},
|
||||
initialOrganizationOwnership,
|
||||
),
|
||||
],
|
||||
{ owner: this },
|
||||
);
|
||||
|
||||
this.root = JazzAccountRoot.create(
|
||||
{ organizations },
|
||||
{ owner: this },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
This schema now allows users to create `Organization`s and add `Project`s to them.
|
||||
|
||||
## Adding other users to an Organization
|
||||
|
||||
To give users access to an `Organization`, you can either send them an invite link, or
|
||||
add their `Account` manually.
|
||||
|
||||
### Adding users through invite links
|
||||
|
||||
Here's how you can generate an [invite link](/docs/groups/sharing#invite-links).
|
||||
|
||||
When the user accepts the invite, add the `Organization` to the user's `organizations` list.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
if (me?.root?.organizations) {
|
||||
Organization.load(organizationId, me, []).then((organization) => {
|
||||
if (organization) {
|
||||
// Avoid duplicates
|
||||
const ids = me.root.organizations.map(
|
||||
(organization) => organization?.id,
|
||||
);
|
||||
|
||||
if (ids.includes(organizationId)) return;
|
||||
|
||||
me.root.organizations.push(organization);
|
||||
navigate("/organizations/" + organizationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useAcceptInvite({
|
||||
invitedObjectSchema: Organization,
|
||||
onAccept,
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Adding users through their Account ID
|
||||
|
||||
...more on this coming soon
|
||||
@@ -6,16 +6,6 @@ import { VueLogo } from "@/components/icons/VueLogo";
|
||||
import { H2 } from "gcmp-design-system/src/app/components/atoms/Headings";
|
||||
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
|
||||
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
|
||||
import {
|
||||
CloudUploadIcon,
|
||||
FingerprintIcon,
|
||||
FolderArchiveIcon,
|
||||
Icon,
|
||||
ImageIcon,
|
||||
LockIcon,
|
||||
PencilLineIcon,
|
||||
UserPlusIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
Schema_ts as ImageUploadSchema,
|
||||
@@ -29,6 +19,7 @@ import { ExampleCard } from "@/components/examples/ExampleCard";
|
||||
import { ExampleDemo } from "@/components/examples/ExampleDemo";
|
||||
import { SvelteLogo } from "@/components/icons/SvelteLogo";
|
||||
import { Example, features, tech } from "@/lib/example";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
|
||||
const MockButton = ({ children }: { children: React.ReactNode }) => (
|
||||
<p className="bg-blue-100 text-blue-800 py-1 p-2 rounded-full font-medium text-center text-xs">
|
||||
@@ -81,16 +72,16 @@ const OnboardingIllustration = () => (
|
||||
<div className="flex h-full flex-col justify-center text-sm dark:bg-transparent">
|
||||
<div className="mx-auto grid gap-3">
|
||||
{[
|
||||
{ icon: UserPlusIcon, text: "Add new employee" },
|
||||
{ icon: "addUser", text: "Add new employee" },
|
||||
{
|
||||
icon: PencilLineIcon,
|
||||
icon: "write",
|
||||
text: "Invite employee to fill in their profile",
|
||||
},
|
||||
{ icon: LockIcon, text: "Get confirmation from admin" },
|
||||
].map(({ text, icon: Icon }, index) => (
|
||||
{ icon: "permissions", text: "Get confirmation from admin" },
|
||||
].map(({ text, icon }, index) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-green-800 bg-green-100 leading-none font-medium text-center p-1.5 block rounded-full dark:bg-green-800 dark:text-green-200">
|
||||
<Icon strokeWidth={2} size={15} />
|
||||
<Icon name={icon} size="xs" />
|
||||
</span>
|
||||
{text}
|
||||
</div>
|
||||
@@ -102,9 +93,9 @@ const OnboardingIllustration = () => (
|
||||
const MusicIllustration = () => (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8">
|
||||
<div className="p-3 w-[12rem] h-[8rem] border border-dashed border-blue dark:border-blue-500 rounded-lg flex gap-2 flex-col items-center justify-center">
|
||||
<CloudUploadIcon
|
||||
size={40}
|
||||
strokeWidth={1.5}
|
||||
<Icon
|
||||
name="upload"
|
||||
size="4xl"
|
||||
className="stroke-blue mx-auto dark:stroke-blue-500"
|
||||
/>
|
||||
<p className="whitespace-nowrap text-stone-900 dark:text-white">
|
||||
@@ -117,9 +108,9 @@ const MusicIllustration = () => (
|
||||
const ImageUploadIllustration = () => (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8">
|
||||
<div className="p-3 w-[12rem] h-[8rem] border border-dashed border-blue dark:border-blue-500 rounded-lg flex gap-2 flex-col items-center justify-center">
|
||||
<ImageIcon
|
||||
size={40}
|
||||
strokeWidth={1.5}
|
||||
<Icon
|
||||
name="upload"
|
||||
size="4xl"
|
||||
className="stroke-blue mx-auto dark:stroke-blue-500"
|
||||
/>
|
||||
<p className="whitespace-nowrap text-stone-900 dark:text-white">
|
||||
@@ -245,9 +236,9 @@ const FileShareIllustration = () => (
|
||||
<p>This file was shared with you.</p>
|
||||
<div className="p-3 w-full border rounded-lg flex justify-between gap-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderArchiveIcon
|
||||
size={24}
|
||||
strokeWidth={1.5}
|
||||
<Icon
|
||||
name="zip"
|
||||
size="xl"
|
||||
className="stroke-blue dark:stroke-blue-500"
|
||||
/>
|
||||
<p className="whitespace-nowrap text-stone-900 dark:text-white">
|
||||
@@ -263,11 +254,7 @@ const FileShareIllustration = () => (
|
||||
const PasskeyIllustration = () => (
|
||||
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent">
|
||||
<div className="p-4 flex flex-col items-center gap-3 rounded-md shadow-xl shadow-stone-400/20 bg-white dark:shadow-none">
|
||||
<FingerprintIcon
|
||||
size={36}
|
||||
strokeWidth={0.75}
|
||||
className="stroke-red-600"
|
||||
/>
|
||||
<Icon name="touchId" size="3xl" className="stroke-red-600" />
|
||||
<p className="text-xs dark:text-stone-900">Continue with Touch ID</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { SideNav } from "@/components/SideNav";
|
||||
import { SideNavHeader } from "@/components/SideNavHeader";
|
||||
import { SideNavItem } from "@/components/SideNavItem";
|
||||
import { docNavigationItems } from "@/lib/docNavigationItems";
|
||||
import { packages } from "@/lib/packages";
|
||||
import { clsx } from "clsx";
|
||||
import { ChevronRight, PackageIcon } from "lucide-react";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import Link from "next/link";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { requestProject } from "./requestProject";
|
||||
|
||||
export function ApiNav({ className }: { className?: string }) {
|
||||
@@ -37,7 +34,7 @@ export async function PackageNavItem({
|
||||
className="mb-1 flex gap-2 items-center"
|
||||
href={`/api-reference/${packageName}`}
|
||||
>
|
||||
<PackageIcon size={15} strokeWidth={1.5} />
|
||||
<Icon name="package" size="xs" />
|
||||
{packageName}
|
||||
</SideNavItem>
|
||||
{project.categories?.map((category) => {
|
||||
@@ -50,7 +47,11 @@ export async function PackageNavItem({
|
||||
<summary className="pl-[13px] py-1 cursor-pointer flex gap-2 items-center justify-between hover:text-stone-800 dark:hover:text-stone-200 [&::-webkit-details-marker]:hidden">
|
||||
{category.title}
|
||||
|
||||
<ChevronRight className="w-4 h-4 text-stone-300 group-open:rotate-90 transition-transform dark:text-stone-800" />
|
||||
<Icon
|
||||
name="chevronRight"
|
||||
size="sm"
|
||||
className="text-stone-300 group-open:rotate-90 transition-transform dark:text-stone-800"
|
||||
/>
|
||||
</summary>
|
||||
<div className="pl-6">
|
||||
{category.children.map(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PackageIcon, Type } from "lucide-react";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import {
|
||||
CommentDisplayPart,
|
||||
DeclarationReflection,
|
||||
@@ -30,7 +30,7 @@ export async function PackageDocs({
|
||||
return (
|
||||
<>
|
||||
<h2 className="flex items-center gap-2">
|
||||
<code>{packageName}</code> <PackageIcon />
|
||||
<code>{packageName}</code> <Icon name="package" size="md" />
|
||||
</h2>
|
||||
{project.categories?.map((category) => {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
import { getHighlighter } from "shiki";
|
||||
@@ -79,7 +79,7 @@ export function ClassOrInterface({
|
||||
href={"#" + name}
|
||||
className="inline-flex items-center gap-2 lg:-ml-[22px]"
|
||||
>
|
||||
<LinkIcon size={14} className="hidden lg:inline" />
|
||||
<Icon name="link" size="xs" className="hidden lg:inline" />
|
||||
<h3 className="text-lg lg:text-xl">
|
||||
<Highlight>
|
||||
{(isInterface ? "interface " : "class ") + name + typeParameters}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { Card } from "gcmp-design-system/src/app/components/atoms/Card";
|
||||
import { H3 } from "gcmp-design-system/src/app/components/atoms/Headings";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
|
||||
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import QRCode from "qrcode";
|
||||
import {
|
||||
@@ -179,7 +179,11 @@ export function ChatDemoSection() {
|
||||
className="text-blue dark:text-blue-400"
|
||||
onClick={copyUrl}
|
||||
>
|
||||
{copied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
|
||||
{copied ? (
|
||||
<Icon name="check" size="xs" />
|
||||
) : (
|
||||
<Icon name="copy" size="xs" />
|
||||
)}
|
||||
<span className="sr-only">Copy URL</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card } from "gcmp-design-system/src/app/components/atoms/Card";
|
||||
import { H3 } from "gcmp-design-system/src/app/components/atoms/Headings";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
import { LockKeyholeIcon } from "lucide-react";
|
||||
|
||||
const randomChars = [
|
||||
"SFPOHVKNPDKETOMQLMJKX#QDI=TFFFMRJDSJ",
|
||||
@@ -48,13 +48,19 @@ function Illustration() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LockKeyholeIcon
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="butt"
|
||||
size={80}
|
||||
className="z-30 size-8 text-blue p-1.5 rounded-lg bg-blue-50 dark:text-blue-500 dark:bg-stone-900 md:size-10"
|
||||
<Icon
|
||||
name="encryption"
|
||||
size="3xl"
|
||||
className="z-30 text-blue p-1.5 rounded-lg bg-blue-50 dark:text-blue-500 dark:bg-stone-900"
|
||||
/>
|
||||
|
||||
{/*<LockKeyholeIcon*/}
|
||||
{/* strokeWidth={1.5}*/}
|
||||
{/* strokeLinecap="butt"*/}
|
||||
{/* size={80}*/}
|
||||
{/* className="z-30 size-8 text-blue p-1.5 rounded-lg bg-blue-50 dark:text-blue-500 dark:bg-stone-900 md:size-10"*/}
|
||||
{/*/>*/}
|
||||
|
||||
<div className="w-20 h-full bg-gradient-to-r from-white to-transparent absolute top-0 left-0 z-10 dark:from-stone-925"></div>
|
||||
<div className="hidden md:block h-20 w-full bg-gradient-to-b from-white to-transparent absolute top-0 left-0 z-10 dark:from-stone-925"></div>
|
||||
<div className="h-20 w-full bg-gradient-to-t from-white to-transparent absolute bottom-0 left-0 z-10 dark:from-stone-925"></div>
|
||||
|
||||
@@ -3,24 +3,14 @@ import { ClerkLogo } from "@/components/icons/ClerkLogo";
|
||||
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
|
||||
import { Card } from "gcmp-design-system/src/app/components/atoms/Card";
|
||||
import { H3 } from "gcmp-design-system/src/app/components/atoms/Headings";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
|
||||
import {
|
||||
CheckIcon,
|
||||
FileTextIcon,
|
||||
FingerprintIcon,
|
||||
ImageIcon,
|
||||
ScanFace,
|
||||
TrashIcon,
|
||||
UploadCloudIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "File uploads",
|
||||
icon: UploadCloudIcon,
|
||||
description: (
|
||||
<>
|
||||
Just use <code>{`<input type="file"/>`}</code>, and easily convert from
|
||||
@@ -37,22 +27,21 @@ const features = [
|
||||
</pre>
|
||||
|
||||
<div className="w-full bg-white rounded-md py-3 px-3 flex gap-4 items-center border rounded-xl shadow-lg shadow-stone-500/10 dark:bg-stone-925">
|
||||
<FileTextIcon
|
||||
size={32}
|
||||
strokeWidth={1}
|
||||
<Icon
|
||||
size="2xl"
|
||||
name="file"
|
||||
className="text-blue dark:text-blue-500"
|
||||
/>
|
||||
<div className="text-2xl flex-1 text-blue dark:text-blue-500">
|
||||
file.pdf
|
||||
</div>
|
||||
<TrashIcon size={32} strokeWidth={1} className="text-stone-500" />
|
||||
<Icon size="2xl" name="delete" className="text-stone-500" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Progressive image loading",
|
||||
icon: ImageIcon,
|
||||
description: (
|
||||
<>
|
||||
Using Jazz's <code>ImageDefinition</code> component, you get
|
||||
@@ -80,7 +69,6 @@ const features = [
|
||||
},
|
||||
{
|
||||
title: "Server workers",
|
||||
icon: ImageIcon,
|
||||
description: (
|
||||
<>
|
||||
Expose an HTTP API that mutates Jazz state. Or subscribe to Jazz state
|
||||
@@ -91,7 +79,6 @@ const features = [
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
icon: UserIcon,
|
||||
description: (
|
||||
<>
|
||||
Plug and play different kinds of auth like Passkeys (Touch ID, Face ID),
|
||||
@@ -100,9 +87,9 @@ const features = [
|
||||
),
|
||||
illustration: (
|
||||
<div className="flex gap-4 justify-center text-black dark:text-white">
|
||||
<ScanFace className="h-16 w-auto" strokeWidth={1} />
|
||||
<Icon size="5xl" name="faceId" className="h-16 w-auto" />
|
||||
<ClerkLogo className="h-16 py-0.5 w-auto" />
|
||||
<FingerprintIcon className="h-16 w-auto" strokeWidth={1} />
|
||||
<Icon size="5xl" name="touchId" className="h-16 w-auto" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -124,7 +111,7 @@ export function FeaturesSection() {
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-4 lg:gap-8">
|
||||
{features.map(({ title, icon: Icon, description, illustration }) => (
|
||||
{features.map(({ title, description, illustration }) => (
|
||||
<Card key={title} className="col-span-2 overflow-hidden">
|
||||
<div className="h-48 flex w-full items-center justify-center">
|
||||
{illustration}
|
||||
@@ -183,7 +170,7 @@ export function FeaturesSection() {
|
||||
className="flex items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<span className="text-blue p-1 rounded-full bg-blue-50 dark:text-blue-500 dark:bg-white/10">
|
||||
<CheckIcon size={12} strokeWidth={3} />
|
||||
<Icon name="check" size="xs" />
|
||||
</span>
|
||||
{feature}
|
||||
</li>
|
||||
|
||||
@@ -1,48 +1,39 @@
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
import {
|
||||
FileLock2Icon,
|
||||
GaugeIcon,
|
||||
LockKeyholeIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
MousePointerSquareDashedIcon,
|
||||
UploadCloudIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Instant updates",
|
||||
icon: GaugeIcon,
|
||||
icon: "instant",
|
||||
},
|
||||
{
|
||||
title: "Real-time sync",
|
||||
icon: MonitorSmartphoneIcon,
|
||||
icon: "devices",
|
||||
},
|
||||
{
|
||||
title: "Multiplayer",
|
||||
icon: MousePointerSquareDashedIcon,
|
||||
icon: "spatialPresence",
|
||||
},
|
||||
{
|
||||
title: "File uploads",
|
||||
icon: UploadCloudIcon,
|
||||
icon: "upload",
|
||||
},
|
||||
{
|
||||
title: "Social features",
|
||||
icon: UsersIcon,
|
||||
icon: "social",
|
||||
},
|
||||
{
|
||||
title: "Permissions",
|
||||
icon: FileLock2Icon,
|
||||
icon: "permissions",
|
||||
},
|
||||
{
|
||||
title: "E2E encryption",
|
||||
icon: LockKeyholeIcon,
|
||||
icon: "encryption",
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
icon: UserIcon,
|
||||
icon: "auth",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,13 +65,13 @@ export function HeroSection() {
|
||||
</Prose>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 max-w-3xl sm:grid-cols-4 sm:gap-4">
|
||||
{features.map(({ title, icon: Icon }) => (
|
||||
{features.map(({ title, icon }) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex text-xs sm:text-sm gap-2 items-center"
|
||||
>
|
||||
<span className="text-blue p-1.5 rounded-lg bg-blue-50 dark:text-blue-500 dark:bg-stone-900">
|
||||
<Icon size={16} />
|
||||
<Icon size="xs" name={icon} />
|
||||
</span>
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { FeatureCard } from "gcmp-design-system/src/app/components/molecules/FeatureCard";
|
||||
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
|
||||
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
|
||||
import {
|
||||
GaugeIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
MousePointerSquareDashedIcon,
|
||||
WifiOffIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export function LocalFirstFeaturesSection() {
|
||||
const features = [
|
||||
{
|
||||
title: "Offline-first",
|
||||
icon: WifiOffIcon,
|
||||
icon: "offline",
|
||||
description: (
|
||||
<>
|
||||
Your app works seamlessly offline or on sketchy connections. When
|
||||
@@ -22,7 +16,7 @@ export function LocalFirstFeaturesSection() {
|
||||
},
|
||||
{
|
||||
title: "Instant updates",
|
||||
icon: GaugeIcon,
|
||||
icon: "instant",
|
||||
description: (
|
||||
<>
|
||||
Since you're working with local state, your UI updates instantly.
|
||||
@@ -32,7 +26,7 @@ export function LocalFirstFeaturesSection() {
|
||||
},
|
||||
{
|
||||
title: "Real-time sync",
|
||||
icon: MonitorSmartphoneIcon,
|
||||
icon: "devices",
|
||||
description: (
|
||||
<>
|
||||
Every device with the same account will always have everything in
|
||||
@@ -42,7 +36,7 @@ export function LocalFirstFeaturesSection() {
|
||||
},
|
||||
{
|
||||
title: "Multiplayer",
|
||||
icon: MousePointerSquareDashedIcon,
|
||||
icon: "spatialPresence",
|
||||
description: (
|
||||
<>
|
||||
Adding multiplayer is as easy as sharing synced data with other users.
|
||||
@@ -66,10 +60,10 @@ export function LocalFirstFeaturesSection() {
|
||||
}
|
||||
/>
|
||||
<GappedGrid cols={4}>
|
||||
{features.map(({ title, icon: Icon, description }) => (
|
||||
{features.map(({ title, icon, description }) => (
|
||||
<FeatureCard
|
||||
label={title}
|
||||
icon={Icon}
|
||||
icon={icon}
|
||||
explanation={description}
|
||||
key={title}
|
||||
></FeatureCard>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DiagramAfterJazz } from "@/components/DiagramAfterJazz";
|
||||
import { DiagramBeforeJazz } from "@/components/DiagramBeforeJazz";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
import { SectionHeader } from "gcmp-design-system/src/app/components/molecules/SectionHeader";
|
||||
import { ArrowDownIcon, ArrowRightIcon } from "lucide-react";
|
||||
|
||||
export default function ProblemStatementSection() {
|
||||
return (
|
||||
@@ -17,12 +17,12 @@ export default function ProblemStatementSection() {
|
||||
<div className="flex flex-col bg-stone-50 relative gap-3 p-4 pb-8 md:p-8 md:gap-5 border-b sm:border-b-0 sm:border-r dark:bg-transparent dark:border-stone-900">
|
||||
<span className="hidden absolute top-0 -right-4 md:-right-6 sm:flex items-center h-full">
|
||||
<span className="p-1 md:p-3 bg-stone-200 rounded-full dark:bg-stone-900 dark:text-white">
|
||||
<ArrowRightIcon size={24} />
|
||||
<Icon name="arrowRight" />
|
||||
</span>
|
||||
</span>
|
||||
<span className="sm:hidden w-full absolute -bottom-6 flex justify-center left-0">
|
||||
<span className="p-3 bg-stone-200 rounded-full dark:bg-stone-900 dark:text-white">
|
||||
<ArrowDownIcon size={24} />
|
||||
<Icon name="arrowDown" />
|
||||
</span>
|
||||
</span>
|
||||
<Prose>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { RustLogo } from "@/components/icons/RustLogo";
|
||||
import { SvelteLogo } from "@/components/icons/SvelteLogo";
|
||||
import { SwiftLogo } from "@/components/icons/SwiftLogo";
|
||||
import { VueLogo } from "@/components/icons/VueLogo";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
|
||||
import React from "react";
|
||||
|
||||
export function SupportedEnvironmentsSection() {
|
||||
@@ -14,8 +14,9 @@ export function SupportedEnvironmentsSection() {
|
||||
{
|
||||
name: "Browser (vanilla JS)",
|
||||
icon: (
|
||||
<GlobeIcon
|
||||
strokeWidth={1}
|
||||
<Icon
|
||||
name="browser"
|
||||
size="3xl"
|
||||
className="text-stone-900 dark:text-white"
|
||||
height="1em"
|
||||
width="1em"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { socials } from "@/lib/socials";
|
||||
import { JazzLogo } from "gcmp-design-system/src/app/components/atoms/logos/JazzLogo";
|
||||
import { Nav } from "gcmp-design-system/src/app/components/organisms/Nav";
|
||||
import { BookTextIcon, BoxIcon, CodeIcon } from "lucide-react";
|
||||
import { DocNav } from "./docs/nav";
|
||||
|
||||
export function JazzNav() {
|
||||
@@ -17,36 +16,21 @@ export function JazzNav() {
|
||||
href: "/docs",
|
||||
items: [
|
||||
{
|
||||
icon: (
|
||||
<BookTextIcon
|
||||
className="size-5 stroke-blue dark:stroke-blue-500 shrink-0"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
),
|
||||
icon: "docs",
|
||||
title: "Documentation",
|
||||
href: "/docs",
|
||||
description:
|
||||
"Get started with using Jazz by learning the core concepts, and going through guides.",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<CodeIcon
|
||||
className="size-5 stroke-blue dark:stroke-blue-500 shrink-0"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
),
|
||||
icon: "code",
|
||||
title: "Example apps",
|
||||
href: "/examples",
|
||||
description:
|
||||
"Demo and source code for example apps built with Jazz.",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<BoxIcon
|
||||
className="size-5 stroke-blue dark:stroke-blue-500 shrink-0"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
),
|
||||
icon: "package",
|
||||
title: "API reference",
|
||||
href: "/api-reference",
|
||||
description:
|
||||
|
||||
@@ -162,6 +162,16 @@ export const docNavigationItems = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Design patterns",
|
||||
items: [
|
||||
{
|
||||
name: "Organization/Team",
|
||||
href: "/docs/design-patterns/organization",
|
||||
done: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Resources",
|
||||
items: [
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version && pnpm i --no-frozen-lockfile",
|
||||
"release": "pnpm changeset publish && git push --follow-tags",
|
||||
"release": "turbo run build --filter='./packages/*' && pnpm changeset publish && git push --follow-tags",
|
||||
"clean": "rm -rf ./packages/*/dist && rm -rf ./packages/*/node_modules && rm -rf ./examples/*/node_modules && rm -rf ./examples/*/dist"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.8.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3252502]
|
||||
- Updated dependencies [6370348]
|
||||
- Updated dependencies [ac216b9]
|
||||
- cojson@0.8.41
|
||||
- cojson-storage@0.8.41
|
||||
|
||||
## 0.8.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.8.40",
|
||||
"version": "0.8.41",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.8.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3252502]
|
||||
- Updated dependencies [6370348]
|
||||
- Updated dependencies [ac216b9]
|
||||
- cojson@0.8.41
|
||||
- cojson-storage@0.8.41
|
||||
|
||||
## 0.8.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.8.40",
|
||||
"version": "0.8.41",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cojson": "workspace:0.8.39",
|
||||
"cojson": "workspace:0.8.41",
|
||||
"cojson-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.8.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3252502]
|
||||
- Updated dependencies [6370348]
|
||||
- Updated dependencies [ac216b9]
|
||||
- cojson@0.8.41
|
||||
|
||||
## 0.8.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage",
|
||||
"version": "0.8.40",
|
||||
"version": "0.8.41",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.8.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3252502]
|
||||
- Updated dependencies [6370348]
|
||||
- Updated dependencies [ac216b9]
|
||||
- cojson@0.8.41
|
||||
|
||||
## 0.8.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.8.39",
|
||||
"version": "0.8.41",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.39",
|
||||
"cojson": "workspace:0.8.41",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson
|
||||
|
||||
## 0.8.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3252502: Optimize the transactions processing on CoMap and CoStream
|
||||
- 6370348: Remove @opentelemetry/api as a peer dependency and add it as a dependency
|
||||
- ac216b9: Add a new writeOnly role, to limit access only to their own changes. Useful to push objects into lists of moderated content.
|
||||
|
||||
## 0.8.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.8.39",
|
||||
"version": "0.8.41",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^1.29.0",
|
||||
"@types/jest": "^29.5.3",
|
||||
|
||||
@@ -443,7 +443,16 @@ export class CoValueCore {
|
||||
signatureAfter: signatureAfter,
|
||||
});
|
||||
|
||||
this._cachedContent = undefined;
|
||||
if (
|
||||
this._cachedContent &&
|
||||
"processNewTransactions" in this._cachedContent &&
|
||||
typeof this._cachedContent.processNewTransactions === "function"
|
||||
) {
|
||||
this._cachedContent.processNewTransactions();
|
||||
} else {
|
||||
this._cachedContent = undefined;
|
||||
}
|
||||
|
||||
this._cachedKnownState = undefined;
|
||||
this._cachedDependentOn = undefined;
|
||||
this._cachedNewContentSinceEmpty = undefined;
|
||||
@@ -616,14 +625,19 @@ export class CoValueCore {
|
||||
return newContent;
|
||||
}
|
||||
|
||||
getValidSortedTransactions(options?: {
|
||||
getValidTransactions(options?: {
|
||||
ignorePrivateTransactions: boolean;
|
||||
knownTransactions?: CoValueKnownState["sessions"];
|
||||
}): DecryptedTransaction[] {
|
||||
const validTransactions = determineValidTransactions(this);
|
||||
|
||||
const allTransactions: DecryptedTransaction[] = [];
|
||||
|
||||
for (const { txID, tx } of validTransactions) {
|
||||
if (options?.knownTransactions?.[txID.sessionID]! >= txID.txIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tx.privacy === "trusting") {
|
||||
allTransactions.push({
|
||||
txID,
|
||||
@@ -670,21 +684,35 @@ export class CoValueCore {
|
||||
});
|
||||
}
|
||||
|
||||
allTransactions.sort(
|
||||
(a, b) =>
|
||||
a.madeAt - b.madeAt ||
|
||||
(a.txID.sessionID < b.txID.sessionID ? -1 : 1) ||
|
||||
a.txID.txIndex - b.txID.txIndex,
|
||||
);
|
||||
return allTransactions;
|
||||
}
|
||||
|
||||
getValidSortedTransactions(options?: {
|
||||
ignorePrivateTransactions: boolean;
|
||||
}): DecryptedTransaction[] {
|
||||
const allTransactions = this.getValidTransactions(options);
|
||||
|
||||
allTransactions.sort(this.compareTransactions);
|
||||
|
||||
return allTransactions;
|
||||
}
|
||||
|
||||
compareTransactions(
|
||||
a: Pick<DecryptedTransaction, "madeAt" | "txID">,
|
||||
b: Pick<DecryptedTransaction, "madeAt" | "txID">,
|
||||
) {
|
||||
return (
|
||||
a.madeAt - b.madeAt ||
|
||||
(a.txID.sessionID < b.txID.sessionID ? -1 : 1) ||
|
||||
a.txID.txIndex - b.txID.txIndex
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
|
||||
if (this.header.ruleset.type === "group") {
|
||||
const content = expectGroup(this.getCurrentContent());
|
||||
|
||||
const currentKeyId = content.get("readKey");
|
||||
const currentKeyId = content.getCurrentReadKeyId();
|
||||
|
||||
if (!currentKeyId) {
|
||||
throw new Error("No readKey set");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { CoID, RawCoValue } from "../coValue.js";
|
||||
import { CoValueCore, DecryptedTransaction } from "../coValueCore.js";
|
||||
import { CoValueCore } from "../coValueCore.js";
|
||||
import { AgentID, TransactionID } from "../ids.js";
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoValueKnownState } from "../sync.js";
|
||||
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
||||
import { isCoValue } from "../typeUtils/isCoValue.js";
|
||||
import { RawAccountID } from "./account.js";
|
||||
@@ -46,14 +47,14 @@ export class RawCoMapView<
|
||||
/** @internal */
|
||||
latestTxMadeAt: number;
|
||||
/** @internal */
|
||||
cachedOps?: {
|
||||
ops: {
|
||||
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
||||
};
|
||||
/** @internal */
|
||||
validSortedTransactions?: DecryptedTransaction[];
|
||||
knownTransactions: CoValueKnownState["sessions"];
|
||||
|
||||
/** @internal */
|
||||
options?: { ignorePrivateTransactions: boolean; atTime?: number };
|
||||
ignorePrivateTransactions: boolean;
|
||||
/** @internal */
|
||||
atTimeFilter?: number = undefined;
|
||||
/** @category 6. Meta */
|
||||
@@ -64,123 +65,81 @@ export class RawCoMapView<
|
||||
core: CoValueCore,
|
||||
options?: {
|
||||
ignorePrivateTransactions: boolean;
|
||||
atTime?: number;
|
||||
validSortedTransactions?: DecryptedTransaction[];
|
||||
cachedOps?: {
|
||||
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
||||
};
|
||||
},
|
||||
) {
|
||||
this.id = core.id as CoID<this>;
|
||||
this.core = core;
|
||||
this.latest = {};
|
||||
this.latestTxMadeAt = 0;
|
||||
this.options = options;
|
||||
this.cachedOps = options?.cachedOps;
|
||||
this.validSortedTransactions = options?.validSortedTransactions;
|
||||
this.ignorePrivateTransactions =
|
||||
options?.ignorePrivateTransactions ?? false;
|
||||
this.ops = {};
|
||||
this.latest = {};
|
||||
this.knownTransactions = {};
|
||||
|
||||
this.processLatestTransactions();
|
||||
this.processNewTransactions();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private getValidSortedTransactions() {
|
||||
if (this.validSortedTransactions) {
|
||||
return this.validSortedTransactions;
|
||||
processNewTransactions() {
|
||||
if (this.isTimeTravelEntity()) {
|
||||
throw new Error("Cannot process transactions on a time travel entity");
|
||||
}
|
||||
|
||||
const validSortedTransactions = this.core.getValidSortedTransactions({
|
||||
ignorePrivateTransactions:
|
||||
this.options?.ignorePrivateTransactions ?? false,
|
||||
const { ops } = this;
|
||||
|
||||
const changedEntries = new Map<
|
||||
keyof typeof ops,
|
||||
NonNullable<(typeof ops)[keyof typeof ops]>
|
||||
>();
|
||||
|
||||
const nextValidTransactions = this.core.getValidTransactions({
|
||||
ignorePrivateTransactions: this.ignorePrivateTransactions,
|
||||
knownTransactions: this.knownTransactions,
|
||||
});
|
||||
|
||||
this.validSortedTransactions = validSortedTransactions;
|
||||
|
||||
return validSortedTransactions;
|
||||
}
|
||||
|
||||
private resetCachedValues() {
|
||||
this.validSortedTransactions = undefined;
|
||||
this.cachedOps = undefined;
|
||||
}
|
||||
|
||||
private processLatestTransactions() {
|
||||
// Reset all internal state and cached values
|
||||
this.latest = {};
|
||||
this.latestTxMadeAt = 0;
|
||||
|
||||
const { latest } = this;
|
||||
|
||||
const atTimeFilter = this.options?.atTime;
|
||||
|
||||
for (const { txID, changes, madeAt } of this.getValidSortedTransactions()) {
|
||||
if (atTimeFilter && madeAt > atTimeFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (madeAt > this.latestTxMadeAt) {
|
||||
this.latestTxMadeAt = madeAt;
|
||||
}
|
||||
|
||||
for (const { txID, changes, madeAt } of nextValidTransactions) {
|
||||
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
|
||||
const change = changes[changeIdx] as MapOpPayload<
|
||||
keyof Shape & string,
|
||||
Shape[keyof Shape & string]
|
||||
>;
|
||||
const entry = latest[change.key];
|
||||
if (!entry) {
|
||||
latest[change.key] = {
|
||||
txID,
|
||||
madeAt,
|
||||
changeIdx,
|
||||
change,
|
||||
};
|
||||
} else if (madeAt >= entry.madeAt) {
|
||||
entry.txID = txID;
|
||||
entry.madeAt = madeAt;
|
||||
entry.changeIdx = changeIdx;
|
||||
entry.change = change;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revalidateTransactions() {
|
||||
this.resetCachedValues();
|
||||
this.processLatestTransactions();
|
||||
}
|
||||
|
||||
private getOps() {
|
||||
if (this.cachedOps) {
|
||||
return this.cachedOps;
|
||||
}
|
||||
|
||||
const ops: {
|
||||
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
||||
} = {};
|
||||
|
||||
for (const { txID, changes, madeAt } of this.getValidSortedTransactions()) {
|
||||
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
|
||||
const change = changes[changeIdx] as MapOpPayload<
|
||||
keyof Shape & string,
|
||||
Shape[keyof Shape & string]
|
||||
>;
|
||||
let entries = ops[change.key];
|
||||
if (!entries) {
|
||||
entries = [];
|
||||
ops[change.key] = entries;
|
||||
}
|
||||
entries.push({
|
||||
const entry = {
|
||||
txID,
|
||||
madeAt,
|
||||
changeIdx,
|
||||
change,
|
||||
});
|
||||
};
|
||||
|
||||
if (madeAt > this.latestTxMadeAt) {
|
||||
this.latestTxMadeAt = madeAt;
|
||||
}
|
||||
|
||||
const entries = ops[change.key];
|
||||
if (!entries) {
|
||||
const entries = [entry];
|
||||
ops[change.key] = entries;
|
||||
changedEntries.set(change.key, entries);
|
||||
} else {
|
||||
entries.push(entry);
|
||||
changedEntries.set(change.key, entries);
|
||||
}
|
||||
this.knownTransactions[txID.sessionID] = Math.max(
|
||||
this.knownTransactions[txID.sessionID] ?? 0,
|
||||
txID.txIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedOps = ops;
|
||||
for (const entries of changedEntries.values()) {
|
||||
entries.sort(this.core.compareTransactions);
|
||||
}
|
||||
|
||||
return ops;
|
||||
for (const [key, entries] of changedEntries.entries()) {
|
||||
this.latest[key] = entries[entries.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
isTimeTravelEntity() {
|
||||
return Boolean(this.atTimeFilter);
|
||||
}
|
||||
|
||||
/** @category 6. Meta */
|
||||
@@ -198,14 +157,11 @@ export class RawCoMapView<
|
||||
if (time >= this.latestTxMadeAt) {
|
||||
return this;
|
||||
} else {
|
||||
const clone = new RawCoMapView(this.core, {
|
||||
ignorePrivateTransactions:
|
||||
this.options?.ignorePrivateTransactions ?? false,
|
||||
atTime: time,
|
||||
cachedOps: this.cachedOps,
|
||||
validSortedTransactions: this.validSortedTransactions,
|
||||
});
|
||||
Object.setPrototypeOf(clone, this);
|
||||
const clone = Object.create(this) as RawCoMapView<Shape, Meta>;
|
||||
|
||||
clone.atTimeFilter = time;
|
||||
clone.latest = {};
|
||||
|
||||
return clone as this;
|
||||
}
|
||||
}
|
||||
@@ -218,12 +174,12 @@ export class RawCoMapView<
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const atTimeFilter = this.options?.atTime;
|
||||
const atTimeFilter = this.atTimeFilter;
|
||||
|
||||
if (atTimeFilter) {
|
||||
return this.getOps()[key]?.filter((op) => op.madeAt <= atTimeFilter);
|
||||
return this.ops[key]?.filter((op) => op.madeAt <= atTimeFilter);
|
||||
} else {
|
||||
return this.getOps()[key];
|
||||
return this.ops[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,36 +188,64 @@ export class RawCoMapView<
|
||||
*
|
||||
* @category 1. Reading */
|
||||
keys<K extends keyof Shape & string = keyof Shape & string>(): K[] {
|
||||
return (Object.keys(this.latest) as K[]).filter((key) => {
|
||||
const latestChange = this.latest[key];
|
||||
return (Object.keys(this.ops) as K[]).filter((key) => {
|
||||
const entry = this.getRaw(key);
|
||||
|
||||
if (!latestChange) {
|
||||
if (entry === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (latestChange.change.op === "del") {
|
||||
if (entry.change.op === "del") {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
getRaw<K extends keyof Shape & string>(key: K) {
|
||||
let latestChange = this.latest[key];
|
||||
|
||||
if (latestChange === undefined) {
|
||||
const entries = this.ops[key];
|
||||
|
||||
// Time travel values are lazily computed
|
||||
if (entries && !(key in this.latest)) {
|
||||
const atTimeFilter = this.atTimeFilter;
|
||||
|
||||
if (!atTimeFilter) {
|
||||
latestChange = entries[entries.length - 1];
|
||||
} else {
|
||||
latestChange = entries.findLast((op) => op.madeAt <= atTimeFilter);
|
||||
}
|
||||
|
||||
this.latest[key] = latestChange;
|
||||
}
|
||||
|
||||
if (latestChange === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return latestChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value for the given key.
|
||||
*
|
||||
* @category 1. Reading
|
||||
**/
|
||||
get<K extends keyof Shape & string>(key: K): Shape[K] | undefined {
|
||||
const latestChange = this.latest[key];
|
||||
if (!latestChange) {
|
||||
const entry = this.getRaw(key);
|
||||
|
||||
if (entry === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (latestChange.change.op === "del") {
|
||||
if (entry.change.op === "del") {
|
||||
return undefined;
|
||||
} else {
|
||||
return latestChange.change.value as Shape[K];
|
||||
return entry.change.value as Shape[K];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +257,7 @@ export class RawCoMapView<
|
||||
[K in keyof Shape & string]: Shape[K];
|
||||
}> = {};
|
||||
|
||||
for (const key of Object.keys(this.latest) as (keyof Shape & string)[]) {
|
||||
for (const key of Object.keys(this.ops) as (keyof Shape & string)[]) {
|
||||
const value = this.get(key);
|
||||
if (value !== undefined) {
|
||||
object[key] = value;
|
||||
@@ -294,9 +278,9 @@ export class RawCoMapView<
|
||||
|
||||
/** @category 5. Edit history */
|
||||
nthEditAt<K extends keyof Shape & string>(key: K, n: number) {
|
||||
const ops = this.getOps()[key];
|
||||
const ops = this.ops[key];
|
||||
|
||||
const atTimeFilter = this.options?.atTime;
|
||||
const atTimeFilter = this.atTimeFilter;
|
||||
const entry = ops?.[n];
|
||||
|
||||
if (!entry) {
|
||||
@@ -321,24 +305,31 @@ export class RawCoMapView<
|
||||
value?: Shape[K];
|
||||
}
|
||||
| undefined {
|
||||
const ops = this.timeFilteredOps(key);
|
||||
const lastEntry = ops?.[ops.length - 1];
|
||||
const entry = this.getRaw(key);
|
||||
|
||||
if (!lastEntry) {
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return operationToEditEntry(lastEntry);
|
||||
return operationToEditEntry(entry);
|
||||
}
|
||||
|
||||
/** @category 5. Edit history */
|
||||
*editsAt<K extends keyof Shape & string>(key: K) {
|
||||
const ops = this.timeFilteredOps(key);
|
||||
if (!ops) {
|
||||
const entries = this.ops[key];
|
||||
|
||||
if (!entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of ops) {
|
||||
const atTimeFilter = this.atTimeFilter;
|
||||
|
||||
for (const entry of entries) {
|
||||
// Entries are sorted by madeAt
|
||||
if (atTimeFilter && entry.madeAt > atTimeFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield operationToEditEntry(entry);
|
||||
}
|
||||
}
|
||||
@@ -374,6 +365,10 @@ export class RawCoMap<
|
||||
value: Shape[K],
|
||||
privacy: "private" | "trusting" = "private",
|
||||
): void {
|
||||
if (this.isTimeTravelEntity()) {
|
||||
throw new Error("Cannot set value on a time travel entity");
|
||||
}
|
||||
|
||||
this.core.makeTransaction(
|
||||
[
|
||||
{
|
||||
@@ -385,7 +380,7 @@ export class RawCoMap<
|
||||
privacy,
|
||||
);
|
||||
|
||||
this.revalidateTransactions();
|
||||
this.processNewTransactions();
|
||||
}
|
||||
|
||||
/** Delete the given key (setting it to undefined).
|
||||
@@ -400,6 +395,10 @@ export class RawCoMap<
|
||||
key: keyof Shape & string,
|
||||
privacy: "private" | "trusting" = "private",
|
||||
) {
|
||||
if (this.isTimeTravelEntity()) {
|
||||
throw new Error("Cannot delete value on a time travel entity");
|
||||
}
|
||||
|
||||
this.core.makeTransaction(
|
||||
[
|
||||
{
|
||||
@@ -410,7 +409,7 @@ export class RawCoMap<
|
||||
privacy,
|
||||
);
|
||||
|
||||
this.revalidateTransactions();
|
||||
this.processNewTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CoID, RawCoValue } from "../coValue.js";
|
||||
import { CoValueCore } from "../coValueCore.js";
|
||||
import { AgentID, SessionID, TransactionID } from "../ids.js";
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoValueKnownState } from "../sync.js";
|
||||
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
||||
import { isAccountID } from "../typeUtils/isAccountID.js";
|
||||
import { isCoValue } from "../typeUtils/isCoValue.js";
|
||||
@@ -52,13 +53,16 @@ export class RawCoStreamView<
|
||||
items: {
|
||||
[key: SessionID]: CoStreamItem<Item>[];
|
||||
};
|
||||
/** @internal */
|
||||
knownTransactions: CoValueKnownState["sessions"];
|
||||
readonly _item!: Item;
|
||||
|
||||
constructor(core: CoValueCore) {
|
||||
this.id = core.id as CoID<this>;
|
||||
this.core = core;
|
||||
this.items = {};
|
||||
this.fillFromCoValue();
|
||||
this.knownTransactions = {};
|
||||
this.processNewTransactions();
|
||||
}
|
||||
|
||||
get headerMeta(): Meta {
|
||||
@@ -75,14 +79,13 @@ export class RawCoStreamView<
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
protected fillFromCoValue() {
|
||||
this.items = {};
|
||||
protected processNewTransactions() {
|
||||
const changeEntries = new Set<CoStreamItem<Item>[]>();
|
||||
|
||||
for (const {
|
||||
txID,
|
||||
madeAt,
|
||||
changes,
|
||||
} of this.core.getValidSortedTransactions()) {
|
||||
for (const { txID, madeAt, changes } of this.core.getValidTransactions({
|
||||
ignorePrivateTransactions: false,
|
||||
knownTransactions: this.knownTransactions,
|
||||
})) {
|
||||
for (const changeUntyped of changes) {
|
||||
const change = changeUntyped as Item;
|
||||
let entries = this.items[txID.sessionID];
|
||||
@@ -91,7 +94,21 @@ export class RawCoStreamView<
|
||||
this.items[txID.sessionID] = entries;
|
||||
}
|
||||
entries.push({ value: change, madeAt, tx: txID });
|
||||
changeEntries.add(entries);
|
||||
}
|
||||
this.knownTransactions[txID.sessionID] = Math.max(
|
||||
this.knownTransactions[txID.sessionID] ?? 0,
|
||||
txID.txIndex,
|
||||
);
|
||||
}
|
||||
|
||||
for (const entries of changeEntries) {
|
||||
entries.sort(
|
||||
(a, b) =>
|
||||
a.madeAt - b.madeAt ||
|
||||
(a.tx.sessionID < b.tx.sessionID ? -1 : 1) ||
|
||||
a.tx.txIndex - b.tx.txIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +272,7 @@ export class RawCoStream<
|
||||
{
|
||||
push(item: Item, privacy: "private" | "trusting" = "private"): void {
|
||||
this.core.makeTransaction([isCoValue(item) ? item.id : item], privacy);
|
||||
this.fillFromCoValue();
|
||||
this.processNewTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +360,7 @@ export class RawBinaryCoStream<
|
||||
): void {
|
||||
this.core.makeTransaction([item], privacy);
|
||||
if (updateView) {
|
||||
this.fillFromCoValue();
|
||||
this.processNewTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
isParentGroupReference,
|
||||
} from "../ids.js";
|
||||
import { JsonObject } from "../jsonValue.js";
|
||||
import { Role } from "../permissions.js";
|
||||
import { AccountRole, Role } from "../permissions.js";
|
||||
import { expectGroup } from "../typeUtils/expectGroup.js";
|
||||
import {
|
||||
ControlledAccountOrAgent,
|
||||
@@ -33,6 +33,7 @@ export type GroupShape = {
|
||||
[key: RawAccountID | AgentID]: Role;
|
||||
[EVERYONE]?: Role;
|
||||
readKey?: KeyID;
|
||||
[writeKeyFor: `writeKeyFor_${RawAccountID | AgentID}`]: KeyID;
|
||||
[revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`]: Sealed<KeySecret>;
|
||||
[revelationFor: `${KeyID}_for_${Everyone}`]: KeySecret;
|
||||
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
||||
@@ -93,7 +94,7 @@ export class RawGroup<
|
||||
}
|
||||
| undefined = roleHere && { role: roleHere, via: undefined };
|
||||
|
||||
const parentGroups = this.getParentGroups(this.options?.atTime);
|
||||
const parentGroups = this.getParentGroups(this.atTimeFilter);
|
||||
|
||||
for (const parentGroup of parentGroups) {
|
||||
const roleInParent = parentGroup.roleOfInternal(accountID);
|
||||
@@ -213,18 +214,19 @@ export class RawGroup<
|
||||
account: RawAccount | ControlledAccountOrAgent | AgentID | Everyone,
|
||||
role: Role,
|
||||
) {
|
||||
const currentReadKey = this.core.getCurrentReadKey();
|
||||
|
||||
if (!currentReadKey.secret) {
|
||||
throw new Error("Can't add member without read key secret");
|
||||
}
|
||||
|
||||
if (account === EVERYONE) {
|
||||
if (!(role === "reader" || role === "writer")) {
|
||||
throw new Error(
|
||||
"Can't make everyone something other than reader or writer",
|
||||
);
|
||||
}
|
||||
|
||||
const currentReadKey = this.core.getCurrentReadKey();
|
||||
|
||||
if (!currentReadKey.secret) {
|
||||
throw new Error("Can't add member without read key secret");
|
||||
}
|
||||
|
||||
this.set(account, role, "trusting");
|
||||
|
||||
if (this.get(account) !== role) {
|
||||
@@ -236,44 +238,168 @@ export class RawGroup<
|
||||
currentReadKey.secret,
|
||||
"trusting",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const memberKey = typeof account === "string" ? account : account.id;
|
||||
const agent =
|
||||
typeof account === "string"
|
||||
? account
|
||||
: account.currentAgentID()._unsafeUnwrap({ withStackTrace: true });
|
||||
|
||||
/**
|
||||
* WriteOnly members can only see their own changes.
|
||||
*
|
||||
* We don't want to reveal the readKey to them so we create a new one specifically for them and also reveal it to everyone else with a reader or higher-capability role (but crucially not to other writer-only members)
|
||||
* to everyone else.
|
||||
*
|
||||
* To never reveal the readKey to writeOnly members we also create a dedicated writeKey for the
|
||||
* invite.
|
||||
*/
|
||||
if (role === "writeOnly" || role === "writeOnlyInvite") {
|
||||
const writeKeyForNewMember = this.core.crypto.newRandomKeySecret();
|
||||
|
||||
this.set(memberKey, role, "trusting");
|
||||
this.set(`writeKeyFor_${memberKey}`, writeKeyForNewMember.id, "trusting");
|
||||
|
||||
this.storeKeyRevelationForMember(
|
||||
memberKey,
|
||||
agent,
|
||||
writeKeyForNewMember.id,
|
||||
writeKeyForNewMember.secret,
|
||||
);
|
||||
|
||||
for (const otherMemberKey of this.getMemberKeys()) {
|
||||
const memberRole = this.get(otherMemberKey);
|
||||
|
||||
if (
|
||||
memberRole === "reader" ||
|
||||
memberRole === "writer" ||
|
||||
memberRole === "admin" ||
|
||||
memberRole === "readerInvite" ||
|
||||
memberRole === "writerInvite" ||
|
||||
memberRole === "adminInvite"
|
||||
) {
|
||||
const otherMemberAgent = this.core.node
|
||||
.resolveAccountAgent(
|
||||
otherMemberKey,
|
||||
"Expected member agent to be loaded",
|
||||
)
|
||||
._unsafeUnwrap({ withStackTrace: true });
|
||||
|
||||
this.storeKeyRevelationForMember(
|
||||
otherMemberKey,
|
||||
otherMemberAgent,
|
||||
writeKeyForNewMember.id,
|
||||
writeKeyForNewMember.secret,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const memberKey = typeof account === "string" ? account : account.id;
|
||||
const agent =
|
||||
typeof account === "string"
|
||||
? account
|
||||
: account.currentAgentID()._unsafeUnwrap({ withStackTrace: true });
|
||||
const currentReadKey = this.core.getCurrentReadKey();
|
||||
|
||||
if (!currentReadKey.secret) {
|
||||
throw new Error("Can't add member without read key secret");
|
||||
}
|
||||
|
||||
this.set(memberKey, role, "trusting");
|
||||
|
||||
if (this.get(memberKey) !== role) {
|
||||
throw new Error("Failed to set role");
|
||||
}
|
||||
|
||||
this.set(
|
||||
`${currentReadKey.id}_for_${memberKey}`,
|
||||
this.core.crypto.seal({
|
||||
message: currentReadKey.secret,
|
||||
from: this.core.node.account.currentSealerSecret(),
|
||||
to: this.core.crypto.getAgentSealerID(agent),
|
||||
nOnceMaterial: {
|
||||
in: this.id,
|
||||
tx: this.core.nextTransactionID(),
|
||||
},
|
||||
}),
|
||||
"trusting",
|
||||
this.storeKeyRevelationForMember(
|
||||
memberKey,
|
||||
agent,
|
||||
currentReadKey.id,
|
||||
currentReadKey.secret,
|
||||
);
|
||||
|
||||
for (const keyID of this.getWriteOnlyKeys()) {
|
||||
const secret = this.core.getReadKey(keyID);
|
||||
|
||||
if (!secret) {
|
||||
console.error("Can't find key", keyID);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.storeKeyRevelationForMember(memberKey, agent, keyID, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private storeKeyRevelationForMember(
|
||||
memberKey: RawAccountID | AgentID,
|
||||
agent: AgentID,
|
||||
keyID: KeyID,
|
||||
secret: KeySecret,
|
||||
) {
|
||||
this.set(
|
||||
`${keyID}_for_${memberKey}`,
|
||||
this.core.crypto.seal({
|
||||
message: secret,
|
||||
from: this.core.node.account.currentSealerSecret(),
|
||||
to: this.core.crypto.getAgentSealerID(agent),
|
||||
nOnceMaterial: {
|
||||
in: this.id,
|
||||
tx: this.core.nextTransactionID(),
|
||||
},
|
||||
}),
|
||||
"trusting",
|
||||
);
|
||||
}
|
||||
|
||||
private getWriteOnlyKeys() {
|
||||
const keys: KeyID[] = [];
|
||||
|
||||
for (const key of this.keys()) {
|
||||
if (key.startsWith("writeKeyFor_")) {
|
||||
keys.push(
|
||||
this.get(key as `writeKeyFor_${RawAccountID | AgentID}`) as KeyID,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
getCurrentReadKeyId() {
|
||||
if (this.myRole() === "writeOnly") {
|
||||
const accountId = this.core.node.account.id;
|
||||
|
||||
return this.get(`writeKeyFor_${accountId}`) as KeyID;
|
||||
}
|
||||
|
||||
return this.get("readKey");
|
||||
}
|
||||
|
||||
getMemberKeys(): (RawAccountID | AgentID)[] {
|
||||
return this.keys().filter((key): key is RawAccountID | AgentID => {
|
||||
return key.startsWith("co_") || isAgentID(key);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
rotateReadKey() {
|
||||
const currentlyPermittedReaders = this.keys().filter((key) => {
|
||||
if (key.startsWith("co_") || isAgentID(key)) {
|
||||
const role = this.get(key);
|
||||
return role === "admin" || role === "writer" || role === "reader";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}) as (RawAccountID | AgentID)[];
|
||||
const memberKeys = this.getMemberKeys();
|
||||
|
||||
const currentlyPermittedReaders = memberKeys.filter((key) => {
|
||||
const role = this.get(key);
|
||||
return (
|
||||
role === "admin" ||
|
||||
role === "writer" ||
|
||||
role === "reader" ||
|
||||
role === "adminInvite" ||
|
||||
role === "writerInvite" ||
|
||||
role === "readerInvite"
|
||||
);
|
||||
});
|
||||
|
||||
const writeOnlyMembers = memberKeys.filter((key) => {
|
||||
const role = this.get(key);
|
||||
return role === "writeOnly" || role === "writeOnlyInvite";
|
||||
});
|
||||
|
||||
// Get these early, so we fail fast if they are unavailable
|
||||
const parentGroups = this.getParentGroups();
|
||||
@@ -293,28 +419,60 @@ export class RawGroup<
|
||||
const newReadKey = this.core.crypto.newRandomKeySecret();
|
||||
|
||||
for (const readerID of currentlyPermittedReaders) {
|
||||
const reader = this.core.node
|
||||
const agent = this.core.node
|
||||
.resolveAccountAgent(
|
||||
readerID,
|
||||
"Expected to know currently permitted reader",
|
||||
)
|
||||
._unsafeUnwrap({ withStackTrace: true });
|
||||
|
||||
this.set(
|
||||
`${newReadKey.id}_for_${readerID}`,
|
||||
this.core.crypto.seal({
|
||||
message: newReadKey.secret,
|
||||
from: this.core.node.account.currentSealerSecret(),
|
||||
to: this.core.crypto.getAgentSealerID(reader),
|
||||
nOnceMaterial: {
|
||||
in: this.id,
|
||||
tx: this.core.nextTransactionID(),
|
||||
},
|
||||
}),
|
||||
"trusting",
|
||||
this.storeKeyRevelationForMember(
|
||||
readerID,
|
||||
agent,
|
||||
newReadKey.id,
|
||||
newReadKey.secret,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are some writeOnly members we need to rotate their keys
|
||||
* and reveal them to the other non-writeOnly members
|
||||
*/
|
||||
for (const writeOnlyMemberID of writeOnlyMembers) {
|
||||
const agent = this.core.node
|
||||
.resolveAccountAgent(
|
||||
writeOnlyMemberID,
|
||||
"Expected to know writeOnly member",
|
||||
)
|
||||
._unsafeUnwrap({ withStackTrace: true });
|
||||
|
||||
const writeOnlyKey = this.core.crypto.newRandomKeySecret();
|
||||
|
||||
this.storeKeyRevelationForMember(
|
||||
writeOnlyMemberID,
|
||||
agent,
|
||||
writeOnlyKey.id,
|
||||
writeOnlyKey.secret,
|
||||
);
|
||||
this.set(`writeKeyFor_${writeOnlyMemberID}`, writeOnlyKey.id, "trusting");
|
||||
|
||||
for (const readerID of currentlyPermittedReaders) {
|
||||
const agent = this.core.node
|
||||
.resolveAccountAgent(
|
||||
readerID,
|
||||
"Expected to know currently permitted reader",
|
||||
)
|
||||
._unsafeUnwrap({ withStackTrace: true });
|
||||
|
||||
this.storeKeyRevelationForMember(
|
||||
readerID,
|
||||
agent,
|
||||
writeOnlyKey.id,
|
||||
writeOnlyKey.secret,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.set(
|
||||
`${currentReadKey.id}_for_${newReadKey.id}`,
|
||||
this.core.crypto.encryptKeySecret({
|
||||
@@ -326,8 +484,11 @@ export class RawGroup<
|
||||
|
||||
this.set("readKey", newReadKey.id, "trusting");
|
||||
|
||||
// when we rotate our readKey (because someone got kicked out), we also need to (recursively)
|
||||
// rotate the readKeys of all child groups (so they are kicked out there as well)
|
||||
/**
|
||||
* The new read key needs to be revealed to the parent groups
|
||||
*
|
||||
* This way the members from the parent groups can still have access to this group
|
||||
*/
|
||||
for (const parent of parentGroups) {
|
||||
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
||||
parent.core.getCurrentReadKey();
|
||||
@@ -426,7 +587,7 @@ export class RawGroup<
|
||||
*
|
||||
* @category 2. Role changing
|
||||
*/
|
||||
createInvite(role: "reader" | "writer" | "admin"): InviteSecret {
|
||||
createInvite(role: AccountRole): InviteSecret {
|
||||
const secretSeed = this.core.crypto.newRandomSecretSeed();
|
||||
|
||||
const inviteSecret = this.core.crypto.agentSecretFromSecretSeed(secretSeed);
|
||||
@@ -558,13 +719,20 @@ function isMorePermissiveAndShouldInherit(
|
||||
}
|
||||
|
||||
if (roleInParent === "writer") {
|
||||
return !roleInChild || roleInChild === "reader";
|
||||
return (
|
||||
!roleInChild || roleInChild === "reader" || roleInChild === "writeOnly"
|
||||
);
|
||||
}
|
||||
|
||||
if (roleInParent === "reader") {
|
||||
return !roleInChild;
|
||||
}
|
||||
|
||||
// writeOnly can't be inherited
|
||||
if (roleInParent === "writeOnly") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import type { AgentSecret } from "./crypto/crypto.js";
|
||||
import type { AgentID, RawCoID, SessionID } from "./ids.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type * as Media from "./media.js";
|
||||
import { disablePermissionErrors } from "./permissions.js";
|
||||
import type {
|
||||
IncomingSyncStream,
|
||||
OutgoingSyncQueue,
|
||||
@@ -91,6 +92,7 @@ export const cojsonInternals = {
|
||||
getPriorityFromHeader,
|
||||
getGroupDependentKeyList,
|
||||
getGroupDependentKey,
|
||||
disablePermissionErrors,
|
||||
};
|
||||
|
||||
export {
|
||||
|
||||
@@ -387,7 +387,8 @@ export class LocalNode {
|
||||
existingRole === "admin" ||
|
||||
(existingRole === "writer" && inviteRole === "writerInvite") ||
|
||||
(existingRole === "writer" && inviteRole === "reader") ||
|
||||
(existingRole === "reader" && inviteRole === "readerInvite")
|
||||
(existingRole === "reader" && inviteRole === "readerInvite") ||
|
||||
(existingRole && inviteRole === "writeOnlyInvite")
|
||||
) {
|
||||
console.debug(
|
||||
"Not accepting invite that would replace or downgrade role",
|
||||
@@ -410,7 +411,9 @@ export class LocalNode {
|
||||
? "admin"
|
||||
: inviteRole === "writerInvite"
|
||||
? "writer"
|
||||
: "reader",
|
||||
: inviteRole === "writeOnlyInvite"
|
||||
? "writeOnly"
|
||||
: "reader",
|
||||
);
|
||||
|
||||
group.core._sessionLogs = groupAsInvite.core.sessionLogs;
|
||||
|
||||
@@ -22,18 +22,33 @@ export type PermissionsDef =
|
||||
| { type: "ownedByGroup"; group: RawCoID }
|
||||
| { type: "unsafeAllowAll" };
|
||||
|
||||
export type AccountRole = "reader" | "writer" | "admin" | "writeOnly";
|
||||
|
||||
export type Role =
|
||||
| "reader"
|
||||
| "writer"
|
||||
| "admin"
|
||||
| AccountRole
|
||||
| "revoked"
|
||||
| "adminInvite"
|
||||
| "writerInvite"
|
||||
| "readerInvite";
|
||||
| "readerInvite"
|
||||
| "writeOnlyInvite";
|
||||
|
||||
type ValidTransactionsResult = { txID: TransactionID; tx: Transaction };
|
||||
type MemberState = { [agent: RawAccountID | AgentID]: Role; [EVERYONE]?: Role };
|
||||
|
||||
let logPermissionErrors = true;
|
||||
|
||||
export function disablePermissionErrors() {
|
||||
logPermissionErrors = false;
|
||||
}
|
||||
|
||||
function logPermissionError(...args: unknown[]) {
|
||||
if (logPermissionErrors === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(...args);
|
||||
}
|
||||
|
||||
export function determineValidTransactions(
|
||||
coValue: CoValueCore,
|
||||
): { txID: TransactionID; tx: Transaction }[] {
|
||||
@@ -81,7 +96,8 @@ export function determineValidTransactions(
|
||||
|
||||
if (
|
||||
transactorRoleAtTxTime !== "admin" &&
|
||||
transactorRoleAtTxTime !== "writer"
|
||||
transactorRoleAtTxTime !== "writer" &&
|
||||
transactorRoleAtTxTime !== "writeOnly"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -177,6 +193,9 @@ function determineValidTransactionsForGroup(
|
||||
const memberState: MemberState = {};
|
||||
const validTransactions: ValidTransactionsResult[] = [];
|
||||
|
||||
const keyRevelations = new Set<string>();
|
||||
const writeKeys = new Set<string>();
|
||||
|
||||
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
||||
// console.log("before", { memberState, validTransactions });
|
||||
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
||||
@@ -189,7 +208,9 @@ function determineValidTransactionsForGroup(
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
console.warn("Only admins can make private transactions in groups");
|
||||
logPermissionError(
|
||||
"Only admins can make private transactions in groups",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -199,7 +220,7 @@ function determineValidTransactionsForGroup(
|
||||
try {
|
||||
changes = parseJSON(tx.changes);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
logPermissionError(
|
||||
coValue.id,
|
||||
"Invalid JSON in transaction",
|
||||
e,
|
||||
@@ -221,18 +242,18 @@ function determineValidTransactionsForGroup(
|
||||
| MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
|
||||
|
||||
if (changes.length !== 1) {
|
||||
console.warn("Group transaction must have exactly one change");
|
||||
logPermissionError("Group transaction must have exactly one change");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.op !== "set") {
|
||||
console.warn("Group transaction must set a role or readKey");
|
||||
logPermissionError("Group transaction must set a role or readKey");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.key === "readKey") {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set readKeys");
|
||||
logPermissionError("Only admins can set readKeys");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -240,7 +261,7 @@ function determineValidTransactionsForGroup(
|
||||
continue;
|
||||
} else if (change.key === "profile") {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set profile");
|
||||
logPermissionError("Only admins can set profile");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -254,19 +275,36 @@ function determineValidTransactionsForGroup(
|
||||
memberState[transactor] !== "admin" &&
|
||||
memberState[transactor] !== "adminInvite" &&
|
||||
memberState[transactor] !== "writerInvite" &&
|
||||
memberState[transactor] !== "readerInvite"
|
||||
memberState[transactor] !== "readerInvite" &&
|
||||
memberState[transactor] !== "writeOnlyInvite"
|
||||
) {
|
||||
console.warn("Only admins can reveal keys");
|
||||
logPermissionError("Only admins can reveal keys");
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: check validity of agents who the key is revealed to?
|
||||
/**
|
||||
* We don't want to give the ability to invite members to override
|
||||
* key revelations, otherwise they could hide a key revelation to any user
|
||||
* blocking them from accessing the group.
|
||||
*/
|
||||
if (
|
||||
keyRevelations.has(change.key) &&
|
||||
memberState[transactor] !== "admin"
|
||||
) {
|
||||
logPermissionError(
|
||||
"Key revelation already exists and can't be overridden by invite",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
keyRevelations.add(change.key);
|
||||
|
||||
// TODO: check validity of agents who the key is revealed to?
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (isParentExtension(change.key)) {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set parent extensions");
|
||||
logPermissionError("Only admins can set parent extensions");
|
||||
continue;
|
||||
}
|
||||
resolveMemberStateFromParentReference(coValue, memberState, change.key);
|
||||
@@ -274,9 +312,37 @@ function determineValidTransactionsForGroup(
|
||||
continue;
|
||||
} else if (isChildExtension(change.key)) {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set child extensions");
|
||||
logPermissionError("Only admins can set child extensions");
|
||||
continue;
|
||||
}
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (isWriteKeyForMember(change.key)) {
|
||||
if (
|
||||
memberState[transactor] !== "admin" &&
|
||||
memberState[transactor] !== "writeOnlyInvite"
|
||||
) {
|
||||
logPermissionError("Only admins can set writeKeys");
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* writeOnlyInvite need to be able to set writeKeys because every new writeOnly
|
||||
* member comes with their own write key.
|
||||
*
|
||||
* We don't want to give the ability to invite members to override
|
||||
* write keys, otherwise they could hide a write key to other writeOnly users
|
||||
* blocking them from accessing the group.ß
|
||||
*/
|
||||
if (writeKeys.has(change.key) && memberState[transactor] !== "admin") {
|
||||
logPermissionError(
|
||||
"Write key already exists and can't be overridden by invite",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
writeKeys.add(change.key);
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
}
|
||||
@@ -288,12 +354,14 @@ function determineValidTransactionsForGroup(
|
||||
change.value !== "admin" &&
|
||||
change.value !== "writer" &&
|
||||
change.value !== "reader" &&
|
||||
change.value !== "writeOnly" &&
|
||||
change.value !== "revoked" &&
|
||||
change.value !== "adminInvite" &&
|
||||
change.value !== "writerInvite" &&
|
||||
change.value !== "readerInvite"
|
||||
change.value !== "readerInvite" &&
|
||||
change.value !== "writeOnlyInvite"
|
||||
) {
|
||||
console.warn("Group transaction must set a valid role");
|
||||
logPermissionError("Group transaction must set a valid role");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -305,7 +373,9 @@ function determineValidTransactionsForGroup(
|
||||
change.value === "revoked"
|
||||
)
|
||||
) {
|
||||
console.warn("Everyone can only be set to reader, writer or revoked");
|
||||
logPermissionError(
|
||||
"Everyone can only be set to reader, writer or revoked",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -323,26 +393,31 @@ function determineValidTransactionsForGroup(
|
||||
affectedMember !== transactor &&
|
||||
assignedRole !== "admin"
|
||||
) {
|
||||
console.warn("Admins can only demote themselves.");
|
||||
logPermissionError("Admins can only demote themselves.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "adminInvite") {
|
||||
if (change.value !== "admin") {
|
||||
console.warn("AdminInvites can only create admins.");
|
||||
logPermissionError("AdminInvites can only create admins.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "writerInvite") {
|
||||
if (change.value !== "writer") {
|
||||
console.warn("WriterInvites can only create writers.");
|
||||
logPermissionError("WriterInvites can only create writers.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "readerInvite") {
|
||||
if (change.value !== "reader") {
|
||||
console.warn("ReaderInvites can only create reader.");
|
||||
logPermissionError("ReaderInvites can only create reader.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "writeOnlyInvite") {
|
||||
if (change.value !== "writeOnly") {
|
||||
logPermissionError("WriteOnlyInvites can only create writeOnly.");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
logPermissionError(
|
||||
"Group transaction must be made by current admin or invite",
|
||||
);
|
||||
continue;
|
||||
@@ -377,6 +452,12 @@ function agentInAccountOrMemberInGroup(
|
||||
return transactor;
|
||||
}
|
||||
|
||||
export function isWriteKeyForMember(
|
||||
co: string,
|
||||
): co is `writeKeyFor_${RawAccountID | AgentID}` {
|
||||
return co.startsWith("writeKeyFor_");
|
||||
}
|
||||
|
||||
export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
|
||||
return co.startsWith("key_") && co.includes("_for_key");
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@ test("Can get CoMap entry values at different points in time", () => {
|
||||
expect(content.atTime(beforeB).get("hello")).toEqual("A");
|
||||
expect(content.atTime(beforeC).get("hello")).toEqual("B");
|
||||
|
||||
const ops = content.timeFilteredOps("hello");
|
||||
const ops = content.ops["hello"]!;
|
||||
|
||||
expect(content.atTime(beforeC).lastEditAt("hello")).toEqual(
|
||||
operationToEditEntry(ops![1]!),
|
||||
operationToEditEntry(ops[1]!),
|
||||
);
|
||||
expect(content.atTime(beforeC).nthEditAt("hello", 0)).toEqual(
|
||||
operationToEditEntry(ops![0]!),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { RawCoList } from "../coValues/coList.js";
|
||||
import { RawCoMap } from "../coValues/coMap.js";
|
||||
import { RawCoStream } from "../coValues/coStream.js";
|
||||
@@ -7,6 +7,7 @@ import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
||||
import { LocalNode } from "../localNode.js";
|
||||
import {
|
||||
createThreeConnectedNodes,
|
||||
createTwoConnectedNodes,
|
||||
loadCoValueOrFail,
|
||||
randomAnonymousAccountAndSessionID,
|
||||
} from "./testUtils.js";
|
||||
@@ -59,33 +60,44 @@ test("Can create a FileStream in a group", () => {
|
||||
});
|
||||
|
||||
test("Remove a member from a group where the admin role is inherited", async () => {
|
||||
const { node1, node2, node3, node1ToNode2Peer, node2ToNode3Peer } =
|
||||
createThreeConnectedNodes("server", "server", "server");
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.createGroup();
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(node2.account, "admin");
|
||||
group.addMember(node3.account, "reader");
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"admin",
|
||||
);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2, group.id);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
// The account of node2 create a child group and extend the initial group
|
||||
// This way the node1 account should become "admin" of the child group
|
||||
// by inheriting the admin role from the initial group
|
||||
const childGroup = node2.createGroup();
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Available to everyone");
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3, map.id);
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
|
||||
// Check that the sync between node2 and node3 worked
|
||||
expect(mapOnNode3.get("test")).toEqual("Available to everyone");
|
||||
|
||||
// The node1 account removes the reader from the group
|
||||
// The reader should be automatically kicked out of the child group
|
||||
await group.removeMember(node3.account);
|
||||
await group.removeMember(node3.node.account);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
@@ -97,26 +109,37 @@ test("Remove a member from a group where the admin role is inherited", async ()
|
||||
// Check that the value has not been updated on node3
|
||||
expect(mapOnNode3.get("test")).toEqual("Available to everyone");
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1, map.id);
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
|
||||
expect(mapOnNode1.get("test")).toEqual("Hidden to node3");
|
||||
});
|
||||
|
||||
test("An admin should be able to rotate the readKey on child groups and keep access to new coValues", async () => {
|
||||
const { node1, node2, node3, node1ToNode2Peer, node2ToNode1Peer } =
|
||||
createThreeConnectedNodes("server", "server", "server");
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.createGroup();
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(node2.account, "admin");
|
||||
group.addMember(node3.account, "reader");
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"admin",
|
||||
);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2, group.id);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
// The account of node2 create a child group and extend the initial group
|
||||
// This way the node1 account should become "admin" of the child group
|
||||
// by inheriting the admin role from the initial group
|
||||
const childGroup = node2.createGroup();
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
await childGroup.core.waitForSync();
|
||||
@@ -124,78 +147,349 @@ test("An admin should be able to rotate the readKey on child groups and keep acc
|
||||
// The node1 account removes the reader from the group
|
||||
// In this case we want to ensure that node1 is still able to read new coValues
|
||||
// Even if some childs are not available when the readKey is rotated
|
||||
await group.removeMember(node3.account);
|
||||
await group.removeMember(node3.node.account);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Available to node1");
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1, map.id);
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Available to node1");
|
||||
});
|
||||
|
||||
test("An admin should be able to rotate the readKey on child groups even if it was unavailable when kicking out a member from a parent group", async () => {
|
||||
const { node1, node2, node3, node1ToNode2Peer, node2ToNode1Peer } =
|
||||
createThreeConnectedNodes("server", "server", "server");
|
||||
|
||||
const group = node1.createGroup();
|
||||
|
||||
group.addMember(node2.account, "admin");
|
||||
group.addMember(node3.account, "reader");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2, group.id);
|
||||
|
||||
// The account of node2 create a child group and extend the initial group
|
||||
// This way the node1 account should become "admin" of the child group
|
||||
// by inheriting the admin role from the initial group
|
||||
const childGroup = node2.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
// The node1 account removes the reader from the group
|
||||
// In this case we want to ensure that node1 is still able to read new coValues
|
||||
// Even if some childs are not available when the readKey is rotated
|
||||
await group.removeMember(node3.account);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Available to node1");
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Available to node1");
|
||||
});
|
||||
|
||||
test("An admin should be able to rotate the readKey on child groups even if it was unavailable when kicking out a member from a parent group (grandChild)", async () => {
|
||||
const { node1, node2, node3, node1ToNode2Peer } = createThreeConnectedNodes(
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.createGroup();
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(node2.account, "admin");
|
||||
group.addMember(node3.account, "reader");
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"admin",
|
||||
);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2, group.id);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
// The account of node2 create a child group and extend the initial group
|
||||
// This way the node1 account should become "admin" of the child group
|
||||
// by inheriting the admin role from the initial group
|
||||
const childGroup = node2.createGroup();
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
const grandChildGroup = node2.createGroup();
|
||||
grandChildGroup.extend(childGroup);
|
||||
|
||||
// The node1 account removes the reader from the group
|
||||
// In this case we want to ensure that node1 is still able to read new coValues
|
||||
// Even if some childs are not available when the readKey is rotated
|
||||
await group.removeMember(node3.account);
|
||||
await group.removeMember(node3.node.account);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Available to node1");
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1, map.id);
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Available to node1");
|
||||
});
|
||||
|
||||
test("An admin should be able to rotate the readKey on child groups even if it was unavailable when kicking out a member from a parent group (grandChild)", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"admin",
|
||||
);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
// The account of node2 create a child group and extend the initial group
|
||||
// This way the node1 account should become "admin" of the child group
|
||||
// by inheriting the admin role from the initial group
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
const grandChildGroup = node2.node.createGroup();
|
||||
grandChildGroup.extend(childGroup);
|
||||
|
||||
// The node1 account removes the reader from the group
|
||||
// In this case we want to ensure that node1 is still able to read new coValues
|
||||
// Even if some childs are not available when the readKey is rotated
|
||||
await group.removeMember(node3.node.account);
|
||||
await group.core.waitForSync();
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Available to node1");
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
|
||||
expect(mapOnNode1.get("test")).toEqual("Available to node1");
|
||||
});
|
||||
|
||||
test("A user add after a key rotation should have access to the old transactions", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writer",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
const map = groupOnNode2.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
await group.removeMember(node3.node.account);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("Invites should have access to the new keys", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const invite = group.createInvite("admin");
|
||||
|
||||
await group.removeMember(node3.node.account);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("test", "Written from node1");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
await node2.node.acceptInvite(group.id, invite);
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node1");
|
||||
});
|
||||
|
||||
describe("writeOnly", () => {
|
||||
test("Admins can invite writeOnly members", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
const invite = group.createInvite("writeOnly");
|
||||
|
||||
await node2.node.acceptInvite(group.id, invite);
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
});
|
||||
|
||||
test("writeOnly roles are not inherited", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group);
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("writeOnly roles are not overridded by reader roles", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group);
|
||||
childGroup.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writeOnly");
|
||||
});
|
||||
|
||||
test("writeOnly roles are overridded by writer roles", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writer",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group);
|
||||
childGroup.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("Edits by writeOnly members are visible to other members", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
const map = groupOnNode2.createMap();
|
||||
|
||||
map.set("test", "Written from a writeOnly member");
|
||||
expect(map.get("test")).toEqual("Written from a writeOnly member");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from a writeOnly member");
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from a writeOnly member");
|
||||
});
|
||||
|
||||
test("Edits by other members are not visible to writeOnly members", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
const map = group.createMap();
|
||||
map.set("test", "Written from the admin");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("Write only member keys are rotated when a member is kicked out", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
const map = groupOnNode2.createMap();
|
||||
|
||||
map.set("test", "Written from a writeOnly member");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
await group.removeMember(node3.node.account);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
map.set("test", "Updated after key rotation");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Updated after key rotation");
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from a writeOnly member");
|
||||
});
|
||||
|
||||
test("inherited writer roles should work correctly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writer",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group);
|
||||
childGroup.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from the admin");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
// The writer role should be able to see the edits from the admin
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1574,6 +1574,330 @@ test("ReaderInvites can not invite writers", () => {
|
||||
expect(groupAsInvite.get(invitedWriterID)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("WriteOnlyInvites can not invite writers", () => {
|
||||
const { groupCore, admin } = newGroup();
|
||||
|
||||
const inviteSecret = Crypto.newRandomAgentSecret();
|
||||
const inviteID = Crypto.getAgentID(inviteSecret);
|
||||
|
||||
const group = expectGroup(groupCore.getCurrentContent());
|
||||
|
||||
const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
|
||||
const revelation = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: admin.currentSealerID()._unsafeUnwrap(),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting");
|
||||
group.set("readKey", readKeyID, "trusting");
|
||||
|
||||
group.set(inviteID, "writeOnlyInvite", "trusting");
|
||||
|
||||
expect(group.get(inviteID)).toEqual("writeOnlyInvite");
|
||||
|
||||
const revelationForInvite = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: Crypto.getAgentSealerID(inviteID),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${inviteID}`, revelationForInvite, "trusting");
|
||||
|
||||
const groupAsInvite = expectGroup(
|
||||
groupCore
|
||||
.testWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
Crypto.newRandomSessionID(inviteID),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
const invitedWriterSecret = Crypto.newRandomAgentSecret();
|
||||
const invitedWriterID = Crypto.getAgentID(invitedWriterSecret);
|
||||
|
||||
groupAsInvite.set(invitedWriterID, "writer", "trusting");
|
||||
expect(groupAsInvite.get(invitedWriterID)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("WriteOnlyInvites can not invite admins", () => {
|
||||
const { groupCore, admin } = newGroup();
|
||||
|
||||
const inviteSecret = Crypto.newRandomAgentSecret();
|
||||
const inviteID = Crypto.getAgentID(inviteSecret);
|
||||
|
||||
const group = expectGroup(groupCore.getCurrentContent());
|
||||
|
||||
const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
|
||||
const revelation = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: admin.currentSealerID()._unsafeUnwrap(),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting");
|
||||
group.set("readKey", readKeyID, "trusting");
|
||||
|
||||
group.set(inviteID, "writeOnlyInvite", "trusting");
|
||||
|
||||
expect(group.get(inviteID)).toEqual("writeOnlyInvite");
|
||||
|
||||
const revelationForInvite = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: Crypto.getAgentSealerID(inviteID),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${inviteID}`, revelationForInvite, "trusting");
|
||||
|
||||
const groupAsInvite = expectGroup(
|
||||
groupCore
|
||||
.testWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
Crypto.newRandomSessionID(inviteID),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
const invitedWriterSecret = Crypto.newRandomAgentSecret();
|
||||
const invitedWriterID = Crypto.getAgentID(invitedWriterSecret);
|
||||
|
||||
groupAsInvite.set(invitedWriterID, "admin", "trusting");
|
||||
expect(groupAsInvite.get(invitedWriterID)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("WriteOnlyInvites can invite writeOnly", () => {
|
||||
const { groupCore, admin } = newGroup();
|
||||
|
||||
const inviteSecret = Crypto.newRandomAgentSecret();
|
||||
const inviteID = Crypto.getAgentID(inviteSecret);
|
||||
|
||||
const group = expectGroup(groupCore.getCurrentContent());
|
||||
|
||||
const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
|
||||
const revelation = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: admin.currentSealerID()._unsafeUnwrap(),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting");
|
||||
group.set("readKey", readKeyID, "trusting");
|
||||
|
||||
group.set(inviteID, "writeOnlyInvite", "trusting");
|
||||
|
||||
expect(group.get(inviteID)).toEqual("writeOnlyInvite");
|
||||
|
||||
const revelationForInvite = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: Crypto.getAgentSealerID(inviteID),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${inviteID}`, revelationForInvite, "trusting");
|
||||
|
||||
const groupAsInvite = expectGroup(
|
||||
groupCore
|
||||
.testWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
Crypto.newRandomSessionID(inviteID),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
const invitedWriterSecret = Crypto.newRandomAgentSecret();
|
||||
const invitedWriterID = Crypto.getAgentID(invitedWriterSecret);
|
||||
|
||||
groupAsInvite.set(invitedWriterID, "writeOnly", "trusting");
|
||||
expect(groupAsInvite.get(invitedWriterID)).toEqual("writeOnly");
|
||||
});
|
||||
|
||||
test("WriteOnlyInvites can set writeKeys", () => {
|
||||
const { groupCore, admin } = newGroup();
|
||||
|
||||
const inviteSecret = Crypto.newRandomAgentSecret();
|
||||
const inviteID = Crypto.getAgentID(inviteSecret);
|
||||
|
||||
const group = expectGroup(groupCore.getCurrentContent());
|
||||
|
||||
const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
|
||||
const revelation = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: admin.currentSealerID()._unsafeUnwrap(),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting");
|
||||
group.set("readKey", readKeyID, "trusting");
|
||||
|
||||
group.set(inviteID, "writeOnlyInvite", "trusting");
|
||||
|
||||
expect(group.get(inviteID)).toEqual("writeOnlyInvite");
|
||||
|
||||
const revelationForInvite = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: Crypto.getAgentSealerID(inviteID),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${inviteID}`, revelationForInvite, "trusting");
|
||||
|
||||
const groupAsInvite = expectGroup(
|
||||
groupCore
|
||||
.testWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
Crypto.newRandomSessionID(inviteID),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsInvite.set(`writeKeyFor_${admin.id}`, readKeyID, "trusting");
|
||||
expect(groupAsInvite.get(`writeKeyFor_${admin.id}`)).toEqual(readKeyID);
|
||||
});
|
||||
|
||||
test("Invites can't override key revelations", () => {
|
||||
const { groupCore, admin } = newGroup();
|
||||
|
||||
const inviteSecret = Crypto.newRandomAgentSecret();
|
||||
const inviteID = Crypto.getAgentID(inviteSecret);
|
||||
|
||||
const group = expectGroup(groupCore.getCurrentContent());
|
||||
|
||||
const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
|
||||
const revelation = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: admin.currentSealerID()._unsafeUnwrap(),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting");
|
||||
group.set("readKey", readKeyID, "trusting");
|
||||
|
||||
group.set(inviteID, "readerInvite", "trusting");
|
||||
|
||||
expect(group.get(inviteID)).toEqual("readerInvite");
|
||||
|
||||
const revelationForInvite = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: Crypto.getAgentSealerID(inviteID),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${inviteID}`, revelationForInvite, "trusting");
|
||||
|
||||
const groupAsInvite = expectGroup(
|
||||
groupCore
|
||||
.testWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
Crypto.newRandomSessionID(inviteID),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsInvite.set(
|
||||
`${readKeyID}_for_${admin.id}`,
|
||||
"Evil change" as any,
|
||||
"trusting",
|
||||
);
|
||||
expect(groupAsInvite.get(`${readKeyID}_for_${admin.id}`)).toBe(revelation);
|
||||
});
|
||||
|
||||
test("WriteOnlyInvites can't override writeKeys", () => {
|
||||
const { groupCore, admin } = newGroup();
|
||||
|
||||
const inviteSecret = Crypto.newRandomAgentSecret();
|
||||
const inviteID = Crypto.getAgentID(inviteSecret);
|
||||
|
||||
const group = expectGroup(groupCore.getCurrentContent());
|
||||
|
||||
const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
|
||||
const revelation = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: admin.currentSealerID()._unsafeUnwrap(),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting");
|
||||
group.set("readKey", readKeyID, "trusting");
|
||||
|
||||
group.set(inviteID, "writeOnlyInvite", "trusting");
|
||||
|
||||
expect(group.get(inviteID)).toEqual("writeOnlyInvite");
|
||||
|
||||
const revelationForInvite = Crypto.seal({
|
||||
message: readKey,
|
||||
from: admin.currentSealerSecret(),
|
||||
to: Crypto.getAgentSealerID(inviteID),
|
||||
nOnceMaterial: {
|
||||
in: groupCore.id,
|
||||
tx: groupCore.nextTransactionID(),
|
||||
},
|
||||
});
|
||||
|
||||
group.set(`${readKeyID}_for_${inviteID}`, revelationForInvite, "trusting");
|
||||
|
||||
const groupAsInvite = expectGroup(
|
||||
groupCore
|
||||
.testWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
Crypto.newRandomSessionID(inviteID),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsInvite.set(`writeKeyFor_${admin.id}`, readKeyID, "trusting");
|
||||
groupAsInvite.set(
|
||||
`writeKeyFor_${admin.id}`,
|
||||
"Evil change" as any,
|
||||
"trusting",
|
||||
);
|
||||
expect(groupAsInvite.get(`writeKeyFor_${admin.id}`)).toEqual(readKeyID);
|
||||
});
|
||||
|
||||
test("Can give read permission to 'everyone'", () => {
|
||||
const { node, groupCore } = newGroup();
|
||||
|
||||
|
||||
@@ -67,17 +67,11 @@ export async function createTwoConnectedNodes(
|
||||
};
|
||||
}
|
||||
|
||||
export function createThreeConnectedNodes(
|
||||
export async function createThreeConnectedNodes(
|
||||
node1Role: Peer["role"],
|
||||
node2Role: Peer["role"],
|
||||
node3Role: Peer["role"],
|
||||
) {
|
||||
// Setup nodes
|
||||
const node1 = createTestNode();
|
||||
const node2 = createTestNode();
|
||||
const node3 = createTestNode();
|
||||
|
||||
// Connect nodes initially
|
||||
const [node1ToNode2Peer, node2ToNode1Peer] = connectedPeers(
|
||||
"node1ToNode2",
|
||||
"node2ToNode1",
|
||||
@@ -105,12 +99,23 @@ export function createThreeConnectedNodes(
|
||||
},
|
||||
);
|
||||
|
||||
node1.syncManager.addPeer(node1ToNode2Peer);
|
||||
node1.syncManager.addPeer(node1ToNode3Peer);
|
||||
node2.syncManager.addPeer(node2ToNode1Peer);
|
||||
node2.syncManager.addPeer(node2ToNode3Peer);
|
||||
node3.syncManager.addPeer(node3ToNode1Peer);
|
||||
node3.syncManager.addPeer(node3ToNode2Peer);
|
||||
const node1 = await LocalNode.withNewlyCreatedAccount({
|
||||
peersToLoadFrom: [node1ToNode2Peer, node1ToNode3Peer],
|
||||
crypto: Crypto,
|
||||
creationProps: { name: "Node 1" },
|
||||
});
|
||||
|
||||
const node2 = await LocalNode.withNewlyCreatedAccount({
|
||||
peersToLoadFrom: [node2ToNode1Peer, node2ToNode3Peer],
|
||||
crypto: Crypto,
|
||||
creationProps: { name: "Node 2" },
|
||||
});
|
||||
|
||||
const node3 = await LocalNode.withNewlyCreatedAccount({
|
||||
peersToLoadFrom: [node3ToNode1Peer, node3ToNode2Peer],
|
||||
crypto: Crypto,
|
||||
creationProps: { name: "Node 3" },
|
||||
});
|
||||
|
||||
return {
|
||||
node1,
|
||||
|
||||
171
packages/create-jazz-app/.gitignore
vendored
Normal file
171
packages/create-jazz-app/.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
.DS_Store
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user