Compare commits
35 Commits
cojson@0.1
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e410823087 | ||
|
|
dcc11e5b60 | ||
|
|
1b05fe5b55 | ||
|
|
de8f896bf9 | ||
|
|
d63716a827 | ||
|
|
a1e7fce3b9 | ||
|
|
d5edad7ba5 | ||
|
|
559a4a223b | ||
|
|
16d5553ccd | ||
|
|
ef76c586cc | ||
|
|
8ea4b9761c | ||
|
|
a81f281079 | ||
|
|
3ecb602459 | ||
|
|
ea69ea1f67 | ||
|
|
e430bd061e | ||
|
|
a957485172 | ||
|
|
9060975dfb | ||
|
|
753ec83fdd | ||
|
|
44a9785e93 | ||
|
|
d9b390e538 | ||
|
|
b194f7831b | ||
|
|
b60be9405d | ||
|
|
84588e0798 | ||
|
|
e5b170f25e | ||
|
|
63d0b0673e | ||
|
|
2b456b5e07 | ||
|
|
e20809d314 | ||
|
|
b6de11e125 | ||
|
|
d23c71d511 | ||
|
|
b24071cf33 | ||
|
|
a5c88c08de | ||
|
|
906932db1e | ||
|
|
2033e35a41 | ||
|
|
5a2a12ccbb | ||
|
|
d288cb6954 |
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
project: ["tests/e2e", "examples/chat", "examples/clerk", "examples/betterauth", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "starters/react-passkey-auth"]
|
||||
project: ["tests/e2e", "examples/chat", "examples/clerk", "examples/betterauth", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/organization", "examples/pets", "starters/react-passkey-auth"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,3 +29,5 @@ test-results
|
||||
|
||||
.cursorrules
|
||||
.windsurfrules
|
||||
|
||||
playwright-report
|
||||
@@ -1,5 +1,17 @@
|
||||
# betterauth
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-betterauth-server-plugin@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-react-auth-betterauth@0.13.31
|
||||
- jazz-betterauth-client-plugin@0.13.31
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "betterauth",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# chat-rn-expo-clerk
|
||||
|
||||
## 1.0.122
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-expo@0.13.31
|
||||
- jazz-react-native-media-images@0.13.31
|
||||
|
||||
## 1.0.121
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-expo-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.121",
|
||||
"version": "1.0.122",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# chat-rn-expo
|
||||
|
||||
## 1.0.109
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-expo@0.13.31
|
||||
|
||||
## 1.0.108
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn-expo",
|
||||
"version": "1.0.108",
|
||||
"version": "1.0.109",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.117
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- jazz-tools@0.13.31
|
||||
- cojson@0.13.31
|
||||
- jazz-react-native@0.13.31
|
||||
- cojson-transport-ws@0.13.31
|
||||
|
||||
## 1.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.116",
|
||||
"version": "1.0.117",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.100
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-browser@0.13.31
|
||||
- jazz-vue@0.13.31
|
||||
|
||||
## 0.0.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.99",
|
||||
"version": "0.0.100",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.198
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.197
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.197",
|
||||
"version": "0.0.198",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-react-auth-clerk@0.13.31
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.97",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector-element@0.13.31
|
||||
- jazz-svelte@0.13.31
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.80",
|
||||
"version": "0.0.81",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# jazz-tailwind-demo-auth-starter
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "filestream",
|
||||
"private": true,
|
||||
"version": "0.0.36",
|
||||
"version": "0.0.37",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# form
|
||||
|
||||
## 0.1.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.1.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.1.37",
|
||||
"version": "0.1.38",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.94",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.148
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- cojson-transport-ws@0.13.31
|
||||
|
||||
## 0.0.147
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.147",
|
||||
"version": "0.0.148",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# multi-cursors
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multi-cursors",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.90",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# multiauth
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-react-auth-clerk@0.13.31
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multiauth",
|
||||
"private": true,
|
||||
"version": "0.0.37",
|
||||
"version": "0.0.38",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.119
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.118",
|
||||
"version": "0.0.119",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
2
examples/organization/.gitignore
vendored
2
examples/organization/.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
playwright-report
|
||||
@@ -1,5 +1,13 @@
|
||||
# organization
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.90",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:*",
|
||||
@@ -20,6 +22,7 @@
|
||||
"react-router-dom": "^6.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@types/react": "^18.3.12",
|
||||
|
||||
53
examples/organization/playwright.config.ts
Normal file
53
examples/organization/playwright.config.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import isCI from "is-ci";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: isCI,
|
||||
/* Retry on CI only */
|
||||
retries: isCI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: isCI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://localhost: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,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -5,7 +5,13 @@ import { Heading } from "./components/Heading.tsx";
|
||||
|
||||
export function HomePage() {
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { organizations: true } },
|
||||
resolve: {
|
||||
root: {
|
||||
organizations: {
|
||||
$each: { $onError: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!me?.root.organizations) return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserIcon } from "lucide-react";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { me, logOut } = useAccount({
|
||||
resolve: { root: { draftOrganization: true } },
|
||||
resolve: { profile: true },
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -16,7 +16,20 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<span className="bg-stone-500 pt-1 size-6 flex items-center justify-center rounded-full">
|
||||
<UserIcon size={20} className="stroke-white" />
|
||||
</span>
|
||||
{me?.profile?.name}
|
||||
<label htmlFor="profile-name" className="sr-only">
|
||||
Profile name
|
||||
</label>
|
||||
<input
|
||||
id="profile-name"
|
||||
type="text"
|
||||
value={me?.profile.name ?? ""}
|
||||
className="rounded-md shadow-sm dark:bg-transparent text-sm py-1.5 px-3"
|
||||
onChange={(e) => {
|
||||
if (me) {
|
||||
me.profile.name = e.target.value;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button
|
||||
|
||||
@@ -16,7 +16,21 @@ export function OrganizationPage() {
|
||||
resolve: { projects: true },
|
||||
});
|
||||
|
||||
if (!organization) return <p>Loading organization...</p>;
|
||||
if (organization === undefined) return <p>Loading organization...</p>;
|
||||
if (organization === null) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
You don't have access to this organization
|
||||
</h1>
|
||||
<a href="/#" className="text-blue-500">
|
||||
Go back to home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function OrganizationForm({
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Submit
|
||||
Create
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Group } from "jazz-tools";
|
||||
import { useAccount, useCoState } from "jazz-react";
|
||||
import { Account, Group, ID } from "jazz-tools";
|
||||
import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationMembers({
|
||||
@@ -9,13 +10,51 @@ export function OrganizationMembers({
|
||||
return (
|
||||
<>
|
||||
{group.members.map((member) => (
|
||||
<div key={member.id} className="px-4 py-5 sm:px-6">
|
||||
<strong className="font-medium">
|
||||
{member.account.profile?.name}
|
||||
</strong>{" "}
|
||||
({member.role})
|
||||
</div>
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
accountId={member.account.id}
|
||||
role={member.role}
|
||||
group={group}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MemberItem({
|
||||
accountId,
|
||||
role,
|
||||
group,
|
||||
}: { accountId: ID<Account>; role: string; group: Group }) {
|
||||
const account = useCoState(Account, accountId, {
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
const { me } = useAccount();
|
||||
|
||||
const canRemoveMember = group.myRole() === "admin" && accountId !== me?.id;
|
||||
|
||||
function handleRemoveMember() {
|
||||
if (canRemoveMember && account) {
|
||||
group.removeMember(account);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 py-5 sm:px-6 flex justify-between items-center">
|
||||
<div>
|
||||
<strong className="font-medium">{account?.profile.name}</strong> ({role}
|
||||
)
|
||||
</div>
|
||||
{canRemoveMember && (
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
onClick={handleRemoveMember}
|
||||
>
|
||||
Remove member
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationSelector({ className }: { className?: string }) {
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { organizations: { $each: true } } },
|
||||
resolve: { root: { organizations: { $each: { $onError: null } } } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -48,6 +48,10 @@ export function OrganizationSelector({ className }: { className?: string }) {
|
||||
className="rounded-md shadow-sm dark:bg-transparent w-full"
|
||||
>
|
||||
{me?.root.organizations.map((organization) => {
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Account, CoList, CoMap, Group, co } from "jazz-tools";
|
||||
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
|
||||
import { getRandomUsername } from "./util";
|
||||
|
||||
export class Project extends CoMap {
|
||||
name = co.string;
|
||||
@@ -38,43 +39,49 @@ export class JazzAccountRoot extends CoMap {
|
||||
export class JazzAccount extends Account {
|
||||
root = co.ref(JazzAccountRoot);
|
||||
|
||||
async migrate() {
|
||||
if (!this._refs.root) {
|
||||
const draftOrganizationOwnership = {
|
||||
owner: Group.create({ owner: this }),
|
||||
};
|
||||
async migrate(this: JazzAccount) {
|
||||
if (this.profile === undefined) {
|
||||
const group = Group.create();
|
||||
this.profile = Profile.create(
|
||||
{
|
||||
name: getRandomUsername(),
|
||||
},
|
||||
group,
|
||||
);
|
||||
group.addMember("everyone", "reader");
|
||||
}
|
||||
|
||||
if (this.root === undefined) {
|
||||
const draftOrgGroup = Group.create();
|
||||
const draftOrganization = DraftOrganization.create(
|
||||
{
|
||||
projects: ListOfProjects.create([], draftOrganizationOwnership),
|
||||
projects: ListOfProjects.create([], draftOrgGroup),
|
||||
},
|
||||
draftOrganizationOwnership,
|
||||
draftOrgGroup,
|
||||
);
|
||||
|
||||
const initialOrganizationOwnership = {
|
||||
owner: Group.create({ owner: this }),
|
||||
};
|
||||
const organizations = ListOfOrganizations.create(
|
||||
[
|
||||
Organization.create(
|
||||
{
|
||||
name: this.profile?.name
|
||||
? `${this.profile.name}'s projects`
|
||||
: "Your projects",
|
||||
projects: ListOfProjects.create([], initialOrganizationOwnership),
|
||||
},
|
||||
initialOrganizationOwnership,
|
||||
),
|
||||
],
|
||||
{ owner: this },
|
||||
);
|
||||
const defaultOrgGroup = Group.create();
|
||||
|
||||
this.root = JazzAccountRoot.create(
|
||||
{
|
||||
draftOrganization,
|
||||
organizations,
|
||||
const { profile } = await this.ensureLoaded({
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
{ owner: this },
|
||||
);
|
||||
});
|
||||
|
||||
const organizations = ListOfOrganizations.create([
|
||||
Organization.create(
|
||||
{
|
||||
name: profile.name ? `${profile.name}'s projects` : "Your projects",
|
||||
projects: ListOfProjects.create([], defaultOrgGroup),
|
||||
},
|
||||
defaultOrgGroup,
|
||||
),
|
||||
]);
|
||||
|
||||
this.root = JazzAccountRoot.create({
|
||||
draftOrganization,
|
||||
organizations,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
examples/organization/src/util.ts
Normal file
16
examples/organization/src/util.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const animals = [
|
||||
"elephant",
|
||||
"penguin",
|
||||
"giraffe",
|
||||
"octopus",
|
||||
"kangaroo",
|
||||
"dolphin",
|
||||
"cheetah",
|
||||
"koala",
|
||||
"platypus",
|
||||
"pangolin",
|
||||
];
|
||||
|
||||
export function getRandomUsername() {
|
||||
return `Anonymous ${animals[Math.floor(Math.random() * animals.length)]}`;
|
||||
}
|
||||
65
examples/organization/tests/createAndShare.spec.ts
Normal file
65
examples/organization/tests/createAndShare.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { BrowserContext, expect, test } from "@playwright/test";
|
||||
|
||||
test("create a new organization and share", async ({
|
||||
page: marioPage,
|
||||
browser,
|
||||
}) => {
|
||||
await marioPage.goto("/");
|
||||
|
||||
const luigiContext = await browser.newContext();
|
||||
const luigiPage = await luigiContext.newPage();
|
||||
await luigiPage.goto("/");
|
||||
|
||||
await test.step("Set the profile names", async () => {
|
||||
await marioPage
|
||||
.getByRole("textbox", { name: "Profile name" })
|
||||
.fill("Mario");
|
||||
await luigiPage
|
||||
.getByRole("textbox", { name: "Profile name" })
|
||||
.fill("Luigi");
|
||||
});
|
||||
|
||||
await test.step("Create a new organization", async () => {
|
||||
await marioPage
|
||||
.getByRole("textbox", { name: "Organization name" })
|
||||
.fill("Mario's organization");
|
||||
await marioPage.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
await expect(
|
||||
marioPage.getByRole("heading", { name: "Mario's organization" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Invite Luigi to the organization", async () => {
|
||||
await marioPage.getByRole("button", { name: "Copy invite link" }).click();
|
||||
|
||||
const inviteUrl = await marioPage.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
|
||||
await luigiPage.goto(inviteUrl);
|
||||
|
||||
await expect(
|
||||
luigiPage.getByRole("heading", { name: "Mario's organization" }),
|
||||
).toBeVisible();
|
||||
await expect(marioPage.getByText("Luigi")).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Kick out Luigi from the organization", async () => {
|
||||
await marioPage.getByRole("button", { name: "Remove" }).click();
|
||||
|
||||
await expect(marioPage.getByText("Luigi")).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
luigiPage.getByRole("heading", {
|
||||
name: "You don't have access to this organization",
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await luigiPage.getByRole("link", { name: "Go back to home" }).click();
|
||||
|
||||
await expect(
|
||||
luigiPage.getByRole("heading", { name: "Organizations example app" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,13 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-svelte@0.13.31
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.84",
|
||||
"version": "0.0.85",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.94",
|
||||
"version": "0.0.95",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# passphrase
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.91",
|
||||
"version": "0.0.92",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.115
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.115",
|
||||
"version": "0.0.116",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.214
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.213
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.213",
|
||||
"version": "0.0.214",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.94",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# richtext-tiptap
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-richtext-tiptap@0.1.7
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext-tiptap",
|
||||
"private": true,
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# richtext
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-richtext-prosemirror@0.1.18
|
||||
|
||||
## 0.0.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext",
|
||||
"private": true,
|
||||
"version": "0.0.83",
|
||||
"version": "0.0.84",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-browser@0.13.31
|
||||
- jazz-vue@0.13.31
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.98",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.213
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.212
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.212",
|
||||
"version": "0.0.213",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.91",
|
||||
"version": "0.0.92",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -4,8 +4,6 @@ import { clsx } from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
|
||||
// TODO: add tabs feature, and remove CodeExampleTabs
|
||||
|
||||
export function CopyButton({
|
||||
code,
|
||||
size,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExampleCard } from "@/components/examples/ExampleCard";
|
||||
import { ExampleDemo } from "@/components/examples/ExampleDemo";
|
||||
import { ClerkFullLogo } from "@/components/icons/ClerkFullLogo";
|
||||
import { ReactLogo } from "@/components/icons/ReactLogo";
|
||||
import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
|
||||
@@ -508,16 +507,16 @@ const reactExamples: Example[] = [
|
||||
demoUrl: "https://music-demo.jazz.tools",
|
||||
illustration: <MusicIllustration />,
|
||||
},
|
||||
{
|
||||
name: "Jazz paper scissors",
|
||||
slug: "jazz-paper-scissors",
|
||||
description:
|
||||
"A game that shows how to communicate with other accounts through the experimental Inbox API.",
|
||||
tech: [tech.react],
|
||||
features: [features.serverWorker, features.inbox],
|
||||
illustration: <JazzPaperScissorsIllustration />,
|
||||
demoUrl: "https://jazz-paper-scissors.vercel.app",
|
||||
},
|
||||
// {
|
||||
// name: "Jazz paper scissors",
|
||||
// slug: "jazz-paper-scissors",
|
||||
// description:
|
||||
// "A game that shows how to communicate with other accounts through the experimental Inbox API.",
|
||||
// tech: [tech.react],
|
||||
// features: [features.serverWorker, features.inbox],
|
||||
// illustration: <JazzPaperScissorsIllustration />,
|
||||
// demoUrl: "https://jazz-paper-scissors.vercel.app",
|
||||
// },
|
||||
{
|
||||
name: "Clerk",
|
||||
slug: "clerk",
|
||||
@@ -681,15 +680,11 @@ export default function Page() {
|
||||
|
||||
<GappedGrid>
|
||||
{category.examples.map((example) =>
|
||||
example.showDemo ? (
|
||||
<ExampleDemo key={example.slug} example={example} />
|
||||
) : (
|
||||
<ExampleCard
|
||||
className="border bg-stone-50 shadow-sm p-3 rounded-lg dark:bg-stone-950"
|
||||
key={example.slug}
|
||||
example={example}
|
||||
/>
|
||||
),
|
||||
<ExampleCard
|
||||
className="border bg-stone-50 shadow-sm p-3 rounded-lg dark:bg-stone-950"
|
||||
key={example.slug}
|
||||
example={example}
|
||||
/>
|
||||
)}
|
||||
</GappedGrid>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { HelpLinks } from "@/components/docs/HelpLinks";
|
||||
import { TableOfContents } from "@/components/docs/TableOfContents";
|
||||
import { JazzMobileNav } from "@/components/nav";
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CodeExampleTab {
|
||||
name: string;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface CodeExampleTabsProps {
|
||||
tabs: Array<CodeExampleTab>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// TODO: handle tabs in CodeGroup component
|
||||
|
||||
export function CodeExampleTabs({
|
||||
tabs,
|
||||
className = "",
|
||||
}: CodeExampleTabsProps) {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-white h-full flex flex-col",
|
||||
"dark:bg-stone-925",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex border-b overflow-x-auto overflow-y-hidden dark:bg-stone-950">
|
||||
{tabs.map((tab, index) => (
|
||||
<div key={index}>
|
||||
<button
|
||||
key={index}
|
||||
className={clsx(
|
||||
activeTab === index
|
||||
? "border-blue-700 bg-white text-stone-700 dark:bg-stone-925 dark:text-blue-500 dark:border-blue-500"
|
||||
: "border-transparent text-stone-500 hover:text-stone-700 dark:hover:text-blue-500",
|
||||
"flex items-center -mb-px transition-colors px-3 pb-2 pt-2.5 block text-xs border-b-2 ",
|
||||
)}
|
||||
onClick={() => setActiveTab(index)}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">{tabs[activeTab].content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { CodeExampleTabs } from "@/components/examples/CodeExampleTabs";
|
||||
import { ExampleLinks } from "@/components/examples/ExampleLinks";
|
||||
import { ExampleTags } from "@/components/examples/ExampleTags";
|
||||
import { Example } from "@/content/example";
|
||||
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
|
||||
|
||||
export function ExampleDemo({ example }: { example: Example }) {
|
||||
const { name, demoUrl, illustration } = example;
|
||||
|
||||
return (
|
||||
<GappedGrid
|
||||
gap="none"
|
||||
className="border bg-stone-50 shadow-sm rounded-lg dark:bg-stone-950 overflow-hidden"
|
||||
>
|
||||
<div className="p-3 col-span-full flex flex-col gap-2 justify-between items-baseline border-b sm:flex-row">
|
||||
<div className="flex flex-col gap-2 items-baseline sm:flex-row">
|
||||
<h2 className="font-medium text-stone-900 dark:text-white leading-none">
|
||||
{name}
|
||||
</h2>
|
||||
<ExampleTags example={example} />
|
||||
</div>
|
||||
|
||||
<ExampleLinks example={example} />
|
||||
</div>
|
||||
<div className="h-[25rem] lg:h-[30rem] border-t overflow-auto col-span-full md:col-span-2 lg:col-span-3 order-last md:order-none md:border-r md:border-t-0">
|
||||
{example.codeSamples && (
|
||||
<CodeExampleTabs tabs={example.codeSamples}></CodeExampleTabs>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-full md:p-8 md:col-span-2 lg:col-span-3 h-[25rem] lg:h-[30rem] lg:p-12">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="md:rounded-lg md:shadow-lg"
|
||||
src={demoUrl}
|
||||
title={name}
|
||||
/>
|
||||
</div>
|
||||
</GappedGrid>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
CodeExampleTabs as CodeExampleTabsClient,
|
||||
CodeExampleTabsProps,
|
||||
} from "@/components/examples/CodeExampleTabs";
|
||||
|
||||
import {
|
||||
ContentByFramework as ContentByFrameworkClient,
|
||||
ContentByFrameworkProps,
|
||||
@@ -14,10 +9,6 @@ import { FileDownloadLink as FileDownloadLinkClient } from "./FileDownloadLink";
|
||||
import { Framework as FrameworkClient } from "./docs/Framework";
|
||||
import { IssueTrackerPreview as IssueTrackerPreviewClient } from "./docs/IssueTrackerPreview";
|
||||
|
||||
export function CodeExampleTabs(props: CodeExampleTabsProps) {
|
||||
return <CodeExampleTabsClient {...props} />;
|
||||
}
|
||||
|
||||
export function CodeGroup(props: { children: React.ReactNode }) {
|
||||
return <CodeGroupClient {...props}></CodeGroupClient>;
|
||||
}
|
||||
|
||||
@@ -150,10 +150,12 @@ declare module "jazz-react" {
|
||||
}
|
||||
// ---cut---
|
||||
export function AcceptInvitePage() {
|
||||
const { me } = useAccount({ resolve: { root: { organizations: true } } });
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { organizations: { $each: { $onError: null } } } },
|
||||
});
|
||||
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
if (me?.root?.organizations) {
|
||||
if (me) {
|
||||
Organization.load(organizationId).then((organization) => {
|
||||
if (organization) {
|
||||
// avoid duplicates
|
||||
|
||||
@@ -6,8 +6,6 @@ import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
# Public sharing and invites
|
||||
|
||||
...more docs coming soon
|
||||
|
||||
## Public sharing
|
||||
|
||||
You can share CoValues publicly by setting the `owner` to a `Group`, and granting
|
||||
@@ -94,3 +92,104 @@ useAcceptInvite({
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### Requesting Invites
|
||||
|
||||
To allow a non-group member to request an invitation to a group you can use the `writeOnly` role.
|
||||
This means that users only have write access to a specific requests list (they can't read other requests).
|
||||
However, Administrators can review and approve these requests.
|
||||
|
||||
Create the data models.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { CoMap, co, CoValue, Account, CoList } from "jazz-tools";
|
||||
// ---cut-before---
|
||||
class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.literal("pending", "approved", "rejected");
|
||||
}
|
||||
|
||||
class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Set up the request system with appropriate access controls.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { Group, co, CoList, CoMap, Account } from "jazz-tools";
|
||||
import { createInviteLink } from "jazz-react";
|
||||
|
||||
export class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.literal("pending", "approved", "rejected");
|
||||
}
|
||||
|
||||
export class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
|
||||
// ---cut-before---
|
||||
function createRequestsToJoin() {
|
||||
const requestsGroup = Group.create();
|
||||
requestsGroup.addMember("everyone", "writeOnly");
|
||||
|
||||
return RequestsList.create([], requestsGroup);
|
||||
}
|
||||
|
||||
async function sendJoinRequest(
|
||||
requestsList: RequestsList,
|
||||
account: Account,
|
||||
) {
|
||||
const request = JoinRequest.create(
|
||||
{
|
||||
account,
|
||||
status: "pending",
|
||||
},
|
||||
requestsList._owner // Inherit the access controls of the requestsList
|
||||
);
|
||||
|
||||
requestsList.push(request);
|
||||
|
||||
return request;
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Using the write-only access users can submit requests that only administrators can review and approve.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, CoMap, CoList, ID, Account, Group, Resolved } from "jazz-tools";
|
||||
|
||||
class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.string; // Can be "pending", "approved", "rejected"
|
||||
}
|
||||
|
||||
export class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
|
||||
export class RequestsToJoin extends CoMap {
|
||||
writeOnlyInvite = co.string;
|
||||
requests = co.ref(RequestsList);
|
||||
}
|
||||
|
||||
// ---cut-before---
|
||||
async function approveJoinRequest(
|
||||
joinRequest: JoinRequest,
|
||||
targetGroup: Group,
|
||||
) {
|
||||
const account = await Account.load(joinRequest._refs.account.id);
|
||||
|
||||
if (account) {
|
||||
targetGroup.addMember(account, "reader");
|
||||
joinRequest.status = "approved";
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -465,6 +465,191 @@ project?.owner // => always null
|
||||
The load operation will succeed and return the object, but the inaccessible reference will always be `null`.
|
||||
|
||||
|
||||
#### Deep loading lists with shared items
|
||||
|
||||
When loading a list with shared items, you can use the `$onError` option to safely load the list skipping any inaccessible items.
|
||||
|
||||
This is especially useful when in your app access to these items might be revoked.
|
||||
|
||||
This way the inaccessible items are replaced with `null` in the returned list.
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, CoList, co, Group, Account } from "jazz-tools";
|
||||
import { assert } from "vitest";
|
||||
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
class Friends extends CoList.Of(co.ref(Person)) {}
|
||||
|
||||
const privateGroup = Group.create();
|
||||
const publicGroup = Group.create();
|
||||
const me = {} as unknown as Account;
|
||||
|
||||
// ---cut-before---
|
||||
const source = Friends.create(
|
||||
[
|
||||
Person.create(
|
||||
{
|
||||
name: "Jane",
|
||||
},
|
||||
privateGroup, // We don't have access to Jane
|
||||
),
|
||||
Person.create(
|
||||
{
|
||||
name: "Alice",
|
||||
},
|
||||
publicGroup, // We have access to Alice
|
||||
),
|
||||
],
|
||||
publicGroup,
|
||||
);
|
||||
|
||||
const friends = await Friends.load(source.id, {
|
||||
resolve: {
|
||||
$each: { $onError: null }
|
||||
},
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
// Thanks to $onError catching the errors, the list is loaded
|
||||
// because we have access to friends
|
||||
friends // => Friends
|
||||
|
||||
assert(friends);
|
||||
|
||||
// Jane is null because we lack access rights
|
||||
// and we have used $onError to catch the error on the list items
|
||||
friends[0] // => null
|
||||
|
||||
// Alice is not null because we have access
|
||||
// the type is nullable because we have used $onError
|
||||
friends[1] // => Person
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The `$onError` works as a "catch" clause option to block any error in the resolved childs.
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, CoList, co, Group, Account } from "jazz-tools";
|
||||
import { assert } from "vitest";
|
||||
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
dog = co.ref(Dog);
|
||||
}
|
||||
class Dog extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
class Friends extends CoList.Of(co.ref(Person)) {}
|
||||
|
||||
class User extends CoMap {
|
||||
name = co.string;
|
||||
friends = co.ref(Friends);
|
||||
}
|
||||
|
||||
const privateGroup = Group.create();
|
||||
const publicGroup = Group.create();
|
||||
const me = {} as unknown as Account;
|
||||
|
||||
// ---cut-before---
|
||||
const source = Friends.create(
|
||||
[
|
||||
Person.create(
|
||||
{
|
||||
name: "Jane",
|
||||
dog: Dog.create(
|
||||
{ name: "Rex" },
|
||||
privateGroup,
|
||||
), // We don't have access to Rex
|
||||
},
|
||||
publicGroup,
|
||||
),
|
||||
],
|
||||
publicGroup,
|
||||
);
|
||||
|
||||
const friends = await Friends.load(source.id, {
|
||||
resolve: {
|
||||
$each: { dog: true, $onError: null }
|
||||
},
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
assert(friends);
|
||||
|
||||
// Jane is null because we don't have access to Rex
|
||||
// and we have used $onError to catch the error on the list items
|
||||
friends[0] // => null
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
We can actually use `$onError` everywhere in the resolve query, so we can use it to catch the error on dog:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, CoList, co, Group, Account } from "jazz-tools";
|
||||
import { assert } from "vitest";
|
||||
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
dog = co.ref(Dog);
|
||||
}
|
||||
class Dog extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
class Friends extends CoList.Of(co.ref(Person)) {}
|
||||
|
||||
class User extends CoMap {
|
||||
name = co.string;
|
||||
friends = co.ref(Friends);
|
||||
}
|
||||
|
||||
const privateGroup = Group.create();
|
||||
const publicGroup = Group.create();
|
||||
const me = {} as unknown as Account;
|
||||
|
||||
const source = Friends.create(
|
||||
[
|
||||
Person.create(
|
||||
{
|
||||
name: "Jane",
|
||||
dog: Dog.create(
|
||||
{ name: "Rex" },
|
||||
privateGroup,
|
||||
), // We don't have access to Rex
|
||||
},
|
||||
publicGroup,
|
||||
),
|
||||
],
|
||||
publicGroup,
|
||||
);
|
||||
|
||||
// ---cut-before---
|
||||
const friends = await Friends.load(source.id, {
|
||||
resolve: {
|
||||
$each: { dog: { $onError: null } }
|
||||
},
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
assert(friends);
|
||||
|
||||
// Jane now is not-nullable at type level because
|
||||
// we have moved $onError down to the dog field
|
||||
//
|
||||
// This also means that if we don't have access to Jane
|
||||
// the entire friends list will be null
|
||||
friends[0] // => Person
|
||||
|
||||
// Jane's dog is null because we don't have access to Rex
|
||||
// and we have used $onError to catch the error
|
||||
friends[0].dog // => null
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Type Safety with Resolved Type
|
||||
|
||||
Jazz provides the `Resolved` type to help you define and enforce the structure of deeply loaded data in your application. This makes it easier to ensure that components receive the data they expect with proper TypeScript validation.
|
||||
|
||||
@@ -6,7 +6,6 @@ export type Example = {
|
||||
tech?: string[];
|
||||
features?: string[];
|
||||
demoUrl?: string;
|
||||
showDemo?: boolean;
|
||||
imageUrl?: string;
|
||||
codeSamples?: { name: string; content: React.ReactNode }[];
|
||||
};
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
- cojson-storage@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
- cojson-storage@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cojson": "workspace:0.13.30",
|
||||
"cojson": "workspace:0.13.31",
|
||||
"cojson-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# cojson
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d63716a: Fix removing members when the admin doesn't have access to the parent group readkeys
|
||||
- d5edad7: Group invites: restore support for role upgrades and inviting revoked members
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||
"typescript": "catalog:"
|
||||
|
||||
@@ -1044,11 +1044,6 @@ export class CoValueCore {
|
||||
});
|
||||
peer.trackLoadRequestSent(this.id);
|
||||
|
||||
const timeoutDuration =
|
||||
peer.role === "storage"
|
||||
? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
|
||||
: CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const markNotFound = () => {
|
||||
if (this.peers.get(peer.id)?.type === "pending") {
|
||||
@@ -1060,7 +1055,7 @@ export class CoValueCore {
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(markNotFound, timeoutDuration);
|
||||
const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
|
||||
const removeCloseListener = peer.addCloseListener(markNotFound);
|
||||
|
||||
const listener = (state: CoValueCore) => {
|
||||
|
||||
@@ -606,9 +606,12 @@ export class RawGroup<
|
||||
parent.core.getCurrentReadKey();
|
||||
|
||||
if (!parentReadKeySecret) {
|
||||
throw new Error(
|
||||
// We can't reveal the new child key to the parent group where we don't have access to the parent read key
|
||||
// TODO: This will be fixed with: https://github.com/garden-co/jazz/issues/1979
|
||||
logger.warn(
|
||||
"Can't reveal new child key to parent where we don't have access to the parent read key",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.set(
|
||||
|
||||
@@ -544,7 +544,8 @@ export class LocalNode {
|
||||
groupAsInvite.core.verified,
|
||||
{ forceOverwrite: true },
|
||||
);
|
||||
group.core.internalShamefullyResetCachedContent();
|
||||
|
||||
group.processNewTransactions();
|
||||
|
||||
group.core.notifyUpdate("immediate");
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@ function determineValidTransactionsForGroup(
|
||||
const writeOnlyKeys: Record<RawAccountID | AgentID, KeyID> = {};
|
||||
const validTransactions: ValidTransactionsResult[] = [];
|
||||
|
||||
const keyRevelations = new Set<string>();
|
||||
const writeKeys = new Set<string>();
|
||||
|
||||
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
||||
@@ -316,23 +315,6 @@ function determineValidTransactionsForGroup(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
418
packages/cojson/src/tests/group.inheritance.test.ts
Normal file
418
packages/cojson/src/tests/group.inheritance.test.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
createThreeConnectedNodes,
|
||||
createTwoConnectedNodes,
|
||||
loadCoValueOrFail,
|
||||
} from "./testUtils";
|
||||
|
||||
describe("extend", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
test("a user should be able to extend a group when his role on the parent group is writer", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("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 childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("a user should be able to extend a group when his role on the parent group is reader", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("a user should be able to extend a group when his role on the parent group is writeOnly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("self-extend a group should not break anything", async () => {
|
||||
const { node1 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.extend(group);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("test", "Hello!");
|
||||
|
||||
expect(map.get("test")).toEqual("Hello!");
|
||||
});
|
||||
|
||||
test("should not break when introducing extend cycles", async () => {
|
||||
const { node1 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
const group2 = node1.node.createGroup();
|
||||
const group3 = node1.node.createGroup();
|
||||
|
||||
group.extend(group2);
|
||||
group2.extend(group3);
|
||||
group3.extend(group);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("test", "Hello!");
|
||||
|
||||
expect(map.get("test")).toEqual("Hello!");
|
||||
});
|
||||
|
||||
test("a writerInvite role should not be inherited", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writerInvite",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group);
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unextend", () => {
|
||||
test("should revoke roles", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
// `parentGroup` has `alice` as a writer
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const alice = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
parentGroup.addMember(alice, "writer");
|
||||
// `alice`'s role in `parentGroup` is `"writer"`
|
||||
expect(parentGroup.roleOf(alice.id)).toBe("writer");
|
||||
|
||||
// `childGroup` has `bob` as a reader
|
||||
const childGroup = node1.node.createGroup();
|
||||
const bob = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
childGroup.addMember(bob, "reader");
|
||||
// `bob`'s role in `childGroup` is `"reader"`
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
|
||||
// `childGroup` has `parentGroup`'s members (in this case, `alice` as a writer)
|
||||
childGroup.extend(parentGroup);
|
||||
expect(childGroup.roleOf(alice.id)).toBe("writer");
|
||||
|
||||
// `childGroup` no longer has `parentGroup`'s members
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
expect(childGroup.roleOf(alice.id)).toBe(undefined);
|
||||
});
|
||||
|
||||
test("should do nothing if applied to a group that is not extended", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const alice = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
parentGroup.addMember(alice, "writer");
|
||||
const childGroup = node1.node.createGroup();
|
||||
const bob = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
childGroup.addMember(bob, "reader");
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
expect(childGroup.roleOf(alice.id)).toBe(undefined);
|
||||
});
|
||||
|
||||
test("should not throw if the revokeExtend is called twice", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const alice = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
parentGroup.addMember(alice, "writer");
|
||||
const childGroup = node1.node.createGroup();
|
||||
const bob = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
childGroup.addMember(bob, "reader");
|
||||
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
expect(childGroup.roleOf(alice.id)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend with role mapping", () => {
|
||||
test("mapping to writer should add the ability to write", 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, "writer");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
const map = childGroup.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("Written from the admin");
|
||||
mapOnNode2.set("test", "Written from the inherited role");
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the inherited role");
|
||||
|
||||
await mapOnNode2.core.waitForSync();
|
||||
|
||||
expect(map.get("test")).toEqual("Written from the inherited role");
|
||||
});
|
||||
|
||||
test("mapping to reader should remove the ability to write", 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, "reader");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
|
||||
const map = childGroup.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("Written from the admin");
|
||||
|
||||
mapOnNode2.set("test", "Should not be visible");
|
||||
|
||||
await mapOnNode2.core.waitForSync();
|
||||
|
||||
expect(map.get("test")).toEqual("Written from the admin");
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
|
||||
test("mapping to admin should add the ability to add members", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group, "admin");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("admin");
|
||||
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
childGroupOnNode2.addMember(
|
||||
await loadCoValueOrFail(node2.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
expect(childGroupOnNode2.roleOf(node3.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("mapping to reader should remove the ability to add members", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"admin",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group, "reader");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
const accountToAdd = await loadCoValueOrFail(node2.node, node3.accountID);
|
||||
|
||||
expect(() => {
|
||||
childGroupOnNode2.addMember(accountToAdd, "reader");
|
||||
}).toThrow();
|
||||
|
||||
expect(childGroupOnNode2.roleOf(node3.accountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("non-inheritable roles should not give access to the child group when role mapping is used", 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, "reader");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
|
||||
const map = childGroup.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("invite roles should not give write access to the child group when role mapping is used", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writerInvite",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group, "writer");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
|
||||
const map = childGroup.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("Written from the admin"); // The invite roles have access to the readKey hence can read the values on inherited groups
|
||||
|
||||
mapOnNode2.set("test", "Should not be visible");
|
||||
|
||||
await mapOnNode2.core.waitForSync();
|
||||
|
||||
expect(map.get("test")).toEqual("Written from the admin");
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
});
|
||||
350
packages/cojson/src/tests/group.invite.test.ts
Normal file
350
packages/cojson/src/tests/group.invite.test.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { expectMap } from "../coValue.js";
|
||||
import { LogLevel, logger } from "../logger.js";
|
||||
import {
|
||||
SyncMessagesLog,
|
||||
loadCoValueOrFail,
|
||||
setupTestAccount,
|
||||
setupTestNode,
|
||||
waitFor,
|
||||
} from "./testUtils.js";
|
||||
|
||||
beforeEach(async () => {
|
||||
SyncMessagesLog.clear();
|
||||
setupTestNode({ isSyncServer: true });
|
||||
});
|
||||
|
||||
describe("Group invites", () => {
|
||||
test("should be able to accept a reader invite", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const newMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const person = group.createMap({
|
||||
name: "John Doe",
|
||||
});
|
||||
|
||||
const inviteSecret = group.createInvite("reader");
|
||||
|
||||
const personOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
person.id,
|
||||
);
|
||||
expect(personOnNewMemberNode.get("name")).toEqual(undefined);
|
||||
|
||||
await newMember.node.acceptInvite(group.id, inviteSecret);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
expectMap(personOnNewMemberNode.core.getCurrentContent()).get("name"),
|
||||
).toEqual("John Doe");
|
||||
});
|
||||
|
||||
const groupOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
group.id,
|
||||
);
|
||||
|
||||
expect(groupOnNewMemberNode.roleOf(newMember.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("should be able to accept a writer invite", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const newMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const person = group.createMap({
|
||||
name: "John Doe",
|
||||
});
|
||||
|
||||
const inviteSecret = group.createInvite("writer");
|
||||
|
||||
const personOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
person.id,
|
||||
);
|
||||
expect(personOnNewMemberNode.get("name")).toEqual(undefined);
|
||||
|
||||
await newMember.node.acceptInvite(group.id, inviteSecret);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
expectMap(personOnNewMemberNode.core.getCurrentContent()).get("name"),
|
||||
).toEqual("John Doe");
|
||||
});
|
||||
|
||||
const groupOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
group.id,
|
||||
);
|
||||
|
||||
expect(groupOnNewMemberNode.roleOf(newMember.accountID)).toEqual("writer");
|
||||
|
||||
// Verify write access
|
||||
personOnNewMemberNode.set("name", "Jane Doe");
|
||||
expect(personOnNewMemberNode.get("name")).toEqual("Jane Doe");
|
||||
});
|
||||
|
||||
test("should be able to accept a writeOnly invite", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const newMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const person = group.createMap({
|
||||
name: "John Doe",
|
||||
});
|
||||
|
||||
const inviteSecret = group.createInvite("writeOnly");
|
||||
|
||||
const personOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
person.id,
|
||||
);
|
||||
expect(personOnNewMemberNode.get("name")).toEqual(undefined);
|
||||
|
||||
await newMember.node.acceptInvite(group.id, inviteSecret);
|
||||
|
||||
const groupOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
group.id,
|
||||
);
|
||||
|
||||
expect(groupOnNewMemberNode.roleOf(newMember.accountID)).toEqual(
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
// Should not be able to read
|
||||
expect(personOnNewMemberNode.get("name")).toEqual(undefined);
|
||||
|
||||
// Should be able to write
|
||||
personOnNewMemberNode.set("name", "Jane Doe");
|
||||
expect(personOnNewMemberNode.get("name")).toEqual("Jane Doe");
|
||||
});
|
||||
|
||||
test("should be able to accept an admin invite", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const newMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const person = group.createMap({
|
||||
name: "John Doe",
|
||||
});
|
||||
|
||||
const inviteSecret = group.createInvite("admin");
|
||||
|
||||
const personOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
person.id,
|
||||
);
|
||||
expect(personOnNewMemberNode.get("name")).toEqual(undefined);
|
||||
|
||||
await newMember.node.acceptInvite(group.id, inviteSecret);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
expectMap(personOnNewMemberNode.core.getCurrentContent()).get("name"),
|
||||
).toEqual("John Doe");
|
||||
});
|
||||
|
||||
const groupOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
group.id,
|
||||
);
|
||||
|
||||
expect(groupOnNewMemberNode.roleOf(newMember.accountID)).toEqual("admin");
|
||||
|
||||
// Verify admin access by adding another member
|
||||
const reader = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const readerOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
reader.accountID,
|
||||
);
|
||||
groupOnNewMemberNode.addMember(readerOnNewMemberNode, "reader");
|
||||
|
||||
const personOnReaderNode = await loadCoValueOrFail(reader.node, person.id);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
expectMap(personOnReaderNode.core.getCurrentContent()).get("name"),
|
||||
).toEqual("John Doe");
|
||||
});
|
||||
});
|
||||
|
||||
test("should not be able to accept an invite twice", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const newMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const inviteSecret = group.createInvite("reader");
|
||||
|
||||
await newMember.node.acceptInvite(group.id, inviteSecret);
|
||||
|
||||
const groupOnNewMemberNode = await loadCoValueOrFail(
|
||||
newMember.node,
|
||||
group.id,
|
||||
);
|
||||
|
||||
expect(groupOnNewMemberNode.roleOf(newMember.accountID)).toEqual("reader");
|
||||
|
||||
// Try to accept the same invite again
|
||||
await newMember.node.acceptInvite(group.id, inviteSecret);
|
||||
expect(groupOnNewMemberNode.roleOf(newMember.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("invites should not downgrade the role of an existing member", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const member = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const person = group.createMap({
|
||||
name: "John Doe",
|
||||
});
|
||||
|
||||
// First add member as admin
|
||||
const memberAccount = await loadCoValueOrFail(
|
||||
member.node,
|
||||
member.accountID,
|
||||
);
|
||||
group.addMember(memberAccount, "admin");
|
||||
|
||||
// Create a reader invite
|
||||
const inviteSecret = group.createInvite("reader");
|
||||
|
||||
// Try to accept the lower-privilege invite
|
||||
await member.node.acceptInvite(group.id, inviteSecret);
|
||||
|
||||
const groupOnMemberNode = await loadCoValueOrFail(member.node, group.id);
|
||||
expect(groupOnMemberNode.roleOf(member.accountID)).toEqual("admin");
|
||||
});
|
||||
|
||||
logger.setLevel(LogLevel.DEBUG);
|
||||
|
||||
test("invites should be able to upgrade the role of an existing member", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const member = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
|
||||
// First add member as reader
|
||||
const memberAccount = await loadCoValueOrFail(
|
||||
member.node,
|
||||
member.accountID,
|
||||
);
|
||||
group.addMember(memberAccount, "reader");
|
||||
|
||||
// Create an admin invite
|
||||
const inviteSecret = group.createInvite("admin");
|
||||
|
||||
const groupOnMemberNode = await loadCoValueOrFail(member.node, group.id);
|
||||
|
||||
// Accept the higher-privilege invite
|
||||
await member.node.acceptInvite(groupOnMemberNode.id, inviteSecret);
|
||||
|
||||
expect(groupOnMemberNode.roleOf(member.accountID)).toEqual("admin");
|
||||
|
||||
// Verify admin access by adding another member
|
||||
const reader = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
const readerAccount = await loadCoValueOrFail(
|
||||
member.node,
|
||||
reader.accountID,
|
||||
);
|
||||
groupOnMemberNode.addMember(readerAccount, "reader");
|
||||
});
|
||||
|
||||
test("invites should work on revoked members", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const member = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const person = group.createMap({
|
||||
name: "John Doe",
|
||||
});
|
||||
|
||||
// First add member as reader
|
||||
const memberAccount = await loadCoValueOrFail(
|
||||
member.node,
|
||||
member.accountID,
|
||||
);
|
||||
group.addMember(memberAccount, "reader");
|
||||
group.removeMember(memberAccount);
|
||||
|
||||
// Create a new reader invite
|
||||
const inviteSecret = group.createInvite("reader");
|
||||
|
||||
const groupOnMemberNode = await loadCoValueOrFail(member.node, group.id);
|
||||
|
||||
// Accept the invite after being revoked
|
||||
await member.node.acceptInvite(groupOnMemberNode.id, inviteSecret);
|
||||
|
||||
expect(groupOnMemberNode.roleOf(member.accountID)).toEqual("reader");
|
||||
|
||||
// Verify read access is restored
|
||||
const personOnMemberNode = await loadCoValueOrFail(member.node, person.id);
|
||||
expect(personOnMemberNode.get("name")).toEqual("John Doe");
|
||||
});
|
||||
|
||||
test("should not be able to accept an invalid invite", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const newMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const invalidInvite = "inviteSecret_zinvalid";
|
||||
|
||||
try {
|
||||
await newMember.node.acceptInvite(group.id, invalidInvite);
|
||||
throw new Error("Should not be able to accept invalid invite");
|
||||
} catch (e) {
|
||||
expect(e).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -252,4 +252,52 @@ describe("Group.removeMember", () => {
|
||||
|
||||
expect(groupOnReaderNode.roleOf(otherMember.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("removing a member when inheriting a group where the user lacks read rights", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const childAdmin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const reader = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const childAdminOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
childAdmin.accountID,
|
||||
);
|
||||
|
||||
const readerOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
reader.accountID,
|
||||
);
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
|
||||
const childGroup = admin.node.createGroup();
|
||||
childGroup.addMember(childAdminOnAdminNode, "admin");
|
||||
childGroup.addMember(readerOnAdminNode, "reader");
|
||||
|
||||
childGroup.extend(group);
|
||||
|
||||
const readerOnChildAdminNode = await loadCoValueOrFail(
|
||||
childAdmin.node,
|
||||
reader.accountID,
|
||||
);
|
||||
|
||||
const childGroupOnChildAdminNode = await loadCoValueOrFail(
|
||||
childAdmin.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
await childGroupOnChildAdminNode.removeMember(readerOnChildAdminNode);
|
||||
|
||||
expect(childGroupOnChildAdminNode.roleOf(reader.accountID)).toEqual(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
450
packages/cojson/src/tests/group.roleOf.test.ts
Normal file
450
packages/cojson/src/tests/group.roleOf.test.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { RawAccountID } from "../exports";
|
||||
import {
|
||||
createThreeConnectedNodes,
|
||||
createTwoConnectedNodes,
|
||||
loadCoValueOrFail,
|
||||
nodeWithRandomAgentAndSessionID,
|
||||
randomAgentAndSessionID,
|
||||
waitFor,
|
||||
} from "./testUtils";
|
||||
|
||||
describe("roleOf", () => {
|
||||
test("returns direct role assignments", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMember(agent2, "writer");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("returns undefined for non-members", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2] = randomAgentAndSessionID();
|
||||
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("revoked roles return undefined", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMember(agent2, "writer");
|
||||
group.removeMemberInternal(agent2);
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("everyone role applies to all accounts", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMemberInternal("everyone", "reader");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("account role overrides everyone role", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMemberInternal("everyone", "writer");
|
||||
group.addMember(agent2, "reader");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("Revoking access on everyone role should not affect existing members", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMemberInternal("everyone", "reader");
|
||||
group.addMember(agent2, "writer");
|
||||
group.removeMemberInternal("everyone");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("writer");
|
||||
expect(group.roleOfInternal("123" as RawAccountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("Everyone role is inherited following the most permissive algorithm", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
const parentGroup = node.createGroup();
|
||||
parentGroup.addMemberInternal("everyone", "writer");
|
||||
|
||||
group.extend(parentGroup);
|
||||
group.addMember(agent2, "reader");
|
||||
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("writer");
|
||||
});
|
||||
test("roleOf should prioritize explicit account role over everyone role in same group", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Add both everyone and specific account
|
||||
group.addMember("everyone", "reader");
|
||||
group.addMember(account2, "writer");
|
||||
|
||||
// Should return the explicit role, not everyone's role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Change everyone's role
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
// Should still return the explicit role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should prioritize inherited everyone role over explicit account role", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent and account to child
|
||||
parentGroup.addMember("everyone", "writer");
|
||||
childGroup.addMember(account2, "reader");
|
||||
|
||||
// Should return the explicit role from child, not inherited everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should use everyone role when no explicit role exists", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
// Add only everyone role
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
// Should return everyone's role when no explicit role exists
|
||||
expect(group.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should inherit everyone role from parent when no explicit roles exist", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent only
|
||||
parentGroup.addMember("everyone", "reader");
|
||||
|
||||
// Should inherit everyone's role from parent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should handle everyone role inheritance through multiple levels", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const grandParentGroup = node1.node.createGroup();
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance chain
|
||||
parentGroup.extend(grandParentGroup);
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to grandparent
|
||||
grandParentGroup.addMember("everyone", "writer");
|
||||
|
||||
// Should inherit everyone's role from grandparent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in parent
|
||||
parentGroup.addMember(account2, "reader");
|
||||
|
||||
// Should use parent's explicit role instead of grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in child
|
||||
childGroup.addMember(account2, "admin");
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should use child's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("admin");
|
||||
|
||||
// Remove child's explicit role
|
||||
await childGroupOnNode2.removeMember(account2);
|
||||
await childGroupOnNode2.core.waitForSync();
|
||||
|
||||
// Should fall back to parent's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Remove parent's explicit role
|
||||
await parentGroup.removeMember(account2);
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should fall back to grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
describe("writeOnly can be used as a role for everyone", () => {
|
||||
test("writeOnly can be used as a role for everyone", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
|
||||
test("switching from everyone reader to writeOnly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("test", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
|
||||
test("switching from everyone writer to writeOnly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("test", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
|
||||
test("switching from everyone writeOnly to reader", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
mapOnNode2.set("test", "Updated after the downgrade");
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from everyone");
|
||||
expect(mapOnNode2.get("fromAdmin")).toEqual("Written from admin");
|
||||
});
|
||||
|
||||
test("switching from everyone writeOnly to writer", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
mapOnNode2.set("test", "Updated after the upgrade");
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Updated after the upgrade");
|
||||
expect(mapOnNode2.get("fromAdmin")).toEqual("Written from admin");
|
||||
});
|
||||
|
||||
test("adding a reader member after writeOnly", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
const account3 = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
|
||||
group.addMember(account3, "reader");
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
test.skip("adding a reader member while creating the writeOnly keys", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
const account3 = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
group.addMember(account3, "reader");
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -499,855 +499,3 @@ describe("writeOnly", () => {
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the writeOnly member");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
test("a user should be able to extend a group when his role on the parent group is writer", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("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 childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("a user should be able to extend a group when his role on the parent group is reader", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("a user should be able to extend a group when his role on the parent group is writeOnly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writeOnly",
|
||||
);
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
const childGroup = node2.node.createGroup();
|
||||
childGroup.extend(groupOnNode2);
|
||||
|
||||
const map = childGroup.createMap();
|
||||
map.set("test", "Written from node2");
|
||||
|
||||
await map.core.waitForSync();
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
||||
});
|
||||
|
||||
test("self-extend a group should not break anything", async () => {
|
||||
const { node1 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.extend(group);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("test", "Hello!");
|
||||
|
||||
expect(map.get("test")).toEqual("Hello!");
|
||||
});
|
||||
|
||||
test("should not break when introducing extend cycles", async () => {
|
||||
const { node1 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
const group2 = node1.node.createGroup();
|
||||
const group3 = node1.node.createGroup();
|
||||
|
||||
group.extend(group2);
|
||||
group2.extend(group3);
|
||||
group3.extend(group);
|
||||
|
||||
const map = group.createMap();
|
||||
map.set("test", "Hello!");
|
||||
|
||||
expect(map.get("test")).toEqual("Hello!");
|
||||
});
|
||||
|
||||
test("a writerInvite role should not be inherited", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writerInvite",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group);
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unextend", () => {
|
||||
test("should revoke roles", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
// `parentGroup` has `alice` as a writer
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const alice = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
parentGroup.addMember(alice, "writer");
|
||||
// `alice`'s role in `parentGroup` is `"writer"`
|
||||
expect(parentGroup.roleOf(alice.id)).toBe("writer");
|
||||
|
||||
// `childGroup` has `bob` as a reader
|
||||
const childGroup = node1.node.createGroup();
|
||||
const bob = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
childGroup.addMember(bob, "reader");
|
||||
// `bob`'s role in `childGroup` is `"reader"`
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
|
||||
// `childGroup` has `parentGroup`'s members (in this case, `alice` as a writer)
|
||||
childGroup.extend(parentGroup);
|
||||
expect(childGroup.roleOf(alice.id)).toBe("writer");
|
||||
|
||||
// `childGroup` no longer has `parentGroup`'s members
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
expect(childGroup.roleOf(alice.id)).toBe(undefined);
|
||||
});
|
||||
|
||||
test("should do nothing if applied to a group that is not extended", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const alice = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
parentGroup.addMember(alice, "writer");
|
||||
const childGroup = node1.node.createGroup();
|
||||
const bob = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
childGroup.addMember(bob, "reader");
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
expect(childGroup.roleOf(alice.id)).toBe(undefined);
|
||||
});
|
||||
|
||||
test("should not throw if the revokeExtend is called twice", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const alice = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
parentGroup.addMember(alice, "writer");
|
||||
const childGroup = node1.node.createGroup();
|
||||
const bob = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
childGroup.addMember(bob, "reader");
|
||||
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
await childGroup.revokeExtend(parentGroup);
|
||||
expect(childGroup.roleOf(bob.id)).toBe("reader");
|
||||
expect(childGroup.roleOf(alice.id)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend with role mapping", () => {
|
||||
test("mapping to writer should add the ability to write", 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, "writer");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
const map = childGroup.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("Written from the admin");
|
||||
mapOnNode2.set("test", "Written from the inherited role");
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the inherited role");
|
||||
|
||||
await mapOnNode2.core.waitForSync();
|
||||
|
||||
expect(map.get("test")).toEqual("Written from the inherited role");
|
||||
});
|
||||
|
||||
test("mapping to reader should remove the ability to write", 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, "reader");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
|
||||
const map = childGroup.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("Written from the admin");
|
||||
|
||||
mapOnNode2.set("test", "Should not be visible");
|
||||
|
||||
await mapOnNode2.core.waitForSync();
|
||||
|
||||
expect(map.get("test")).toEqual("Written from the admin");
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
|
||||
test("mapping to admin should add the ability to add members", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group, "admin");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("admin");
|
||||
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
childGroupOnNode2.addMember(
|
||||
await loadCoValueOrFail(node2.node, node3.accountID),
|
||||
"reader",
|
||||
);
|
||||
|
||||
expect(childGroupOnNode2.roleOf(node3.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("mapping to reader should remove the ability to add members", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"admin",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group, "reader");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
const accountToAdd = await loadCoValueOrFail(node2.node, node3.accountID);
|
||||
|
||||
expect(() => {
|
||||
childGroupOnNode2.addMember(accountToAdd, "reader");
|
||||
}).toThrow();
|
||||
|
||||
expect(childGroupOnNode2.roleOf(node3.accountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("non-inheritable roles should not give access to the child group when role mapping is used", 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, "reader");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
|
||||
const map = childGroup.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("invite roles should not give write access to the child group when role mapping is used", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
group.addMember(
|
||||
await loadCoValueOrFail(node1.node, node2.accountID),
|
||||
"writerInvite",
|
||||
);
|
||||
|
||||
const childGroup = node1.node.createGroup();
|
||||
childGroup.extend(group, "writer");
|
||||
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual(undefined);
|
||||
|
||||
const map = childGroup.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("Written from the admin"); // The invite roles have access to the readKey hence can read the values on inherited groups
|
||||
|
||||
mapOnNode2.set("test", "Should not be visible");
|
||||
|
||||
await mapOnNode2.core.waitForSync();
|
||||
|
||||
expect(map.get("test")).toEqual("Written from the admin");
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("roleOf", () => {
|
||||
test("returns direct role assignments", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMember(agent2, "writer");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("returns undefined for non-members", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2] = randomAgentAndSessionID();
|
||||
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("revoked roles return undefined", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMember(agent2, "writer");
|
||||
group.removeMemberInternal(agent2);
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("everyone role applies to all accounts", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMemberInternal("everyone", "reader");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("account role overrides everyone role", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMemberInternal("everyone", "writer");
|
||||
group.addMember(agent2, "reader");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("Revoking access on everyone role should not affect existing members", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
group.addMemberInternal("everyone", "reader");
|
||||
group.addMember(agent2, "writer");
|
||||
group.removeMemberInternal("everyone");
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("writer");
|
||||
expect(group.roleOfInternal("123" as RawAccountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("Everyone role is inherited following the most permissive algorithm", () => {
|
||||
const node = nodeWithRandomAgentAndSessionID();
|
||||
const group = node.createGroup();
|
||||
const [agent2, sessionID2] = randomAgentAndSessionID();
|
||||
|
||||
const parentGroup = node.createGroup();
|
||||
parentGroup.addMemberInternal("everyone", "writer");
|
||||
|
||||
group.extend(parentGroup);
|
||||
group.addMember(agent2, "reader");
|
||||
|
||||
expect(group.roleOfInternal(agent2.id)).toEqual("writer");
|
||||
});
|
||||
test("roleOf should prioritize explicit account role over everyone role in same group", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Add both everyone and specific account
|
||||
group.addMember("everyone", "reader");
|
||||
group.addMember(account2, "writer");
|
||||
|
||||
// Should return the explicit role, not everyone's role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Change everyone's role
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
// Should still return the explicit role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should prioritize inherited everyone role over explicit account role", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent and account to child
|
||||
parentGroup.addMember("everyone", "writer");
|
||||
childGroup.addMember(account2, "reader");
|
||||
|
||||
// Should return the explicit role from child, not inherited everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should use everyone role when no explicit role exists", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
// Add only everyone role
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
// Should return everyone's role when no explicit role exists
|
||||
expect(group.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should inherit everyone role from parent when no explicit roles exist", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent only
|
||||
parentGroup.addMember("everyone", "reader");
|
||||
|
||||
// Should inherit everyone's role from parent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should handle everyone role inheritance through multiple levels", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const grandParentGroup = node1.node.createGroup();
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance chain
|
||||
parentGroup.extend(grandParentGroup);
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to grandparent
|
||||
grandParentGroup.addMember("everyone", "writer");
|
||||
|
||||
// Should inherit everyone's role from grandparent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in parent
|
||||
parentGroup.addMember(account2, "reader");
|
||||
|
||||
// Should use parent's explicit role instead of grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in child
|
||||
childGroup.addMember(account2, "admin");
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should use child's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("admin");
|
||||
|
||||
// Remove child's explicit role
|
||||
await childGroupOnNode2.removeMember(account2);
|
||||
await childGroupOnNode2.core.waitForSync();
|
||||
|
||||
// Should fall back to parent's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Remove parent's explicit role
|
||||
await parentGroup.removeMember(account2);
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should fall back to grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
describe("writeOnly can be used as a role for everyone", () => {
|
||||
test("writeOnly can be used as a role for everyone", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
|
||||
test("switching from everyone reader to writeOnly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("test", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
|
||||
test("switching from everyone writer to writeOnly", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("test", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
|
||||
test("switching from everyone writeOnly to reader", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
mapOnNode2.set("test", "Updated after the downgrade");
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from everyone");
|
||||
expect(mapOnNode2.get("fromAdmin")).toEqual("Written from admin");
|
||||
});
|
||||
|
||||
test("switching from everyone writeOnly to writer", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
mapOnNode2.set("test", "Updated after the upgrade");
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual("Updated after the upgrade");
|
||||
expect(mapOnNode2.get("fromAdmin")).toEqual("Written from admin");
|
||||
});
|
||||
|
||||
test("adding a reader member after writeOnly", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
const account3 = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
|
||||
group.addMember(account3, "reader");
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
test.skip("adding a reader member while creating the writeOnly keys", async () => {
|
||||
const { node1, node2, node3 } = await createThreeConnectedNodes(
|
||||
"server",
|
||||
"server",
|
||||
"server",
|
||||
);
|
||||
const account3 = await loadCoValueOrFail(node1.node, node3.accountID);
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
group.addMember("everyone", "writeOnly");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("fromAdmin", "Written from admin");
|
||||
|
||||
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
||||
|
||||
expect(groupOnNode2.myRole()).toEqual("writeOnly");
|
||||
|
||||
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
||||
group.addMember(account3, "reader");
|
||||
|
||||
expect(mapOnNode2.get("test")).toEqual(undefined);
|
||||
|
||||
mapOnNode2.set("test", "Written from everyone");
|
||||
|
||||
await waitFor(async () => {
|
||||
const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
|
||||
expect(mapOnNode1.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
|
||||
await group.core.waitForSync();
|
||||
|
||||
const mapOnNode3 = await loadCoValueOrFail(node3.node, map.id);
|
||||
|
||||
expect(mapOnNode3.get("test")).toEqual("Written from everyone");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1682,58 +1682,6 @@ test("WriteOnlyInvites can set writeKeys", () => {
|
||||
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(),
|
||||
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.contentInClonedNodeWithDifferentAccount(
|
||||
new ControlledAgent(inviteSecret, Crypto),
|
||||
),
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# jazz-auth-betterauth
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- jazz-tools@0.13.31
|
||||
- cojson@0.13.31
|
||||
- jazz-browser@0.13.31
|
||||
- jazz-betterauth-client-plugin@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-auth-betterauth",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# jazz-auth-clerk
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- jazz-tools@0.13.31
|
||||
- cojson@0.13.31
|
||||
- jazz-browser@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user