Compare commits
17 Commits
cojson@0.0
...
cojson@0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a96e1118 | ||
|
|
40b4ebaf00 | ||
|
|
37559b2dec | ||
|
|
81fd3e8aff | ||
|
|
f1747e1aaf | ||
|
|
934365c24d | ||
|
|
ff20c3a260 | ||
|
|
1d0ce83019 | ||
|
|
2951d8452f | ||
|
|
6532e79790 | ||
|
|
09603d17a3 | ||
|
|
10b372da6e | ||
|
|
b556a36db3 | ||
|
|
094c505cf0 | ||
|
|
4d71ab8aac | ||
|
|
f1297c613b | ||
|
|
1ee5c2b3c8 |
104
README.md
104
README.md
@@ -1,9 +1,103 @@
|
||||
# Jazz - instant sync
|
||||
|
||||
Jazz is an open-source toolkit for telepathic data.
|
||||
Homepage: [jazz.tools](https://jazz.tools) — [Discord](https://discord.gg/utDMjHYg42)
|
||||
|
||||
Ship faster and simplify frontend, backend & devops by building with Telepathic Data.
|
||||
Get real-time multiplayer and cross-device sync for free.
|
||||
Jazz is an open-source toolkit for *permissioned telepathic data.*
|
||||
|
||||
## What is Telepathic Data?
|
||||
...
|
||||
- Ship faster & simplify your frontend and backend
|
||||
- Get cross-device sync, real-time collaboration & offline support for free
|
||||
|
||||
[Jazz Global Mesh](https://jazz.tools/mesh) is serverless sync & storage for Jazz apps. (currently free!)
|
||||
|
||||
|
||||
|
||||
## What is Permissioned Telepathic Data?
|
||||
|
||||
**Telepathic** means:
|
||||
|
||||
- **Read and write data as if it was local,** from anywhere in your app.
|
||||
- **Always have that data synced, instantly.** Across devices of the same user — or to other users (coming soon: to your backend, workers, etc.)
|
||||
|
||||
**Permissioned** means:
|
||||
|
||||
- **Fine-grained, role-based permissions are *baked into* your data.**
|
||||
- **Permissions are enforced everywhere, locally.** (using cryptography instead of through an API)
|
||||
- Roles can be changed dynamically, supporting changing teams, invite links and more.
|
||||
|
||||
## How to build an app with Jazz?
|
||||
|
||||
### Building a new app, completely with Jazz
|
||||
|
||||
It's still a bit early, but these are the rough steps:
|
||||
|
||||
1. Define your data model with [CoJSON Values](#cojson).
|
||||
2. Implement permission logic using [CoJSON Groups](#group).
|
||||
3. Hook up a user interface with [jazz-react](#jazz-react).
|
||||
|
||||
The best example is currently the [Todo List app](#example-app-todo-list).
|
||||
|
||||
### Gradually adding Jazz to an existing app
|
||||
|
||||
Coming soon: Jazz will support gradual adoption by integrating with your existing UI, auth and database.
|
||||
|
||||
## Example App: Todo List
|
||||
|
||||
The best example of Jazz is currently the Todo List app.
|
||||
|
||||
- Live version: https://example-todo.jazz.tools
|
||||
- Source code: [`./examples/todo`](./examples/todo). See the README there for a walk-through and running instructions.
|
||||
|
||||
# API Reference
|
||||
|
||||
Note: Since it's early days, this is the only source of documentation so far.
|
||||
|
||||
If you want to build something with Jazz, [join the Jazz Discord](https://discord.gg/utDMjHYg42) for encouragement and help!
|
||||
|
||||
## Overview: Main Packages
|
||||
|
||||
**`cojson`:** A library implementing abstractions and protocols for "Collaborative JSON". This will soon be standardized and forms the basis of permissioned telepathic data.
|
||||
|
||||
**`jazz-react`:** Provides you with everything you need to build react apps around CoJSON, including reactive hooks for telepathic data, local IndexedDB persistence, support for different auth providers and helpers for simple invite links for CoJSON groups.
|
||||
|
||||
### Supporting packages
|
||||
<small>
|
||||
|
||||
**`cojson-simple-sync`:**
|
||||
A generic CoJSON sync server you can run locally if you don't want to use Jazz Global Mesh (the default sync backend, at `wss://sync.jazz.tools`)
|
||||
|
||||
**`jazz-browser`:** framework-agnostic primitives that allow you to use CoJSON in the browser. Used to implement `jazz-react`, will be used to implement bindings for other frameworks in the future.
|
||||
|
||||
**`jazz-react-auth-local`** (and `jazz-browser-auth-local`): A simple auth provider that stores cryptographic keys on user devices using WebAuthentication/Passkeys. Lets you build Jazz apps completely without a backend, with end-to-end encryption by default.
|
||||
|
||||
**`jazz-storage-indexeddb`**: Provides local, offline-capable persistence. Included and enabled in `jazz-react` by default.
|
||||
</small>
|
||||
|
||||
## `CoJSON`
|
||||
|
||||
CoJSON is the core implementation of permissioned telepathic data. It provides abstractions for Collaborative JSON values ("CoValues"), groups for permission management and a protocol for syncing between nodes.
|
||||
|
||||
### `LocalNode`
|
||||
|
||||
A `LocalNode` represents a local view of a set of loaded CoValues
|
||||
|
||||
### `Group`
|
||||
|
||||
### `CoValue` & `ContentType`s
|
||||
|
||||
#### `CoMap`
|
||||
|
||||
#### `CoList` (coming soon)
|
||||
|
||||
#### `CoStram` (coming soon)
|
||||
|
||||
#### `Static` (coming soon)
|
||||
|
||||
## `jazz-react`
|
||||
|
||||
### `<WithJazz>`
|
||||
|
||||
### `useJazz()`
|
||||
|
||||
### `useTelepathicData(coID)`
|
||||
|
||||
### `useProfile(accountID)`
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/png" href="/jazz-logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Jazz Todo List Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -15,13 +15,14 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "^0.0.17",
|
||||
"jazz-react-auth-local": "^0.0.14",
|
||||
"jazz-react": "^0.1.2",
|
||||
"jazz-react-auth-local": "^0.1.2",
|
||||
"lucide-react": "^0.265.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.6"
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"uniqolor": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
|
||||
BIN
examples/todo/public/jazz-logo.png
Normal file
BIN
examples/todo/public/jazz-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -20,6 +20,7 @@ import { SubmittableInput } from "./components/SubmittableInput";
|
||||
import { createInviteLink } from "jazz-react";
|
||||
import { useToast } from "./components/ui/use-toast";
|
||||
import { Skeleton } from "./components/ui/skeleton";
|
||||
import uniqolor from "uniqolor";
|
||||
|
||||
type TaskContent = { done: boolean; text: string };
|
||||
type Task = CoMap<TaskContent>;
|
||||
@@ -38,8 +39,8 @@ function App() {
|
||||
|
||||
const createList = useCallback(
|
||||
(title: string) => {
|
||||
const listTeam = localNode.createTeam();
|
||||
const list = listTeam.createMap<TodoListContent>();
|
||||
const listGroup = localNode.createGroup();
|
||||
const list = listGroup.createMap<TodoListContent>();
|
||||
|
||||
list.edit((list) => {
|
||||
list.set("title", title);
|
||||
@@ -72,7 +73,7 @@ function App() {
|
||||
}, [localNode]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 md:pt-[30vh] pb-10 px-5">
|
||||
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
|
||||
{listId ? (
|
||||
<TodoList listId={listId} />
|
||||
) : (
|
||||
@@ -100,24 +101,18 @@ export function TodoList({ listId }: { listId: CoID<TodoList> }) {
|
||||
|
||||
const createTask = (text: string) => {
|
||||
if (!list) return;
|
||||
let task = list.coValue.getTeam().createMap<TaskContent>();
|
||||
const task = list.coValue.getGroup().createMap<TaskContent>();
|
||||
|
||||
task = task.edit((task) => {
|
||||
task.edit((task) => {
|
||||
task.set("text", text);
|
||||
task.set("done", false);
|
||||
});
|
||||
|
||||
console.log("Created task", task.id, task.toJSON());
|
||||
|
||||
const listAfter = list.edit((list) => {
|
||||
list.edit((list) => {
|
||||
list.set(task.id, true);
|
||||
});
|
||||
|
||||
console.log("Updated list", listAfter.toJSON());
|
||||
};
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<div className="max-w-full w-4xl">
|
||||
<div className="flex justify-between items-center gap-4 mb-4">
|
||||
@@ -131,25 +126,7 @@ export function TodoList({ listId }: { listId: CoID<TodoList> }) {
|
||||
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
|
||||
)}
|
||||
</h1>
|
||||
{list && list.coValue.getTeam().myRole() === "admin" && <Button
|
||||
size="sm"
|
||||
className="py-0"
|
||||
disabled={!list}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (list) {
|
||||
const inviteLink = createInviteLink(list, "writer");
|
||||
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||
toast({
|
||||
description:
|
||||
"Copied invite link to clipboard!",
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Invite
|
||||
</Button>}
|
||||
{list && <InviteButton list={list} />}
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -204,9 +181,9 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<span className={task?.get("done") ? "line-through" : ""}>
|
||||
{task?.get("text")}
|
||||
{task?.get("text") || <Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />}
|
||||
</span>
|
||||
<NameBadge accountID={task?.getLastEditor("text")} />
|
||||
</div>
|
||||
@@ -218,11 +195,56 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
|
||||
function NameBadge({ accountID }: { accountID?: AccountID }) {
|
||||
const profile = useProfile({ accountID });
|
||||
|
||||
const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
const brightColor = uniqolor(accountID || "", { lightness: 80 }).color;
|
||||
const darkColor = uniqolor(accountID || "", { lightness: 20 }).color;
|
||||
|
||||
return (
|
||||
<span className="rounded-full bg-neutral-200 dark:bg-neutral-600 py-0.5 px-2 text-xs text-neutral-500 dark:text-neutral-300">
|
||||
{profile?.get("name") || "..."}
|
||||
profile?.get("name") && <span
|
||||
className="rounded-full py-0.5 px-2 text-xs"
|
||||
style={{
|
||||
color: theme == "light" ? darkColor : brightColor,
|
||||
background: theme == "light" ? brightColor : darkColor,
|
||||
}}
|
||||
>
|
||||
{profile?.get("name")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function InviteButton({ list }: { list: TodoList }) {
|
||||
const [existingInviteLink, setExistingInviteLink] = useState<string>();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
list.coValue.getGroup().myRole() === "admin" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="py-0"
|
||||
disabled={!list}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
let inviteLink = existingInviteLink;
|
||||
if (list && !inviteLink) {
|
||||
inviteLink = createInviteLink(list, "writer");
|
||||
setExistingInviteLink(inviteLink);
|
||||
}
|
||||
if (inviteLink) {
|
||||
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||
toast({
|
||||
description: "Copied invite link to clipboard!",
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -11,9 +11,10 @@ import { Toaster } from "./components/ui/toaster.tsx";
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<div className="flex items-center gap-2 justify-center mt-5"><img src="jazz-logo.png" className="h-5"/> Jazz Todo List Example</div>
|
||||
<WithJazz
|
||||
auth={LocalAuth({
|
||||
appName: "Todo List Example",
|
||||
appName: "Jazz Todo List Example",
|
||||
Component: PrettyAuthComponent,
|
||||
})}
|
||||
syncAddress={
|
||||
|
||||
18
packages/cojson-simple-sync/.eslintrc.cjs
Normal file
18
packages/cojson-simple-sync/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
},
|
||||
|
||||
};
|
||||
173
packages/cojson-simple-sync/.gitignore
vendored
Normal file
173
packages/cojson-simple-sync/.gitignore
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
# 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
|
||||
|
||||
out
|
||||
34
packages/cojson-simple-sync/package.json
Normal file
34
packages/cojson-simple-sync/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "cojson-simple-sync",
|
||||
"module": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.1",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"eslint": "^8.46.0",
|
||||
"jest": "^29.6.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist && npm run add-shebang && chmod +x ./dist/index.js",
|
||||
"add-shebang": "echo \"#!/usr/bin/env node\" | cat - ./dist/index.js > /tmp/out && mv /tmp/out ./dist/index.js",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"bin": "./dist/index.js",
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
58
packages/cojson-simple-sync/src/index.ts
Normal file
58
packages/cojson-simple-sync/src/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { AnonymousControlledAccount, LocalNode, cojsonInternals } from "cojson";
|
||||
import { WebSocketServer, createWebSocketStream } from "ws";
|
||||
import { Duplex } from "node:stream";
|
||||
import { TransformStream } from "node:stream/web"
|
||||
|
||||
const wss = new WebSocketServer({ port: 4200 });
|
||||
|
||||
console.log("COJSON sync server listening on port " + wss.options.port)
|
||||
|
||||
const agentSecret = cojsonInternals.newRandomAgentSecret();
|
||||
const agentID = cojsonInternals.getAgentID(agentSecret);
|
||||
|
||||
const localNode = new LocalNode(
|
||||
new AnonymousControlledAccount(agentSecret),
|
||||
cojsonInternals.newRandomSessionID(agentID)
|
||||
);
|
||||
|
||||
wss.on("connection", function connection(ws, req) {
|
||||
const duplexStream = createWebSocketStream(ws, {
|
||||
decodeStrings: false,
|
||||
readableObjectMode: true,
|
||||
writableObjectMode: true,
|
||||
encoding: "utf-8",
|
||||
defaultEncoding: "utf-8",
|
||||
});
|
||||
|
||||
const { readable: incomingStrings, writable: outgoingStrings } = Duplex.toWeb(duplexStream);
|
||||
|
||||
const toJSON = new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
controller.enqueue(JSON.parse(chunk));
|
||||
}
|
||||
})
|
||||
|
||||
const fromJSON = new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
controller.enqueue(JSON.stringify(chunk));
|
||||
}
|
||||
});
|
||||
|
||||
const clientAddress =
|
||||
(req.headers["x-forwarded-for"] as string | undefined)
|
||||
?.split(",")[0]
|
||||
?.trim() || req.socket.remoteAddress;
|
||||
|
||||
const clientId = clientAddress + "@" + new Date().toISOString();
|
||||
|
||||
localNode.sync.addPeer({
|
||||
id: clientId,
|
||||
role: "client",
|
||||
incoming: incomingStrings.pipeThrough(toJSON),
|
||||
outgoing: fromJSON.writable,
|
||||
});
|
||||
|
||||
void fromJSON.readable.pipeTo(outgoingStrings);
|
||||
|
||||
ws.on("error", (e) => console.error(`Error on connection ${clientId}:`, e));
|
||||
});
|
||||
19
packages/cojson-simple-sync/tsconfig.json
Normal file
19
packages/cojson-simple-sync/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
}
|
||||
2801
packages/cojson-simple-sync/yarn.lock
Normal file
2801
packages/cojson-simple-sync/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,53 +1,3 @@
|
||||
# CoJSON
|
||||
|
||||
CoJSON ("Collaborative JSON") will be a minimal protocol and implementation for collaborative values (CRDTs + public-key cryptography).
|
||||
|
||||
CoJSON is developed by [Garden Computing](https://gcmp.io) as the underpinnings of [Jazz](https://jazz.tools), a framework for building apps with telepathic data.
|
||||
|
||||
The protocol and implementation will cover:
|
||||
|
||||
- how to represent collaborative values internally
|
||||
- the APIs collaborative values expose
|
||||
- how to sync and query for collaborative values between peers
|
||||
- how to enforce access rights within collaborative values locally and at sync boundaries
|
||||
|
||||
THIS IS WORK IN PROGRESS
|
||||
|
||||
## Core Value Types
|
||||
|
||||
### `Immutable` Values (JSON)
|
||||
- null
|
||||
- boolean
|
||||
- number
|
||||
- string
|
||||
- stringly-encoded CoJSON identifiers & data (`CoID`, `AgentID`, `SessionID`, `SignerID`, `SignerSecret`, `Signature`, `SealerID`, `SealerSecret`, `Sealed`, `Hash`, `ShortHash`, `KeySecret`, `KeyID`, `Encrypted`, `Role`)
|
||||
|
||||
- array
|
||||
- object
|
||||
|
||||
### `Collaborative` Values
|
||||
- CoMap (`string` → `Immutable`, last-writer-wins per key)
|
||||
- Team (`AgentID` → `Role`)
|
||||
- CoList (`Immutable[]`, addressable positions, insertAfter semantics)
|
||||
- Agent (`{signerID, sealerID}[]`)
|
||||
- CoStream (independent per-session streams of `Immutable`s)
|
||||
- Static (single addressable `Immutable`)
|
||||
|
||||
## Implementation Abstractions
|
||||
- CoValue
|
||||
- Session Logs
|
||||
- Transactions
|
||||
- Private (encrypted) transactions
|
||||
- Trusting (unencrypted) transactions
|
||||
- Rulesets
|
||||
- CoValue Content Types
|
||||
- LocalNode
|
||||
- Peers
|
||||
- AgentCredentials
|
||||
- Peer
|
||||
|
||||
## Extensions & higher-level protocols
|
||||
|
||||
### More complex datastructures
|
||||
- CoText: a clean way to collaboratively mark up rich text with CoJSON
|
||||
- CoJSON Tree: a clean way to represent collaborative tree structures with CoJSON
|
||||
[See the top-level README](../../README.md#cojson)
|
||||
@@ -5,7 +5,7 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.0.24",
|
||||
"version": "0.1.2",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
@@ -19,14 +19,14 @@ test("Can create a node while creating a new account with profile", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("A node with an account can create teams and and objects within them", async () => {
|
||||
test("A node with an account can create groups and and objects within them", async () => {
|
||||
const { node, accountID } =
|
||||
LocalNode.withNewlyCreatedAccount("Hermes Puggington");
|
||||
|
||||
const team = await node.createTeam();
|
||||
expect(team).not.toBeNull();
|
||||
const group = await node.createGroup();
|
||||
expect(group).not.toBeNull();
|
||||
|
||||
let map = team.createMap();
|
||||
let map = group.createMap();
|
||||
map = map.edit((edit) => {
|
||||
edit.set("foo", "bar", "private");
|
||||
expect(edit.get("foo")).toEqual("bar");
|
||||
@@ -41,10 +41,10 @@ test("Can create account with one node, and then load it on another", async () =
|
||||
const { node, accountID, accountSecret } =
|
||||
LocalNode.withNewlyCreatedAccount("Hermes Puggington");
|
||||
|
||||
const team = await node.createTeam();
|
||||
expect(team).not.toBeNull();
|
||||
const group = await node.createGroup();
|
||||
expect(group).not.toBeNull();
|
||||
|
||||
let map = team.createMap();
|
||||
let map = group.createMap();
|
||||
map = map.edit((edit) => {
|
||||
edit.set("foo", "bar", "private");
|
||||
expect(edit.get("foo")).toEqual("bar");
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "./crypto.js";
|
||||
import { AgentID } from "./ids.js";
|
||||
import { CoMap, LocalNode } from "./index.js";
|
||||
import { Team, TeamContent } from "./team.js";
|
||||
import { Group, GroupContent } from "./group.js";
|
||||
|
||||
export function accountHeaderForInitialAgentSecret(
|
||||
agentSecret: AgentSecret
|
||||
@@ -22,7 +22,7 @@ export function accountHeaderForInitialAgentSecret(
|
||||
const agent = getAgentID(agentSecret);
|
||||
return {
|
||||
type: "comap",
|
||||
ruleset: { type: "team", initialAdmin: agent },
|
||||
ruleset: { type: "group", initialAdmin: agent },
|
||||
meta: {
|
||||
type: "account",
|
||||
},
|
||||
@@ -31,13 +31,13 @@ export function accountHeaderForInitialAgentSecret(
|
||||
};
|
||||
}
|
||||
|
||||
export class Account extends Team {
|
||||
export class Account extends Group {
|
||||
get id(): AccountID {
|
||||
return this.teamMap.id as AccountID;
|
||||
return this.groupMap.id as AccountID;
|
||||
}
|
||||
|
||||
getCurrentAgentID(): AgentID {
|
||||
const agents = this.teamMap
|
||||
const agents = this.groupMap
|
||||
.keys()
|
||||
.filter((k): k is AgentID => k.startsWith("sealer_"));
|
||||
|
||||
@@ -70,10 +70,10 @@ export class ControlledAccount
|
||||
|
||||
constructor(
|
||||
agentSecret: AgentSecret,
|
||||
teamMap: CoMap<AccountContent, AccountMeta>,
|
||||
groupMap: CoMap<AccountContent, AccountMeta>,
|
||||
node: LocalNode
|
||||
) {
|
||||
super(teamMap, node);
|
||||
super(groupMap, node);
|
||||
|
||||
this.agentSecret = agentSecret;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export class AnonymousControlledAccount
|
||||
}
|
||||
}
|
||||
|
||||
export type AccountContent = TeamContent & { profile: CoID<Profile> };
|
||||
export type AccountContent = GroupContent & { profile: CoID<Profile> };
|
||||
export type AccountMeta = { type: "account" };
|
||||
export type AccountID = CoID<CoMap<AccountContent, AccountMeta>>;
|
||||
|
||||
|
||||
@@ -125,17 +125,17 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("New transactions in a team correctly update owned values, including subscriptions", async () => {
|
||||
test("New transactions in a group correctly update owned values, including subscriptions", async () => {
|
||||
const [account, sessionID] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(account, sessionID);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const timeBeforeEdit = Date.now();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
let map = team.createMap();
|
||||
let map = group.createMap();
|
||||
|
||||
let mapAfterEdit = map.edit((map) => {
|
||||
map.set("hello", "world");
|
||||
@@ -159,7 +159,7 @@ test("New transactions in a team correctly update owned values, including subscr
|
||||
]
|
||||
} satisfies Transaction;
|
||||
|
||||
const { expectedNewHash } = team.teamMap.coValue.expectedNewHashAfter(sessionID, [
|
||||
const { expectedNewHash } = group.groupMap.coValue.expectedNewHashAfter(sessionID, [
|
||||
resignationThatWeJustLearnedAbout,
|
||||
]);
|
||||
|
||||
@@ -170,7 +170,7 @@ test("New transactions in a team correctly update owned values, including subscr
|
||||
|
||||
expect(map.coValue.getValidSortedTransactions().length).toBe(1);
|
||||
|
||||
const manuallyAdddedTxSuccess = team.teamMap.coValue.tryAddTransactions(node.ownSessionID, [resignationThatWeJustLearnedAbout], expectedNewHash, signature);
|
||||
const manuallyAdddedTxSuccess = group.groupMap.coValue.tryAddTransactions(node.ownSessionID, [resignationThatWeJustLearnedAbout], expectedNewHash, signature);
|
||||
|
||||
expect(manuallyAdddedTxSuccess).toBe(true);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
determineValidTransactions,
|
||||
isKeyForKeyField,
|
||||
} from "./permissions.js";
|
||||
import { Team, expectTeamContent } from "./team.js";
|
||||
import { Group, expectGroupContent } from "./group.js";
|
||||
import { LocalNode } from "./node.js";
|
||||
import { CoValueKnownState, NewContentMessage } from "./sync.js";
|
||||
import { RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||
@@ -92,6 +92,8 @@ export type DecryptedTransaction = {
|
||||
madeAt: number;
|
||||
};
|
||||
|
||||
const readKeyCache = new WeakMap<CoValue, { [id: KeyID]: KeySecret }>();
|
||||
|
||||
export class CoValue {
|
||||
id: RawCoID;
|
||||
node: LocalNode;
|
||||
@@ -100,16 +102,20 @@ export class CoValue {
|
||||
_cachedContent?: ContentType;
|
||||
listeners: Set<(content?: ContentType) => void> = new Set();
|
||||
|
||||
constructor(header: CoValueHeader, node: LocalNode, internalInitSessions: { [key: SessionID]: SessionLog } = {}) {
|
||||
constructor(
|
||||
header: CoValueHeader,
|
||||
node: LocalNode,
|
||||
internalInitSessions: { [key: SessionID]: SessionLog } = {}
|
||||
) {
|
||||
this.id = idforHeader(header);
|
||||
this.header = header;
|
||||
this._sessions = internalInitSessions;
|
||||
this.node = node;
|
||||
|
||||
if (header.ruleset.type == "ownedByTeam") {
|
||||
if (header.ruleset.type == "ownedByGroup") {
|
||||
this.node
|
||||
.expectCoValueLoaded(header.ruleset.team)
|
||||
.subscribe((_teamUpdate) => {
|
||||
.expectCoValueLoaded(header.ruleset.group)
|
||||
.subscribe((_groupUpdate) => {
|
||||
this._cachedContent = undefined;
|
||||
const newContent = this.getCurrentContent();
|
||||
for (const listener of this.listeners) {
|
||||
@@ -385,8 +391,8 @@ export class CoValue {
|
||||
}
|
||||
|
||||
getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
|
||||
if (this.header.ruleset.type === "team") {
|
||||
const content = expectTeamContent(this.getCurrentContent());
|
||||
if (this.header.ruleset.type === "group") {
|
||||
const content = expectGroupContent(this.getCurrentContent());
|
||||
|
||||
const currentKeyId = content.get("readKey");
|
||||
|
||||
@@ -400,20 +406,23 @@ export class CoValue {
|
||||
secret: secret,
|
||||
id: currentKeyId,
|
||||
};
|
||||
} else if (this.header.ruleset.type === "ownedByTeam") {
|
||||
} else if (this.header.ruleset.type === "ownedByGroup") {
|
||||
return this.node
|
||||
.expectCoValueLoaded(this.header.ruleset.team)
|
||||
.expectCoValueLoaded(this.header.ruleset.group)
|
||||
.getCurrentReadKey();
|
||||
} else {
|
||||
throw new Error(
|
||||
"Only teams or values owned by teams have read secrets"
|
||||
"Only groups or values owned by groups have read secrets"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getReadKey(keyID: KeyID): KeySecret | undefined {
|
||||
if (this.header.ruleset.type === "team") {
|
||||
const content = expectTeamContent(this.getCurrentContent());
|
||||
if (readKeyCache.get(this)?.[keyID]) {
|
||||
return readKeyCache.get(this)?.[keyID];
|
||||
}
|
||||
if (this.header.ruleset.type === "group") {
|
||||
const content = expectGroupContent(this.getCurrentContent());
|
||||
|
||||
// Try to find key revelation for us
|
||||
|
||||
@@ -440,7 +449,16 @@ export class CoValue {
|
||||
}
|
||||
);
|
||||
|
||||
if (secret) return secret as KeySecret;
|
||||
if (secret) {
|
||||
let cache = readKeyCache.get(this);
|
||||
if (!cache) {
|
||||
cache = {};
|
||||
readKeyCache.set(this, cache);
|
||||
}
|
||||
cache[keyID] = secret;
|
||||
|
||||
return secret as KeySecret;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find indirect revelation through previousKeys
|
||||
@@ -467,7 +485,14 @@ export class CoValue {
|
||||
);
|
||||
|
||||
if (secret) {
|
||||
return secret;
|
||||
let cache = readKeyCache.get(this);
|
||||
if (!cache) {
|
||||
cache = {};
|
||||
readKeyCache.set(this, cache);
|
||||
}
|
||||
cache[keyID] = secret;
|
||||
|
||||
return secret as KeySecret;
|
||||
} else {
|
||||
console.error(
|
||||
`Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}`
|
||||
@@ -477,26 +502,26 @@ export class CoValue {
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} else if (this.header.ruleset.type === "ownedByTeam") {
|
||||
} else if (this.header.ruleset.type === "ownedByGroup") {
|
||||
return this.node
|
||||
.expectCoValueLoaded(this.header.ruleset.team)
|
||||
.expectCoValueLoaded(this.header.ruleset.group)
|
||||
.getReadKey(keyID);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Only teams or values owned by teams have read secrets"
|
||||
"Only groups or values owned by groups have read secrets"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getTeam(): Team {
|
||||
if (this.header.ruleset.type !== "ownedByTeam") {
|
||||
throw new Error("Only values owned by teams have teams");
|
||||
getGroup(): Group {
|
||||
if (this.header.ruleset.type !== "ownedByGroup") {
|
||||
throw new Error("Only values owned by groups have groups");
|
||||
}
|
||||
|
||||
return new Team(
|
||||
expectTeamContent(
|
||||
return new Group(
|
||||
expectGroupContent(
|
||||
this.node
|
||||
.expectCoValueLoaded(this.header.ruleset.team)
|
||||
.expectCoValueLoaded(this.header.ruleset.group)
|
||||
.getCurrentContent()
|
||||
),
|
||||
this.node
|
||||
@@ -553,12 +578,12 @@ export class CoValue {
|
||||
}
|
||||
|
||||
getDependedOnCoValues(): RawCoID[] {
|
||||
return this.header.ruleset.type === "team"
|
||||
? expectTeamContent(this.getCurrentContent())
|
||||
return this.header.ruleset.type === "group"
|
||||
? expectGroupContent(this.getCurrentContent())
|
||||
.keys()
|
||||
.filter((k): k is AccountID => k.startsWith("co_"))
|
||||
: this.header.ruleset.type === "ownedByTeam"
|
||||
? [this.header.ruleset.team]
|
||||
: this.header.ruleset.type === "ownedByGroup"
|
||||
? [this.header.ruleset.group]
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { Role } from "./permissions.js";
|
||||
import { base58 } from "@scure/base";
|
||||
|
||||
export type TeamContent = {
|
||||
export type GroupContent = {
|
||||
profile: CoID<Profile> | null;
|
||||
[key: AccountIDOrAgentID]: Role;
|
||||
readKey: KeyID;
|
||||
@@ -36,34 +36,34 @@ export type TeamContent = {
|
||||
>;
|
||||
};
|
||||
|
||||
export function expectTeamContent(
|
||||
export function expectGroupContent(
|
||||
content: ContentType
|
||||
): CoMap<TeamContent, JsonObject | null> {
|
||||
): CoMap<GroupContent, JsonObject | null> {
|
||||
if (content.type !== "comap") {
|
||||
throw new Error("Expected map");
|
||||
}
|
||||
|
||||
return content as CoMap<TeamContent, JsonObject | null>;
|
||||
return content as CoMap<GroupContent, JsonObject | null>;
|
||||
}
|
||||
|
||||
export class Team {
|
||||
teamMap: CoMap<TeamContent, JsonObject | null>;
|
||||
export class Group {
|
||||
groupMap: CoMap<GroupContent, JsonObject | null>;
|
||||
node: LocalNode;
|
||||
|
||||
constructor(
|
||||
teamMap: CoMap<TeamContent, JsonObject | null>,
|
||||
groupMap: CoMap<GroupContent, JsonObject | null>,
|
||||
node: LocalNode
|
||||
) {
|
||||
this.teamMap = teamMap;
|
||||
this.groupMap = groupMap;
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
get id(): CoID<CoMap<TeamContent, JsonObject | null>> {
|
||||
return this.teamMap.id;
|
||||
get id(): CoID<CoMap<GroupContent, JsonObject | null>> {
|
||||
return this.groupMap.id;
|
||||
}
|
||||
|
||||
roleOf(accountID: AccountIDOrAgentID): Role | undefined {
|
||||
return this.teamMap.get(accountID);
|
||||
return this.groupMap.get(accountID);
|
||||
}
|
||||
|
||||
myRole(): Role | undefined {
|
||||
@@ -71,8 +71,8 @@ export class Team {
|
||||
}
|
||||
|
||||
addMember(accountID: AccountIDOrAgentID, role: Role) {
|
||||
this.teamMap = this.teamMap.edit((map) => {
|
||||
const currentReadKey = this.teamMap.coValue.getCurrentReadKey();
|
||||
this.groupMap = this.groupMap.edit((map) => {
|
||||
const currentReadKey = this.groupMap.coValue.getCurrentReadKey();
|
||||
|
||||
if (!currentReadKey.secret) {
|
||||
throw new Error("Can't add member without read key secret");
|
||||
@@ -80,7 +80,7 @@ export class Team {
|
||||
|
||||
const agent = this.node.resolveAccountAgent(
|
||||
accountID,
|
||||
"Expected to know agent to add them to team"
|
||||
"Expected to know agent to add them to group"
|
||||
);
|
||||
|
||||
map.set(accountID, role, "trusting");
|
||||
@@ -93,11 +93,11 @@ export class Team {
|
||||
`${currentReadKey.id}_for_${accountID}`,
|
||||
seal(
|
||||
currentReadKey.secret,
|
||||
this.teamMap.coValue.node.account.currentSealerSecret(),
|
||||
this.groupMap.coValue.node.account.currentSealerSecret(),
|
||||
getAgentSealerID(agent),
|
||||
{
|
||||
in: this.teamMap.coValue.id,
|
||||
tx: this.teamMap.coValue.nextTransactionID(),
|
||||
in: this.groupMap.coValue.id,
|
||||
tx: this.groupMap.coValue.nextTransactionID(),
|
||||
}
|
||||
),
|
||||
"trusting"
|
||||
@@ -117,9 +117,9 @@ export class Team {
|
||||
}
|
||||
|
||||
rotateReadKey() {
|
||||
const currentlyPermittedReaders = this.teamMap.keys().filter((key) => {
|
||||
const currentlyPermittedReaders = this.groupMap.keys().filter((key) => {
|
||||
if (key.startsWith("co_") || isAgentID(key)) {
|
||||
const role = this.teamMap.get(key);
|
||||
const role = this.groupMap.get(key);
|
||||
return (
|
||||
role === "admin" || role === "writer" || role === "reader"
|
||||
);
|
||||
@@ -128,7 +128,7 @@ export class Team {
|
||||
}
|
||||
}) as AccountIDOrAgentID[];
|
||||
|
||||
const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey();
|
||||
const maybeCurrentReadKey = this.groupMap.coValue.getCurrentReadKey();
|
||||
|
||||
if (!maybeCurrentReadKey.secret) {
|
||||
throw new Error(
|
||||
@@ -143,7 +143,7 @@ export class Team {
|
||||
|
||||
const newReadKey = newRandomKeySecret();
|
||||
|
||||
this.teamMap = this.teamMap.edit((map) => {
|
||||
this.groupMap = this.groupMap.edit((map) => {
|
||||
for (const readerID of currentlyPermittedReaders) {
|
||||
const reader = this.node.resolveAccountAgent(
|
||||
readerID,
|
||||
@@ -154,11 +154,11 @@ export class Team {
|
||||
`${newReadKey.id}_for_${readerID}`,
|
||||
seal(
|
||||
newReadKey.secret,
|
||||
this.teamMap.coValue.node.account.currentSealerSecret(),
|
||||
this.groupMap.coValue.node.account.currentSealerSecret(),
|
||||
getAgentSealerID(reader),
|
||||
{
|
||||
in: this.teamMap.coValue.id,
|
||||
tx: this.teamMap.coValue.nextTransactionID(),
|
||||
in: this.groupMap.coValue.id,
|
||||
tx: this.groupMap.coValue.nextTransactionID(),
|
||||
}
|
||||
),
|
||||
"trusting"
|
||||
@@ -179,7 +179,7 @@ export class Team {
|
||||
}
|
||||
|
||||
removeMember(accountID: AccountIDOrAgentID) {
|
||||
this.teamMap = this.teamMap.edit((map) => {
|
||||
this.groupMap = this.groupMap.edit((map) => {
|
||||
map.set(accountID, "revoked", "trusting");
|
||||
});
|
||||
|
||||
@@ -194,8 +194,8 @@ export class Team {
|
||||
.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: {
|
||||
type: "ownedByTeam",
|
||||
team: this.teamMap.id,
|
||||
type: "ownedByGroup",
|
||||
group: this.groupMap.id,
|
||||
},
|
||||
meta: meta || null,
|
||||
...createdNowUnique(),
|
||||
@@ -206,10 +206,10 @@ export class Team {
|
||||
testWithDifferentAccount(
|
||||
account: GeneralizedControlledAccount,
|
||||
sessionId: SessionID
|
||||
): Team {
|
||||
return new Team(
|
||||
expectTeamContent(
|
||||
this.teamMap.coValue
|
||||
): Group {
|
||||
return new Group(
|
||||
expectGroupContent(
|
||||
this.groupMap.coValue
|
||||
.testWithDifferentAccount(account, sessionId)
|
||||
.getCurrentContent()
|
||||
),
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { connectedPeers } from "./streamUtils.js";
|
||||
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
||||
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
||||
import { Team, expectTeamContent } from "./team.js"
|
||||
import { Group, expectGroupContent } from "./group.js"
|
||||
|
||||
import type { SessionID, AgentID } from "./ids.js";
|
||||
import type { CoID, ContentType } from "./contentType.js";
|
||||
@@ -27,7 +27,7 @@ import type {
|
||||
ProfileContent,
|
||||
Profile,
|
||||
} from "./account.js";
|
||||
import type { InviteSecret } from "./team.js";
|
||||
import type { InviteSecret } from "./group.js";
|
||||
|
||||
type Value = JsonValue | ContentType;
|
||||
|
||||
@@ -44,7 +44,7 @@ export const cojsonInternals = {
|
||||
agentSecretFromSecretSeed,
|
||||
secretSeedLength,
|
||||
shortHashLength,
|
||||
expectTeamContent
|
||||
expectGroupContent
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -53,7 +53,7 @@ export {
|
||||
CoMap,
|
||||
AnonymousControlledAccount,
|
||||
ControlledAccount,
|
||||
Team
|
||||
Group
|
||||
};
|
||||
|
||||
export type {
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js";
|
||||
import {
|
||||
InviteSecret,
|
||||
Team,
|
||||
TeamContent,
|
||||
expectTeamContent,
|
||||
Group,
|
||||
GroupContent,
|
||||
expectGroupContent,
|
||||
secretSeedFromInviteSecret,
|
||||
} from "./team.js";
|
||||
} from "./group.js";
|
||||
import { Peer, SyncManager } from "./sync.js";
|
||||
import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
|
||||
import { CoID, ContentType } from "./contentType.js";
|
||||
@@ -151,32 +151,32 @@ export class LocalNode {
|
||||
}
|
||||
|
||||
async acceptInvite<T extends ContentType>(
|
||||
teamOrOwnedValueID: CoID<T>,
|
||||
groupOrOwnedValueID: CoID<T>,
|
||||
inviteSecret: InviteSecret
|
||||
): Promise<void> {
|
||||
const teamOrOwnedValue = await this.load(teamOrOwnedValueID);
|
||||
const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
|
||||
|
||||
if (teamOrOwnedValue.coValue.header.ruleset.type === "ownedByTeam") {
|
||||
if (groupOrOwnedValue.coValue.header.ruleset.type === "ownedByGroup") {
|
||||
return this.acceptInvite(
|
||||
teamOrOwnedValue.coValue.header.ruleset.team as CoID<
|
||||
CoMap<TeamContent>
|
||||
groupOrOwnedValue.coValue.header.ruleset.group as CoID<
|
||||
CoMap<GroupContent>
|
||||
>,
|
||||
inviteSecret
|
||||
);
|
||||
} else if (teamOrOwnedValue.coValue.header.ruleset.type !== "team") {
|
||||
throw new Error("Can only accept invites to teams");
|
||||
} else if (groupOrOwnedValue.coValue.header.ruleset.type !== "group") {
|
||||
throw new Error("Can only accept invites to groups");
|
||||
}
|
||||
|
||||
const team = new Team(expectTeamContent(teamOrOwnedValue), this);
|
||||
const group = new Group(expectGroupContent(groupOrOwnedValue), this);
|
||||
|
||||
const inviteAgentSecret = agentSecretFromSecretSeed(
|
||||
secretSeedFromInviteSecret(inviteSecret)
|
||||
);
|
||||
const inviteAgentID = getAgentID(inviteAgentSecret);
|
||||
|
||||
const invitationRole = await new Promise((resolve, reject) => {
|
||||
team.teamMap.subscribe((teamMap) => {
|
||||
const role = teamMap.get(inviteAgentID);
|
||||
const inviteRole = await new Promise((resolve, reject) => {
|
||||
group.groupMap.subscribe((groupMap) => {
|
||||
const role = groupMap.get(inviteAgentID);
|
||||
if (role) {
|
||||
resolve(role);
|
||||
}
|
||||
@@ -184,45 +184,47 @@ export class LocalNode {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error("Couldn't find invitation before timeout")
|
||||
new Error("Couldn't find invite before timeout")
|
||||
),
|
||||
1000
|
||||
);
|
||||
});
|
||||
|
||||
if (!invitationRole) {
|
||||
throw new Error("No invitation found");
|
||||
if (!inviteRole) {
|
||||
throw new Error("No invite found");
|
||||
}
|
||||
|
||||
const existingRole = team.teamMap.get(this.account.id);
|
||||
const existingRole = group.groupMap.get(this.account.id);
|
||||
|
||||
if (
|
||||
existingRole === "admin" ||
|
||||
(existingRole === "writer" && invitationRole === "reader")
|
||||
(existingRole === "writer" && inviteRole === "writerInvite") ||
|
||||
(existingRole === "writer" && inviteRole === "reader") ||
|
||||
(existingRole === "reader" && inviteRole === "readerInvite")
|
||||
) {
|
||||
console.debug("Not accepting invite that would downgrade role");
|
||||
console.debug("Not accepting invite that would replace or downgrade role");
|
||||
return;
|
||||
}
|
||||
|
||||
const teamAsInvite = team.testWithDifferentAccount(
|
||||
const groupAsInvite = group.testWithDifferentAccount(
|
||||
new AnonymousControlledAccount(inviteAgentSecret),
|
||||
newRandomSessionID(inviteAgentID)
|
||||
);
|
||||
|
||||
teamAsInvite.addMember(
|
||||
groupAsInvite.addMember(
|
||||
this.account.id,
|
||||
invitationRole === "adminInvite"
|
||||
inviteRole === "adminInvite"
|
||||
? "admin"
|
||||
: invitationRole === "writerInvite"
|
||||
: inviteRole === "writerInvite"
|
||||
? "writer"
|
||||
: "reader"
|
||||
);
|
||||
|
||||
team.teamMap.coValue._sessions = teamAsInvite.teamMap.coValue.sessions;
|
||||
team.teamMap.coValue._cachedContent = undefined;
|
||||
group.groupMap.coValue._sessions = groupAsInvite.groupMap.coValue.sessions;
|
||||
group.groupMap.coValue._cachedContent = undefined;
|
||||
|
||||
for (const teamListener of team.teamMap.coValue.listeners) {
|
||||
teamListener(team.teamMap.coValue.getCurrentContent());
|
||||
for (const groupListener of group.groupMap.coValue.listeners) {
|
||||
groupListener(group.groupMap.coValue.getCurrentContent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +247,7 @@ export class LocalNode {
|
||||
|
||||
expectProfileLoaded(id: AccountID, expectation?: string): Profile {
|
||||
const account = this.expectCoValueLoaded(id, expectation);
|
||||
const profileID = expectTeamContent(account.getCurrentContent()).get(
|
||||
const profileID = expectGroupContent(account.getCurrentContent()).get(
|
||||
"profile"
|
||||
);
|
||||
if (!profileID) {
|
||||
@@ -272,12 +274,12 @@ export class LocalNode {
|
||||
newRandomSessionID(getAgentID(agentSecret))
|
||||
);
|
||||
|
||||
const accountAsTeam = new Team(
|
||||
expectTeamContent(account.getCurrentContent()),
|
||||
const accountAsGroup = new Group(
|
||||
expectGroupContent(account.getCurrentContent()),
|
||||
account.node
|
||||
);
|
||||
|
||||
accountAsTeam.teamMap.edit((editable) => {
|
||||
accountAsGroup.groupMap.edit((editable) => {
|
||||
editable.set(getAgentID(agentSecret), "admin", "trusting");
|
||||
|
||||
const readKey = newRandomKeySecret();
|
||||
@@ -305,7 +307,7 @@ export class LocalNode {
|
||||
account.node
|
||||
);
|
||||
|
||||
const profile = accountAsTeam.createMap<ProfileContent, ProfileMeta>({
|
||||
const profile = accountAsGroup.createMap<ProfileContent, ProfileMeta>({
|
||||
type: "profile",
|
||||
});
|
||||
|
||||
@@ -313,13 +315,13 @@ export class LocalNode {
|
||||
editable.set("name", name, "trusting");
|
||||
});
|
||||
|
||||
accountAsTeam.teamMap.edit((editable) => {
|
||||
accountAsGroup.groupMap.edit((editable) => {
|
||||
editable.set("profile", profile.id, "trusting");
|
||||
});
|
||||
|
||||
const accountOnThisNode = this.expectCoValueLoaded(account.id);
|
||||
|
||||
accountOnThisNode._sessions = {...accountAsTeam.teamMap.coValue.sessions};
|
||||
accountOnThisNode._sessions = {...accountAsGroup.groupMap.coValue.sessions};
|
||||
accountOnThisNode._cachedContent = undefined;
|
||||
|
||||
return controlledAccount;
|
||||
@@ -334,7 +336,7 @@ export class LocalNode {
|
||||
|
||||
if (
|
||||
coValue.header.type !== "comap" ||
|
||||
coValue.header.ruleset.type !== "team" ||
|
||||
coValue.header.ruleset.type !== "group" ||
|
||||
!coValue.header.meta ||
|
||||
!("type" in coValue.header.meta) ||
|
||||
coValue.header.meta.type !== "account"
|
||||
@@ -347,22 +349,22 @@ export class LocalNode {
|
||||
}
|
||||
|
||||
return new Account(
|
||||
coValue.getCurrentContent() as CoMap<TeamContent, AccountMeta>,
|
||||
coValue.getCurrentContent() as CoMap<GroupContent, AccountMeta>,
|
||||
this
|
||||
).getCurrentAgentID();
|
||||
}
|
||||
|
||||
createTeam(): Team {
|
||||
const teamCoValue = this.createCoValue({
|
||||
createGroup(): Group {
|
||||
const groupCoValue = this.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "team", initialAdmin: this.account.id },
|
||||
ruleset: { type: "group", initialAdmin: this.account.id },
|
||||
meta: null,
|
||||
...createdNowUnique(),
|
||||
});
|
||||
|
||||
let teamContent = expectTeamContent(teamCoValue.getCurrentContent());
|
||||
let groupContent = expectGroupContent(groupCoValue.getCurrentContent());
|
||||
|
||||
teamContent = teamContent.edit((editable) => {
|
||||
groupContent = groupContent.edit((editable) => {
|
||||
editable.set(this.account.id, "admin", "trusting");
|
||||
|
||||
const readKey = newRandomKeySecret();
|
||||
@@ -374,8 +376,8 @@ export class LocalNode {
|
||||
this.account.currentSealerSecret(),
|
||||
this.account.currentSealerID(),
|
||||
{
|
||||
in: teamCoValue.id,
|
||||
tx: teamCoValue.nextTransactionID(),
|
||||
in: groupCoValue.id,
|
||||
tx: groupCoValue.nextTransactionID(),
|
||||
}
|
||||
),
|
||||
"trusting"
|
||||
@@ -384,7 +386,7 @@ export class LocalNode {
|
||||
editable.set("readKey", readKey.id, "trusting");
|
||||
});
|
||||
|
||||
return new Team(teamContent, this);
|
||||
return new Group(groupContent, this);
|
||||
}
|
||||
|
||||
testWithDifferentAccount(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,8 +17,8 @@ import {
|
||||
} from "./account.js";
|
||||
|
||||
export type PermissionsDef =
|
||||
| { type: "team"; initialAdmin: AccountIDOrAgentID }
|
||||
| { type: "ownedByTeam"; team: RawCoID }
|
||||
| { type: "group"; initialAdmin: AccountIDOrAgentID }
|
||||
| { type: "ownedByGroup"; group: RawCoID }
|
||||
| { type: "unsafeAllowAll" };
|
||||
|
||||
export type Role =
|
||||
@@ -33,7 +33,7 @@ export type Role =
|
||||
export function determineValidTransactions(
|
||||
coValue: CoValue
|
||||
): { txID: TransactionID; tx: Transaction }[] {
|
||||
if (coValue.header.ruleset.type === "team") {
|
||||
if (coValue.header.ruleset.type === "group") {
|
||||
const allTrustingTransactionsSorted = Object.entries(
|
||||
coValue.sessions
|
||||
).flatMap(([sessionID, sessionLog]) => {
|
||||
@@ -43,7 +43,7 @@ export function determineValidTransactions(
|
||||
if (tx.privacy === "trusting") {
|
||||
return true;
|
||||
} else {
|
||||
console.warn("Unexpected private transaction in Team");
|
||||
console.warn("Unexpected private transaction in Group");
|
||||
return false;
|
||||
}
|
||||
}) as {
|
||||
@@ -60,7 +60,7 @@ export function determineValidTransactions(
|
||||
const initialAdmin = coValue.header.ruleset.initialAdmin;
|
||||
|
||||
if (!initialAdmin) {
|
||||
throw new Error("Team must have initialAdmin");
|
||||
throw new Error("Group must have initialAdmin");
|
||||
}
|
||||
|
||||
const memberState: { [agent: AccountIDOrAgentID]: Role } = {};
|
||||
@@ -81,12 +81,12 @@ export function determineValidTransactions(
|
||||
| MapOpPayload<"readKey", JsonValue>
|
||||
| MapOpPayload<"profile", CoID<Profile>>;
|
||||
if (tx.changes.length !== 1) {
|
||||
console.warn("Team transaction must have exactly one change");
|
||||
console.warn("Group transaction must have exactly one change");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.op !== "set") {
|
||||
console.warn("Team transaction must set a role or readKey");
|
||||
console.warn("Group transaction must set a role or readKey");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export function determineValidTransactions(
|
||||
change.value !== "writerInvite" &&
|
||||
change.value !== "readerInvite"
|
||||
) {
|
||||
console.warn("Team transaction must set a valid role");
|
||||
console.warn("Group transaction must set a valid role");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ export function determineValidTransactions(
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Team transaction must be made by current admin or invite"
|
||||
"Group transaction must be made by current admin or invite"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -189,16 +189,16 @@ export function determineValidTransactions(
|
||||
}
|
||||
|
||||
return validTransactions;
|
||||
} else if (coValue.header.ruleset.type === "ownedByTeam") {
|
||||
const teamContent = coValue.node
|
||||
} else if (coValue.header.ruleset.type === "ownedByGroup") {
|
||||
const groupContent = coValue.node
|
||||
.expectCoValueLoaded(
|
||||
coValue.header.ruleset.team,
|
||||
"Determining valid transaction in owned object but its team wasn't loaded"
|
||||
coValue.header.ruleset.group,
|
||||
"Determining valid transaction in owned object but its group wasn't loaded"
|
||||
)
|
||||
.getCurrentContent();
|
||||
|
||||
if (teamContent.type !== "comap") {
|
||||
throw new Error("Team must be a map");
|
||||
if (groupContent.type !== "comap") {
|
||||
throw new Error("Group must be a map");
|
||||
}
|
||||
|
||||
return Object.entries(coValue.sessions).flatMap(
|
||||
@@ -208,7 +208,7 @@ export function determineValidTransactions(
|
||||
);
|
||||
return sessionLog.transactions
|
||||
.filter((tx) => {
|
||||
const transactorRoleAtTxTime = teamContent.getAtTime(
|
||||
const transactorRoleAtTxTime = groupContent.getAtTime(
|
||||
transactor,
|
||||
tx.madeAt
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LocalNode } from "./node.js";
|
||||
import { Peer, PeerID, SyncMessage } from "./sync.js";
|
||||
import { expectMap } from "./contentType.js";
|
||||
import { MapOpPayload } from "./contentTypes/coMap.js";
|
||||
import { Team } from "./team.js";
|
||||
import { Group } from "./group.js";
|
||||
import {
|
||||
ReadableStream,
|
||||
WritableStream,
|
||||
@@ -23,9 +23,9 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "trusting");
|
||||
@@ -53,7 +53,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
|
||||
const mapTellKnownStateMsg = await reader.read();
|
||||
expect(mapTellKnownStateMsg.value).toEqual({
|
||||
@@ -62,7 +62,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const newContentMsg = await reader.read();
|
||||
|
||||
@@ -71,7 +71,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
id: map.coValue.id,
|
||||
header: {
|
||||
type: "comap",
|
||||
ruleset: { type: "ownedByTeam", team: team.id },
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
createdAt: map.coValue.header.createdAt,
|
||||
uniqueness: map.coValue.header.uniqueness,
|
||||
@@ -104,9 +104,9 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "trusting");
|
||||
@@ -137,7 +137,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
|
||||
const mapTellKnownStateMsg = await reader.read();
|
||||
expect(mapTellKnownStateMsg.value).toEqual({
|
||||
@@ -146,7 +146,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapNewContentMsg = await reader.read();
|
||||
|
||||
@@ -186,9 +186,9 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -214,7 +214,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
|
||||
const mapTellKnownStateMsg = await reader.read();
|
||||
expect(mapTellKnownStateMsg.value).toEqual({
|
||||
@@ -223,7 +223,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapNewContentHeaderOnlyMsg = await reader.read();
|
||||
|
||||
@@ -303,9 +303,9 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "trusting");
|
||||
@@ -323,7 +323,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
// expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
|
||||
const writer = inTx.getWriter();
|
||||
|
||||
@@ -337,7 +337,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
});
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
|
||||
const mapTellKnownStateMsg = await reader.read();
|
||||
expect(mapTellKnownStateMsg.value).toEqual({
|
||||
@@ -346,7 +346,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapNewContentMsg = await reader.read();
|
||||
|
||||
@@ -382,9 +382,9 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -410,7 +410,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
|
||||
const mapTellKnownStateMsg = await reader.read();
|
||||
expect(mapTellKnownStateMsg.value).toEqual({
|
||||
@@ -419,7 +419,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapNewContentHeaderOnlyMsg = await reader.read();
|
||||
|
||||
@@ -485,9 +485,9 @@ test("If we add a peer, but it never subscribes to a coValue, it won't get any m
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const [inRx, _inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -514,9 +514,9 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const [inRx, _inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -535,7 +535,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
||||
// });
|
||||
expect((await reader.read()).value).toMatchObject({
|
||||
action: "load",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
});
|
||||
|
||||
const mapSubscribeMsg = await reader.read();
|
||||
@@ -552,7 +552,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
||||
});
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapNewContentMsg = await reader.read();
|
||||
|
||||
@@ -588,7 +588,7 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const [inRx, _inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -607,10 +607,10 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
|
||||
// });
|
||||
expect((await reader.read()).value).toMatchObject({
|
||||
action: "load",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
});
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const mapSubscribeMsg = await reader.read();
|
||||
|
||||
@@ -620,7 +620,7 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(adminID));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapContentMsg = await reader.read();
|
||||
|
||||
@@ -640,9 +640,9 @@ test("When we connect a new server peer, we try to sync all existing coValues to
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const [inRx, _inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -657,11 +657,11 @@ test("When we connect a new server peer, we try to sync all existing coValues to
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// const _adminSubscribeMessage = await reader.read();
|
||||
const teamSubscribeMessage = await reader.read();
|
||||
const groupSubscribeMessage = await reader.read();
|
||||
|
||||
expect(teamSubscribeMessage.value).toEqual({
|
||||
expect(groupSubscribeMessage.value).toEqual({
|
||||
action: "load",
|
||||
...team.teamMap.coValue.knownState(),
|
||||
...group.groupMap.coValue.knownState(),
|
||||
} satisfies SyncMessage);
|
||||
|
||||
const secondMessage = await reader.read();
|
||||
@@ -676,9 +676,9 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -704,7 +704,7 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe
|
||||
const reader = outRx.getReader();
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
|
||||
const mapTellKnownState = await reader.read();
|
||||
|
||||
expect(mapTellKnownState.value).toEqual({
|
||||
@@ -719,7 +719,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
|
||||
|
||||
const node1 = new LocalNode(admin, session);
|
||||
|
||||
const team = node1.createTeam();
|
||||
const group = node1.createGroup();
|
||||
|
||||
const [inRx1, inTx1] = newStreamPair<SyncMessage>();
|
||||
const [outRx1, outTx1] = newStreamPair<SyncMessage>();
|
||||
@@ -754,40 +754,40 @@ test.skip("When replaying creation and transactions of a coValue as new content,
|
||||
action: "load",
|
||||
id: admin.id,
|
||||
});
|
||||
const teamSubscribeMsg = await from1.read();
|
||||
expect(teamSubscribeMsg.value).toMatchObject({
|
||||
const groupSubscribeMsg = await from1.read();
|
||||
expect(groupSubscribeMsg.value).toMatchObject({
|
||||
action: "load",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
});
|
||||
|
||||
await to2.write(adminSubscribeMessage.value!);
|
||||
await to2.write(teamSubscribeMsg.value!);
|
||||
await to2.write(groupSubscribeMsg.value!);
|
||||
|
||||
// const adminTellKnownStateMsg = await from2.read();
|
||||
// expect(adminTellKnownStateMsg.value).toMatchObject(admStateEx(admin.id));
|
||||
|
||||
const teamTellKnownStateMsg = await from2.read();
|
||||
expect(teamTellKnownStateMsg.value).toMatchObject(teamStateEx(team));
|
||||
const groupTellKnownStateMsg = await from2.read();
|
||||
expect(groupTellKnownStateMsg.value).toMatchObject(groupStateEx(group));
|
||||
|
||||
expect(
|
||||
node2.sync.peers["test1"]!.optimisticKnownStates[
|
||||
team.teamMap.coValue.id
|
||||
group.groupMap.coValue.id
|
||||
]
|
||||
).toBeDefined();
|
||||
|
||||
// await to1.write(adminTellKnownStateMsg.value!);
|
||||
await to1.write(teamTellKnownStateMsg.value!);
|
||||
await to1.write(groupTellKnownStateMsg.value!);
|
||||
|
||||
// const adminContentMsg = await from1.read();
|
||||
// expect(adminContentMsg.value).toMatchObject(admContEx(admin.id));
|
||||
|
||||
const teamContentMsg = await from1.read();
|
||||
expect(teamContentMsg.value).toMatchObject(teamContentEx(team));
|
||||
const groupContentMsg = await from1.read();
|
||||
expect(groupContentMsg.value).toMatchObject(groupContentEx(group));
|
||||
|
||||
// await to2.write(adminContentMsg.value!);
|
||||
await to2.write(teamContentMsg.value!);
|
||||
await to2.write(groupContentMsg.value!);
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const mapSubscriptionMsg = await from1.read();
|
||||
expect(mapSubscriptionMsg.value).toMatchObject({
|
||||
@@ -840,9 +840,9 @@ test.skip("When loading a coValue on one node, the server node it is requested f
|
||||
|
||||
const node1 = new LocalNode(admin, session);
|
||||
|
||||
const team = node1.createTeam();
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "trusting");
|
||||
});
|
||||
@@ -868,9 +868,9 @@ test("Can sync a coValue through a server to another client", async () => {
|
||||
|
||||
const client1 = new LocalNode(admin, session);
|
||||
|
||||
const team = client1.createTeam();
|
||||
const group = client1.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "trusting");
|
||||
});
|
||||
@@ -910,9 +910,9 @@ test("Can sync a coValue with private transactions through a server to another c
|
||||
|
||||
const client1 = new LocalNode(admin, session);
|
||||
|
||||
const team = client1.createTeam();
|
||||
const group = client1.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "private");
|
||||
});
|
||||
@@ -952,7 +952,7 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -971,10 +971,10 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async
|
||||
// });
|
||||
expect((await reader.read()).value).toMatchObject({
|
||||
action: "load",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
});
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const mapSubscribeMsg = await reader.read();
|
||||
|
||||
@@ -984,7 +984,7 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapContentMsg = await reader.read();
|
||||
|
||||
@@ -1006,7 +1006,7 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
const node = new LocalNode(admin, session);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
@@ -1025,10 +1025,10 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async
|
||||
// });
|
||||
expect((await reader.read()).value).toMatchObject({
|
||||
action: "load",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
});
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
const mapSubscribeMsg = await reader.read();
|
||||
|
||||
@@ -1038,7 +1038,7 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async
|
||||
} satisfies SyncMessage);
|
||||
|
||||
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
|
||||
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
||||
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
|
||||
|
||||
const mapContentMsg = await reader.read();
|
||||
|
||||
@@ -1066,9 +1066,9 @@ test("If we start loading a coValue before connecting to a peer that has it, it
|
||||
|
||||
const node1 = new LocalNode(admin, session);
|
||||
|
||||
const team = node1.createTeam();
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
map.edit((editable) => {
|
||||
editable.set("hello", "world", "trusting");
|
||||
});
|
||||
@@ -1096,10 +1096,10 @@ test("If we start loading a coValue before connecting to a peer that has it, it
|
||||
);
|
||||
});
|
||||
|
||||
function teamContentEx(team: Team) {
|
||||
function groupContentEx(group: Group) {
|
||||
return {
|
||||
action: "content",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1110,10 +1110,10 @@ function admContEx(adminID: AccountID) {
|
||||
};
|
||||
}
|
||||
|
||||
function teamStateEx(team: Team) {
|
||||
function groupStateEx(group: Group) {
|
||||
return {
|
||||
action: "known",
|
||||
id: team.teamMap.coValue.id,
|
||||
id: group.groupMap.coValue.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js";
|
||||
import { newRandomSessionID } from "./coValue.js";
|
||||
import { LocalNode } from "./node.js";
|
||||
import { expectTeamContent } from "./team.js";
|
||||
import { expectGroupContent } from "./group.js";
|
||||
import { AnonymousControlledAccount } from "./account.js";
|
||||
import { SessionID } from "./ids.js";
|
||||
|
||||
@@ -13,69 +13,69 @@ export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccoun
|
||||
return [new AnonymousControlledAccount(agentSecret), sessionID];
|
||||
}
|
||||
|
||||
export function newTeam() {
|
||||
export function newGroup() {
|
||||
const [admin, sessionID] = randomAnonymousAccountAndSessionID();
|
||||
|
||||
const node = new LocalNode(admin, sessionID);
|
||||
|
||||
const team = node.createCoValue({
|
||||
const group = node.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "team", initialAdmin: admin.id },
|
||||
ruleset: { type: "group", initialAdmin: admin.id },
|
||||
meta: null,
|
||||
...createdNowUnique(),
|
||||
});
|
||||
|
||||
const teamContent = expectTeamContent(team.getCurrentContent());
|
||||
const groupContent = expectGroupContent(group.getCurrentContent());
|
||||
|
||||
teamContent.edit((editable) => {
|
||||
groupContent.edit((editable) => {
|
||||
editable.set(admin.id, "admin", "trusting");
|
||||
expect(editable.get(admin.id)).toEqual("admin");
|
||||
});
|
||||
|
||||
return { node, team, admin };
|
||||
return { node, group, admin };
|
||||
}
|
||||
|
||||
export function teamWithTwoAdmins() {
|
||||
const { team, admin, node } = newTeam();
|
||||
export function groupWithTwoAdmins() {
|
||||
const { group, admin, node } = newGroup();
|
||||
|
||||
const otherAdmin = node.createAccount("otherAdmin");
|
||||
|
||||
let content = expectTeamContent(team.getCurrentContent());
|
||||
let content = expectGroupContent(group.getCurrentContent());
|
||||
|
||||
content.edit((editable) => {
|
||||
editable.set(otherAdmin.id, "admin", "trusting");
|
||||
expect(editable.get(otherAdmin.id)).toEqual("admin");
|
||||
});
|
||||
|
||||
content = expectTeamContent(team.getCurrentContent());
|
||||
content = expectGroupContent(group.getCurrentContent());
|
||||
|
||||
if (content.type !== "comap") {
|
||||
throw new Error("Expected map");
|
||||
}
|
||||
|
||||
expect(content.get(otherAdmin.id)).toEqual("admin");
|
||||
return { team, admin, otherAdmin, node };
|
||||
return { group, admin, otherAdmin, node };
|
||||
}
|
||||
|
||||
export function newTeamHighLevel() {
|
||||
export function newGroupHighLevel() {
|
||||
const [admin, sessionID] = randomAnonymousAccountAndSessionID();
|
||||
|
||||
|
||||
const node = new LocalNode(admin, sessionID);
|
||||
|
||||
const team = node.createTeam();
|
||||
const group = node.createGroup();
|
||||
|
||||
return { admin, node, team };
|
||||
return { admin, node, group };
|
||||
}
|
||||
|
||||
export function teamWithTwoAdminsHighLevel() {
|
||||
const { admin, node, team } = newTeamHighLevel();
|
||||
export function groupWithTwoAdminsHighLevel() {
|
||||
const { admin, node, group } = newGroupHighLevel();
|
||||
|
||||
const otherAdmin = node.createAccount("otherAdmin");
|
||||
|
||||
team.addMember(otherAdmin.id, "admin");
|
||||
group.addMember(otherAdmin.id, "admin");
|
||||
|
||||
return { admin, node, team, otherAdmin };
|
||||
return { admin, node, group, otherAdmin };
|
||||
}
|
||||
|
||||
export function shouldNotResolve<T>(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-local",
|
||||
"version": "0.0.7",
|
||||
"version": "0.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser": "^0.0.7",
|
||||
"jazz-browser": "^0.1.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.0.7",
|
||||
"version": "0.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.0.24",
|
||||
"jazz-storage-indexeddb": "^0.0.11",
|
||||
"cojson": "^0.1.2",
|
||||
"jazz-storage-indexeddb": "^0.1.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SyncMessage,
|
||||
Peer,
|
||||
ContentType,
|
||||
Team,
|
||||
Group,
|
||||
CoID,
|
||||
} from "cojson";
|
||||
import { ReadableStream, WritableStream } from "isomorphic-streams";
|
||||
@@ -247,24 +247,24 @@ export function createInviteLink(
|
||||
const node = coValue.node;
|
||||
let currentCoValue = coValue;
|
||||
|
||||
while (currentCoValue.header.ruleset.type === "ownedByTeam") {
|
||||
while (currentCoValue.header.ruleset.type === "ownedByGroup") {
|
||||
currentCoValue = node.expectCoValueLoaded(
|
||||
currentCoValue.header.ruleset.team
|
||||
currentCoValue.header.ruleset.group
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCoValue.header.ruleset.type !== "team") {
|
||||
throw new Error("Can't create invite link for object without team");
|
||||
if (currentCoValue.header.ruleset.type !== "group") {
|
||||
throw new Error("Can't create invite link for object without group");
|
||||
}
|
||||
|
||||
const team = new Team(
|
||||
cojsonInternals.expectTeamContent(currentCoValue.getCurrentContent()),
|
||||
const group = new Group(
|
||||
cojsonInternals.expectGroupContent(currentCoValue.getCurrentContent()),
|
||||
node
|
||||
);
|
||||
|
||||
const inviteSecret = team.createInvite(role);
|
||||
const inviteSecret = group.createInvite(role);
|
||||
|
||||
return `${baseURL}#invitedTo=${value.id}&inviteSecret=${inviteSecret}`;
|
||||
return `${baseURL}#invitedTo=${value.id}&${inviteSecret}`;
|
||||
}
|
||||
|
||||
export function parseInviteLink(inviteURL: string):
|
||||
@@ -278,8 +278,7 @@ export function parseInviteLink(inviteURL: string):
|
||||
.split("&")[0]
|
||||
?.replace(/^#invitedTo=/, "") as CoID<ContentType>;
|
||||
const inviteSecret = url.hash
|
||||
.split("&")[1]
|
||||
?.replace(/^inviteSecret=/, "") as InviteSecret;
|
||||
.split("&")[1] as InviteSecret;
|
||||
if (!valueID || !inviteSecret) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react-auth-local",
|
||||
"version": "0.0.14",
|
||||
"version": "0.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser-auth-local": "^0.0.7",
|
||||
"jazz-react": "^0.0.17",
|
||||
"jazz-browser-auth-local": "^0.1.2",
|
||||
"jazz-react": "^0.1.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.0.17",
|
||||
"version": "0.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.0.24",
|
||||
"jazz-browser": "^0.0.7",
|
||||
"cojson": "^0.1.2",
|
||||
"jazz-browser": "^0.1.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-storage-indexeddb",
|
||||
"version": "0.0.11",
|
||||
"version": "0.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.0.24",
|
||||
"cojson": "^0.1.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -16,7 +16,7 @@ test.skip("Should be able to initialize and load from empty DB", async () => {
|
||||
|
||||
console.log("yay!");
|
||||
|
||||
const _team = node.createTeam();
|
||||
const _group = node.createGroup();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
@@ -39,9 +39,9 @@ test("Should be able to sync data to database and then load that from a new node
|
||||
|
||||
console.log("yay!");
|
||||
|
||||
const team = node1.createTeam();
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = team.createMap();
|
||||
const map = group.createMap();
|
||||
|
||||
map.edit((m) => {
|
||||
m.set("hello", "world");
|
||||
|
||||
@@ -205,7 +205,7 @@ export class IDBStorage {
|
||||
}
|
||||
|
||||
const dependedOnCoValues =
|
||||
coValueRow?.header.ruleset.type === "team"
|
||||
coValueRow?.header.ruleset.type === "group"
|
||||
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
||||
sessionEntry.newTransactions.flatMap((tx) => {
|
||||
if (tx.privacy !== "trusting") return [];
|
||||
@@ -226,8 +226,8 @@ export class IDBStorage {
|
||||
);
|
||||
})
|
||||
)
|
||||
: coValueRow?.header.ruleset.type === "ownedByTeam"
|
||||
? [coValueRow?.header.ruleset.team]
|
||||
: coValueRow?.header.ruleset.type === "ownedByGroup"
|
||||
? [coValueRow?.header.ruleset.group]
|
||||
: [];
|
||||
|
||||
for (const dependedOnCoValue of dependedOnCoValues) {
|
||||
|
||||
@@ -1640,7 +1640,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae"
|
||||
integrity sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==
|
||||
|
||||
"@types/ws@^8.5.3":
|
||||
"@types/ws@^8.5.3", "@types/ws@^8.5.5":
|
||||
version "8.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb"
|
||||
integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==
|
||||
@@ -7744,6 +7744,11 @@ unbzip2-stream@1.4.3:
|
||||
buffer "^5.2.1"
|
||||
through "^2.3.8"
|
||||
|
||||
uniqolor@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uniqolor/-/uniqolor-1.1.0.tgz#7519f81133cd54a1f4a59c33c81dbe04a3ad155d"
|
||||
integrity sha512-j2XyokF24fsj+L5u6fbu4rM3RQc6VWJuAngYM2k0ZdG3yiVxt0smLkps2GmQIYqK8VkELGdM9vFU/HfOkK/zoQ==
|
||||
|
||||
unique-filename@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea"
|
||||
@@ -8109,7 +8114,7 @@ write-pkg@4.0.0:
|
||||
type-fest "^0.4.1"
|
||||
write-json-file "^3.2.0"
|
||||
|
||||
ws@8.13.0, ws@^8.8.0:
|
||||
ws@8.13.0, ws@^8.13.0, ws@^8.8.0:
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
|
||||
Reference in New Issue
Block a user