Compare commits

...

26 Commits

Author SHA1 Message Date
Anselm
ddb158d5fa Pre-release 2024-11-13 13:53:00 +00:00
Anselm
8b87117e0f Changeset (pre-release) 2024-11-13 13:45:44 +00:00
Anselm
e5000c2b6b Ensure storage implementations send parent group content first 2024-11-13 13:43:29 +00:00
Anselm
0347e52118 Remove unused imports 2024-11-13 11:48:36 +00:00
Anselm
bb9ba33e73 Formatting 2024-11-13 11:47:43 +00:00
Anselm
92c63d94b9 Treat extended groups (parents) as depended on CoValues for syncing 2024-11-13 11:46:24 +00:00
Anselm
75339c0939 Merge branch 'main' into aeplay-jazz-12 2024-11-12 15:27:29 +00:00
Anselm
22102deabc Merge branch 'main' into aeplay-jazz-12 2024-11-08 17:21:06 +00:00
Anselm
043e2acae4 Format 2024-11-06 16:03:58 +00:00
Anselm
1b7ef1c2c0 Merge branch 'main' into aeplay-jazz-12 2024-11-06 16:03:48 +00:00
Anselm
996092c26f Failing high-level test 2024-11-04 17:37:46 +00:00
Anselm
3c794bba0a Test for extending more than one level deep 2024-11-04 17:35:52 +00:00
Anselm
2d3d53d144 Test key rotation more than one level deep 2024-11-04 17:31:56 +00:00
Anselm
69d05c8c15 More high-level tests 2024-11-04 17:00:26 +00:00
Anselm
d11aeee083 Test revocation in high-level test 2024-11-04 15:54:56 +00:00
Anselm
4d5848161d Failing high-level test 2024-11-01 16:53:08 +00:00
Anselm
8ee456d4e4 Implement and test extend() 2024-11-01 16:44:50 +00:00
Anselm
7b2c2e6084 Rotate child keys on parent group key rotation 2024-11-01 16:38:26 +00:00
Anselm
133f75d34e Reveal new child read keys to parent read key 2024-11-01 16:08:57 +00:00
Anselm
68c9114896 Merge branch 'main' into aeplay-jazz-12 2024-11-01 15:42:27 +00:00
Anselm
57ff9e2d1f Implement and test lookup/revelation of parent read keys 2024-10-18 14:18:33 +01:00
Anselm
1cac820ec6 Add test for grand-parent inheritance 2024-10-18 13:16:43 +01:00
Anselm
eb4646beca Role inheritance (one level) 2024-10-18 12:05:43 +01:00
Anselm
5447d6f10b Start accepting parent and child extension transactions 2024-10-18 11:49:39 +01:00
Anselm
dbb040eb07 Prepare role getter that will traverse parent groups 2024-10-18 11:18:22 +01:00
Anselm
9960320645 Small refactors for valid transactions 2024-10-18 11:10:34 +01:00
67 changed files with 1709 additions and 300 deletions

39
.changeset/pre.json Normal file
View File

@@ -0,0 +1,39 @@
{
"mode": "pre",
"tag": "group-inheritance",
"initialVersions": {
"@jazz-e2e/binarycostream": "0.0.96",
"@jazz-e2e/covalues": "0.0.95",
"jazz-example-book-shelf": "0.1.11",
"jazz-example-chat": "0.0.97",
"jazz-example-chat-clerk": "0.0.95",
"chat-rn": "1.0.13",
"chat-rn-clerk": "1.0.11",
"chat-vue": "0.0.3",
"jazz-inspector": "0.0.71",
"jazz-example-music-player": "0.0.17",
"jazz-password-manager": "0.0.16",
"jazz-example-pets": "0.0.114",
"richtext": "0.0.0",
"jazz-example-todo": "0.0.113",
"cojson": "0.8.18",
"cojson-storage-indexeddb": "0.8.18",
"cojson-storage-sqlite": "0.8.18",
"cojson-transport-ws": "0.8.18",
"hash-slash": "0.2.1",
"jazz-browser": "0.8.18",
"jazz-browser-auth-clerk": "0.8.18",
"jazz-browser-media-images": "0.8.18",
"jazz-nodejs": "0.8.18",
"jazz-react": "0.8.18",
"jazz-react-auth-clerk": "0.8.18",
"jazz-react-native": "0.8.18",
"jazz-react-native-media-images": "0.8.14",
"jazz-run": "0.8.18",
"jazz-tools": "0.8.18",
"jazz-vue": "0.8.8"
},
"changesets": [
"real-steaks-drum"
]
}

View File

@@ -0,0 +1,8 @@
---
"cojson-storage-indexeddb": patch
"cojson-storage-sqlite": patch
"jazz-tools": patch
"cojson": patch
---
Implement Group Inheritance

View File

@@ -1,5 +1,14 @@
# @jazz-e2e/binarycostream
## 0.0.97-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.96
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@jazz-e2e/binarycostream",
"private": true,
"version": "0.0.96",
"version": "0.0.97-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,11 +13,11 @@
"test:ui": "playwright test --ui"
},
"dependencies": {
"cojson": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"hash-slash": "workspace:0.2.1",
"is-ci": "^3.0.1",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View File

@@ -1,5 +1,14 @@
# @jazz-e2e/covalues
## 0.0.96-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.95
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@jazz-e2e/covalues",
"private": true,
"version": "0.0.95",
"version": "0.0.96-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,14 @@
# jazz-example-book-shelf
## 0.1.12-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-browser-media-images@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.1.11
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-example-book-shelf",
"version": "0.1.11",
"version": "0.1.12-group-inheritance.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -11,9 +11,9 @@
},
"dependencies": {
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.8.18",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-browser-media-images": "workspace:0.8.19-group-inheritance.0",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"next": "14.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"

View File

@@ -1,5 +1,15 @@
# jazz-example-chat
## 0.0.96-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
- jazz-react-auth-clerk@0.8.19-group-inheritance.0
## 0.0.95
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat-clerk",
"private": true,
"version": "0.0.95",
"version": "0.0.96-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -17,11 +17,11 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"hash-slash": "workspace:0.2.1",
"jazz-react": "workspace:0.8.18",
"jazz-react-auth-clerk": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-react-auth-clerk": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,5 +1,15 @@
# chat-rn-clerk
## 1.0.12-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-react-auth-clerk@0.8.19-group-inheritance.0
- jazz-react-native@0.8.19-group-inheritance.0
- jazz-react-native-media-images@0.8.15-group-inheritance.0
## 1.0.11
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-clerk",
"main": "index.js",
"version": "1.0.11",
"version": "1.0.12-group-inheritance.0",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",

View File

@@ -1,5 +1,13 @@
# chat-rn
## 1.0.14-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-react-native@0.8.19-group-inheritance.0
## 1.0.13
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.13",
"version": "1.0.14-group-inheritance.0",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",

View File

@@ -1,5 +1,14 @@
# chat-vue
## 0.0.4-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-browser@0.8.19-group-inheritance.0
- jazz-vue@0.8.9-group-inheritance.0
## 0.0.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.3",
"version": "0.0.4-group-inheritance.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,14 @@
# jazz-example-chat
## 0.0.98-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.97
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.97",
"version": "0.0.98-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -18,10 +18,10 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"hash-slash": "workspace:0.2.1",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,5 +1,13 @@
# jazz-example-inspector
## 0.0.72-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- cojson@0.8.19-group-inheritance.0
- cojson-transport-ws@0.8.19-group-inheritance.0
## 0.0.71
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector",
"private": true,
"version": "0.0.71",
"version": "0.0.72-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.8.18",
"cojson-transport-ws": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
"hash-slash": "workspace:0.2.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",

View File

@@ -1,5 +1,13 @@
# jazz-example-musicplayer
## 0.0.18-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.17
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.17",
"version": "0.0.18-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -18,8 +18,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"lucide-react": "^0.274.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -1,5 +1,13 @@
# jazz-password-manager
## 0.0.17-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.16
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.16",
"version": "0.0.17-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,8 +12,8 @@
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
},
"dependencies": {
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.41.5",

View File

@@ -1,5 +1,14 @@
# jazz-example-pets
## 0.0.115-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-browser-media-images@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.114
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.114",
"version": "0.0.115-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -19,9 +19,9 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.8.18",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-browser-media-images": "workspace:0.8.19-group-inheritance.0",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
@@ -41,7 +41,7 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"is-ci": "^3.0.1",
"jazz-run": "workspace:0.8.18",
"jazz-run": "workspace:0.8.19-group-inheritance.0",
"postcss": "^8.4.27",
"tailwindcss": "3.3.2",
"typescript": "^5.3.3",

View File

@@ -1,5 +1,13 @@
# jazz-example-todo
## 0.0.114-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.0.113
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.113",
"version": "0.0.114-group-inheritance.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,5 +1,13 @@
# cojson-storage-indexeddb
## 0.8.19-group-inheritance.0
### Patch Changes
- 8b87117: Implement Group Inheritance
- Updated dependencies [8b87117]
- cojson@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,12 +1,12 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"main": "dist/index.js",
"type": "module",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"typescript": "^5.3.3"
},
"devDependencies": {

View File

@@ -692,9 +692,19 @@ function getDependedOnCoValues(
"key" in change &&
change.key,
)
.filter(
(key): key is CojsonInternalTypes.RawCoID =>
typeof key === "string" && key.startsWith("co_"),
.flatMap((key) =>
typeof key === "string"
? key.startsWith("co_")
? [key as CojsonInternalTypes.RawCoID]
: key.startsWith("parent_co_")
? [
key.replace(
"parent_",
"",
) as CojsonInternalTypes.RawCoID,
]
: []
: [],
);
}),
)

View File

@@ -1,5 +1,13 @@
# cojson-storage-sqlite
## 0.8.19-group-inheritance.0
### Patch Changes
- 8b87117: Implement Group Inheritance
- Updated dependencies [8b87117]
- cojson@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.5.2",
"cojson": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"typescript": "^5.3.3"
},
"devDependencies": {

View File

@@ -392,9 +392,19 @@ export class SQLiteStorage {
"key" in change &&
change.key,
)
.filter(
(key): key is CojsonInternalTypes.RawCoID =>
typeof key === "string" && key.startsWith("co_"),
.flatMap((key) =>
typeof key === "string"
? key.startsWith("co_")
? [key as CojsonInternalTypes.RawCoID]
: key.startsWith("parent_co_")
? [
key.replace(
"parent_",
"",
) as CojsonInternalTypes.RawCoID,
]
: []
: [],
);
}),
)

View File

@@ -1,5 +1,12 @@
# cojson-transport-nodejs-ws
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- cojson@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,12 +1,12 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"typescript": "^5.3.3"
},
"scripts": {

View File

@@ -1,5 +1,11 @@
# cojson
## 0.8.19-group-inheritance.0
### Patch Changes
- 8b87117: Implement Group Inheritance
## 0.8.18
### Patch Changes

View File

@@ -19,7 +19,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"devDependencies": {
"@types/jest": "^29.5.3",
"typescript": "^5.3.3",

View File

@@ -1,5 +1,5 @@
import { Result, err, ok } from "neverthrow";
import { AnyRawCoValue, RawCoValue } from "./coValue.js";
import { AnyRawCoValue, CoID, RawCoValue } from "./coValue.js";
import { ControlledAccountOrAgent, RawAccountID } from "./coValues/account.js";
import { RawGroup } from "./coValues/group.js";
import { coreToCoValue } from "./coreToCoValue.js";
@@ -785,6 +785,46 @@ export class CoValueCore {
}
}
// try to find revelation to parent group read keys
for (const co of content.keys()) {
if (co.startsWith("parent_")) {
const parentGroupID = co.slice("parent_".length) as CoID<RawGroup>;
const parentGroup = this.node.expectCoValueLoaded(
parentGroupID,
"Expected parent group to be loaded",
);
const parentKey = parentGroup.getCurrentReadKey();
if (!parentKey.secret) {
continue;
}
const revelationForParentKey = content.get(
`${keyID}_for_${parentKey.id}`,
);
if (revelationForParentKey) {
const secret = parentGroup.crypto.decryptKeySecret(
{
encryptedID: keyID,
encryptingID: parentKey.id,
encrypted: revelationForParentKey,
},
parentKey.secret,
);
if (secret) {
return secret as KeySecret;
} else {
console.error(
`Encrypting parent ${parentKey.id} key didn't decrypt ${keyID}`,
);
}
}
}
}
return undefined;
} else if (this.header.ruleset.type === "ownedByGroup") {
return this.node
@@ -950,9 +990,15 @@ export class CoValueCore {
/** @internal */
getDependedOnCoValuesUncached(): RawCoID[] {
return this.header.ruleset.type === "group"
? expectGroup(this.getCurrentContent())
.keys()
.filter((k): k is RawAccountID => k.startsWith("co_"))
? [
...expectGroup(this.getCurrentContent())
.keys()
.filter((k): k is RawAccountID => k.startsWith("co_")),
...expectGroup(this.getCurrentContent())
.keys()
.filter((k) => k.startsWith("parent_"))
.map((k) => k.replace("parent_", "") as RawCoID),
]
: this.header.ruleset.type === "ownedByGroup"
? [
this.header.ruleset.group,

View File

@@ -5,6 +5,7 @@ import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
import { AgentID, isAgentID } from "../ids.js";
import { JsonObject } from "../jsonValue.js";
import { Role } from "../permissions.js";
import { expectGroup } from "../typeUtils/expectGroup.js";
import {
ControlledAccountOrAgent,
RawAccount,
@@ -29,6 +30,8 @@ export type GroupShape = {
KeySecret,
{ encryptedID: KeyID; encryptingID: KeyID }
>;
[parent: `parent_${CoID<RawGroup>}`]: "extend";
[child: `child_${CoID<RawGroup>}`]: "extend";
};
/** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
@@ -61,12 +64,68 @@ export class RawGroup<
* @category 1. Role reading
*/
roleOf(accountID: RawAccountID): Role | undefined {
return this.roleOfInternal(accountID);
return this.roleOfInternal(accountID)?.role;
}
/** @internal */
roleOfInternal(accountID: RawAccountID | AgentID): Role | undefined {
return this.get(accountID);
roleOfInternal(
accountID: RawAccountID | AgentID | typeof EVERYONE,
): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
const roleHere = this.get(accountID);
if (roleHere === "revoked") {
return undefined;
}
let roleInfo:
| {
role: Exclude<Role, "revoked">;
via: CoID<RawGroup> | undefined;
}
| undefined = roleHere && { role: roleHere, via: undefined };
const parentGroups = this.getParentGroups();
for (const parentGroup of parentGroups) {
const roleInParent = parentGroup.roleOfInternal(accountID);
if (
roleInParent &&
roleInParent.role !== "revoked" &&
isMorePermissiveAndShouldInherit(roleInParent.role, roleInfo?.role)
) {
roleInfo = { role: roleInParent.role, via: parentGroup.id };
}
}
return roleInfo;
}
getParentGroups(): RawGroup[] {
return (
this.keys().filter((key) =>
key.startsWith("parent_"),
) as `parent_${CoID<RawGroup>}`[]
).map((parentKey) => {
const parent = this.core.node.expectCoValueLoaded(
parentKey.slice("parent_".length) as CoID<RawGroup>,
"Expected parent group to be loaded",
);
return expectGroup(parent.getCurrentContent());
});
}
getChildGroups(): RawGroup[] {
return (
this.keys().filter((key) =>
key.startsWith("child_"),
) as `child_${CoID<RawGroup>}`[]
).map((childKey) => {
const child = this.core.node.expectCoValueLoaded(
childKey.slice("child_".length) as CoID<RawGroup>,
"Expected child group to be loaded",
);
return expectGroup(child.getCurrentContent());
});
}
/**
@@ -75,7 +134,7 @@ export class RawGroup<
* @category 1. Role reading
*/
myRole(): Role | undefined {
return this.roleOfInternal(this.core.node.account.id);
return this.roleOfInternal(this.core.node.account.id)?.role;
}
/**
@@ -203,7 +262,75 @@ export class RawGroup<
"trusting",
);
console.log("Setting", `readKey`, "to", newReadKey.id, "in", this.id);
this.set("readKey", newReadKey.id, "trusting");
for (const parent of this.getParentGroups()) {
const { id: parentReadKeyID, secret: parentReadKeySecret } =
parent.core.getCurrentReadKey();
if (!parentReadKeySecret) {
throw new Error(
"Can't reveal new child key to parent where we don't have access to the parent read key",
);
}
console.log(
"Setting",
`${newReadKey.id}_for_${parentReadKeyID}`,
"in",
this.id,
);
this.set(
`${newReadKey.id}_for_${parentReadKeyID}`,
this.core.crypto.encryptKeySecret({
encrypting: {
id: parentReadKeyID,
secret: parentReadKeySecret,
},
toEncrypt: newReadKey,
}).encrypted,
"trusting",
);
}
for (const child of this.getChildGroups()) {
console.log("Rotating child", child.id);
child.rotateReadKey();
}
}
extend(parent: RawGroup) {
this.set(`parent_${parent.id}`, "extend", "trusting");
parent.set(`child_${this.id}`, "extend", "trusting");
const { id: parentReadKeyID, secret: parentReadKeySecret } =
parent.core.getCurrentReadKey();
if (!parentReadKeySecret) {
throw new Error("Can't extend group without parent read key secret");
}
const { id: childReadKeyID, secret: childReadKeySecret } =
this.core.getCurrentReadKey();
if (!childReadKeySecret) {
throw new Error("Can't extend group without child read key secret");
}
this.set(
`${childReadKeyID}_for_${parentReadKeyID}`,
this.core.crypto.encryptKeySecret({
encrypting: {
id: parentReadKeyID,
secret: parentReadKeySecret,
},
toEncrypt: {
id: childReadKeyID,
secret: childReadKeySecret,
},
}).encrypted,
"trusting",
);
}
/**
@@ -347,6 +474,34 @@ export class RawGroup<
}
}
function isMorePermissiveAndShouldInherit(
roleInParent: Role,
roleInChild: Exclude<Role, "revoked"> | undefined,
) {
// invites should never be inherited
if (
roleInParent === "adminInvite" ||
roleInParent === "writerInvite" ||
roleInParent === "readerInvite"
) {
return false;
}
if (roleInParent === "admin") {
return !roleInChild || roleInChild !== "admin";
}
if (roleInParent === "writer") {
return !roleInChild || roleInChild === "reader";
}
if (roleInParent === "reader") {
return !roleInChild;
}
return false;
}
export type InviteSecret = `inviteSecret_z${string}`;
function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {

View File

@@ -2,7 +2,7 @@ import { CoID } from "./coValue.js";
import { CoValueCore, Transaction } from "./coValueCore.js";
import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
import { MapOpPayload } from "./coValues/coMap.js";
import { EVERYONE, Everyone } from "./coValues/group.js";
import { EVERYONE, Everyone, RawGroup } from "./coValues/group.js";
import { KeyID } from "./crypto/crypto.js";
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
import { parseJSON } from "./jsonStringify.js";
@@ -28,198 +28,12 @@ export function determineValidTransactions(
coValue: CoValueCore,
): { txID: TransactionID; tx: Transaction }[] {
if (coValue.header.ruleset.type === "group") {
const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
([sessionID, sessionLog]) => {
return sessionLog.transactions.map((tx, txIndex) => ({
sessionID,
txIndex,
tx,
})) as {
sessionID: SessionID;
txIndex: number;
tx: Transaction;
}[];
},
);
allTransactionsSorted.sort((a, b) => {
return a.tx.madeAt - b.tx.madeAt;
});
const initialAdmin = coValue.header.ruleset.initialAdmin;
if (!initialAdmin) {
throw new Error("Group must have initialAdmin");
}
const memberState: {
[agent: RawAccountID | AgentID]: Role;
[EVERYONE]?: Role;
} = {};
const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
// console.log("before", { memberState, validTransactions });
const transactor = accountOrAgentIDfromSessionID(sessionID);
if (tx.privacy === "private") {
if (memberState[transactor] === "admin") {
validTransactions.push({
txID: { sessionID, txIndex },
tx,
});
continue;
} else {
console.warn("Only admins can make private transactions in groups");
continue;
}
}
let changes;
try {
changes = parseJSON(tx.changes);
} catch (e) {
console.warn(
coValue.id,
"Invalid JSON in transaction",
e,
tx,
JSON.stringify(tx.changes, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
),
);
continue;
}
const change = changes[0] as
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
| MapOpPayload<"readKey", JsonValue>
| MapOpPayload<"profile", CoID<RawProfile>>;
if (changes.length !== 1) {
console.warn("Group transaction must have exactly one change");
continue;
}
if (change.op !== "set") {
console.warn("Group transaction must set a role or readKey");
continue;
}
if (change.key === "readKey") {
if (memberState[transactor] !== "admin") {
console.warn("Only admins can set readKeys");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (change.key === "profile") {
if (memberState[transactor] !== "admin") {
console.warn("Only admins can set profile");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (
isKeyForKeyField(change.key) ||
isKeyForAccountField(change.key)
) {
if (
memberState[transactor] !== "admin" &&
memberState[transactor] !== "adminInvite" &&
memberState[transactor] !== "writerInvite" &&
memberState[transactor] !== "readerInvite"
) {
console.warn("Only admins can reveal keys");
continue;
}
// TODO: check validity of agents who the key is revealed to?
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
}
const affectedMember = change.key;
const assignedRole = change.value;
if (
change.value !== "admin" &&
change.value !== "writer" &&
change.value !== "reader" &&
change.value !== "revoked" &&
change.value !== "adminInvite" &&
change.value !== "writerInvite" &&
change.value !== "readerInvite"
) {
console.warn("Group transaction must set a valid role");
continue;
}
if (
affectedMember === EVERYONE &&
!(
change.value === "reader" ||
change.value === "writer" ||
change.value === "revoked"
)
) {
console.warn("Everyone can only be set to reader, writer or revoked");
continue;
}
const isFirstSelfAppointment =
!memberState[transactor] &&
transactor === initialAdmin &&
change.op === "set" &&
change.key === transactor &&
change.value === "admin";
if (!isFirstSelfAppointment) {
if (memberState[transactor] === "admin") {
if (
memberState[affectedMember] === "admin" &&
affectedMember !== transactor &&
assignedRole !== "admin"
) {
console.warn("Admins can only demote themselves.");
continue;
}
} else if (memberState[transactor] === "adminInvite") {
if (change.value !== "admin") {
console.warn("AdminInvites can only create admins.");
continue;
}
} else if (memberState[transactor] === "writerInvite") {
if (change.value !== "writer") {
console.warn("WriterInvites can only create writers.");
continue;
}
} else if (memberState[transactor] === "readerInvite") {
if (change.value !== "reader") {
console.warn("ReaderInvites can only create reader.");
continue;
}
} else {
console.warn(
"Group transaction must be made by current admin or invite",
);
continue;
}
}
memberState[affectedMember] = change.value;
validTransactions.push({ txID: { sessionID, txIndex }, tx });
// console.log("after", { memberState, validTransactions });
}
return validTransactions;
return determineValidTransactionsForGroup(coValue, initialAdmin);
} else if (coValue.header.ruleset.type === "ownedByGroup") {
const groupContent = expectGroup(
coValue.node
@@ -241,27 +55,18 @@ export function determineValidTransactions(
return sessionLog.transactions
.filter((tx) => {
const groupAtTime = groupContent.atTime(tx.madeAt);
const effectiveTransactor =
transactor === groupContent.id &&
groupAtTime instanceof RawAccount
? groupAtTime.currentAgentID().match(
(agentID) => agentID,
(e) => {
console.error(
"Error while determining current agent ID in valid transactions",
e,
);
return undefined;
},
)
: transactor;
const effectiveTransactor = agentInAccountOrMemberInGroup(
transactor,
groupAtTime,
);
if (!effectiveTransactor) {
return false;
}
const transactorRoleAtTxTime =
groupAtTime.get(effectiveTransactor) || groupAtTime.get(EVERYONE);
groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
groupAtTime.roleOfInternal(EVERYONE)?.role;
return (
transactorRoleAtTxTime === "admin" ||
@@ -291,6 +96,234 @@ export function determineValidTransactions(
}
}
function determineValidTransactionsForGroup(
coValue: CoValueCore,
initialAdmin: RawAccountID | AgentID,
): { txID: TransactionID; tx: Transaction }[] {
const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
([sessionID, sessionLog]) => {
return sessionLog.transactions.map((tx, txIndex) => ({
sessionID,
txIndex,
tx,
})) as {
sessionID: SessionID;
txIndex: number;
tx: Transaction;
}[];
},
);
allTransactionsSorted.sort((a, b) => {
return a.tx.madeAt - b.tx.madeAt;
});
const memberState: {
[agent: RawAccountID | AgentID]: Role;
[EVERYONE]?: Role;
} = {};
const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
// console.log("before", { memberState, validTransactions });
const transactor = accountOrAgentIDfromSessionID(sessionID);
if (tx.privacy === "private") {
if (memberState[transactor] === "admin") {
validTransactions.push({
txID: { sessionID, txIndex },
tx,
});
continue;
} else {
console.warn("Only admins can make private transactions in groups");
continue;
}
}
let changes;
try {
changes = parseJSON(tx.changes);
} catch (e) {
console.warn(
coValue.id,
"Invalid JSON in transaction",
e,
tx,
JSON.stringify(tx.changes, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
),
);
continue;
}
const change = changes[0] as
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
| MapOpPayload<"readKey", JsonValue>
| MapOpPayload<"profile", CoID<RawProfile>>
| MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
| MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
if (changes.length !== 1) {
console.warn("Group transaction must have exactly one change");
continue;
}
if (change.op !== "set") {
console.warn("Group transaction must set a role or readKey");
continue;
}
if (change.key === "readKey") {
if (memberState[transactor] !== "admin") {
console.warn("Only admins can set readKeys");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (change.key === "profile") {
if (memberState[transactor] !== "admin") {
console.warn("Only admins can set profile");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (
isKeyForKeyField(change.key) ||
isKeyForAccountField(change.key)
) {
if (
memberState[transactor] !== "admin" &&
memberState[transactor] !== "adminInvite" &&
memberState[transactor] !== "writerInvite" &&
memberState[transactor] !== "readerInvite"
) {
console.warn("Only admins can reveal keys");
continue;
}
// TODO: check validity of agents who the key is revealed to?
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (isParentExtension(change.key)) {
if (memberState[transactor] !== "admin") {
console.warn("Only admins can set parent extensions");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (isChildExtension(change.key)) {
if (memberState[transactor] !== "admin") {
console.warn("Only admins can set child extensions");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
}
const affectedMember = change.key;
const assignedRole = change.value;
if (
change.value !== "admin" &&
change.value !== "writer" &&
change.value !== "reader" &&
change.value !== "revoked" &&
change.value !== "adminInvite" &&
change.value !== "writerInvite" &&
change.value !== "readerInvite"
) {
console.warn("Group transaction must set a valid role");
continue;
}
if (
affectedMember === EVERYONE &&
!(
change.value === "reader" ||
change.value === "writer" ||
change.value === "revoked"
)
) {
console.warn("Everyone can only be set to reader, writer or revoked");
continue;
}
const isFirstSelfAppointment =
!memberState[transactor] &&
transactor === initialAdmin &&
change.op === "set" &&
change.key === transactor &&
change.value === "admin";
if (!isFirstSelfAppointment) {
if (memberState[transactor] === "admin") {
if (
memberState[affectedMember] === "admin" &&
affectedMember !== transactor &&
assignedRole !== "admin"
) {
console.warn("Admins can only demote themselves.");
continue;
}
} else if (memberState[transactor] === "adminInvite") {
if (change.value !== "admin") {
console.warn("AdminInvites can only create admins.");
continue;
}
} else if (memberState[transactor] === "writerInvite") {
if (change.value !== "writer") {
console.warn("WriterInvites can only create writers.");
continue;
}
} else if (memberState[transactor] === "readerInvite") {
if (change.value !== "reader") {
console.warn("ReaderInvites can only create reader.");
continue;
}
} else {
console.warn(
"Group transaction must be made by current admin or invite",
);
continue;
}
}
memberState[affectedMember] = change.value;
validTransactions.push({ txID: { sessionID, txIndex }, tx });
// console.log("after", { memberState, validTransactions });
}
return validTransactions;
}
function agentInAccountOrMemberInGroup(
transactor: RawAccountID | AgentID,
groupAtTime: RawGroup,
): RawAccountID | AgentID | undefined {
if (transactor === groupAtTime.id && groupAtTime instanceof RawAccount) {
return groupAtTime.currentAgentID().match(
(agentID) => agentID,
(e) => {
console.error(
"Error while determining current agent ID in valid transactions",
e,
);
return undefined;
},
);
}
return transactor;
}
export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
return co.startsWith("key_") && co.includes("_for_key");
}
@@ -304,3 +337,11 @@ export function isKeyForAccountField(
co.includes("_for_everyone")
);
}
function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
return key.startsWith("parent_");
}
function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
return key.startsWith("child_");
}

View File

@@ -146,7 +146,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
asDependencyOf || id,
);
} else if (!known?.header && coValue.header?.ruleset.type === "group") {
const dependedOnAccounts = new Set();
const dependedOnAccountsAndGroups = new Set();
for (const session of Object.values(coValue.sessionEntries)) {
for (const entry of session) {
for (const tx of entry.transactions) {
@@ -154,16 +154,24 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
const parsedChanges = JSON.parse(tx.changes);
for (const change of parsedChanges) {
if (change.op === "set" && change.key.startsWith("co_")) {
dependedOnAccounts.add(change.key);
dependedOnAccountsAndGroups.add(change.key);
}
if (
change.op === "set" &&
change.key.startsWith("parent_co_")
) {
dependedOnAccountsAndGroups.add(
change.key.replace("parent_", ""),
);
}
}
}
}
}
}
for (const account of dependedOnAccounts) {
for (const accountOrGroup of dependedOnAccountsAndGroups) {
await this.sendNewContent(
account as CoID<RawCoValue>,
accountOrGroup as CoID<RawCoValue>,
undefined,
asDependencyOf || id,
);

View File

@@ -1708,3 +1708,751 @@ test("Can give write permissions to 'everyone' (high-level)", async () => {
childContent2.set("foo", "bar2", "private");
expect(childContent2.get("foo")).toEqual("bar2");
});
test("Admins can set parent extensions", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
});
test("Writers, readers and invitees can not set parent extensions", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
const writer = node.createAccount();
const reader = node.createAccount();
const adminInvite = node.createAccount();
const writerInvite = node.createAccount();
const readerInvite = node.createAccount();
group.addMember(writer, "writer");
group.addMember(reader, "reader");
group.addMember(adminInvite, "adminInvite");
group.addMember(writerInvite, "writerInvite");
group.addMember(readerInvite, "readerInvite");
const groupAsWriter = expectGroup(
group.core
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
.getCurrentContent(),
);
groupAsWriter.set(`parent_${parentGroup.id}`, "extend", "trusting");
expect(groupAsWriter.get(`parent_${parentGroup.id}`)).toBeUndefined();
const groupAsReader = expectGroup(
group.core
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
groupAsReader.set(`parent_${parentGroup.id}`, "extend", "trusting");
expect(groupAsReader.get(`parent_${parentGroup.id}`)).toBeUndefined();
const groupAsAdminInvite = expectGroup(
group.core
.testWithDifferentAccount(
adminInvite,
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
)
.getCurrentContent(),
);
groupAsAdminInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
expect(groupAsAdminInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
const groupAsWriterInvite = expectGroup(
group.core
.testWithDifferentAccount(
writerInvite,
Crypto.newRandomSessionID(
writerInvite.currentAgentID()._unsafeUnwrap(),
),
)
.getCurrentContent(),
);
groupAsWriterInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
expect(groupAsWriterInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
const groupAsReaderInvite = expectGroup(
group.core
.testWithDifferentAccount(
readerInvite,
Crypto.newRandomSessionID(
readerInvite.currentAgentID()._unsafeUnwrap(),
),
)
.getCurrentContent(),
);
groupAsReaderInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
expect(groupAsReaderInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
});
test("Admins can set child extensions", () => {
const { group, node } = newGroupHighLevel();
const childGroup = node.createGroup();
group.set(`child_${childGroup.id}`, "extend", "trusting");
expect(group.get(`child_${childGroup.id}`)).toEqual("extend");
});
test("Writers, readers and invitees can not set child extensions", () => {
const { group, node } = newGroupHighLevel();
const childGroup = node.createGroup();
const writer = node.createAccount();
const reader = node.createAccount();
const adminInvite = node.createAccount();
const writerInvite = node.createAccount();
const readerInvite = node.createAccount();
group.addMember(writer, "writer");
group.addMember(reader, "reader");
group.addMember(adminInvite, "adminInvite");
group.addMember(writerInvite, "writerInvite");
group.addMember(readerInvite, "readerInvite");
const groupAsWriter = expectGroup(
group.core
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
.getCurrentContent(),
);
groupAsWriter.set(`child_${childGroup.id}`, "extend", "trusting");
expect(groupAsWriter.get(`child_${childGroup.id}`)).toBeUndefined();
const groupAsReader = expectGroup(
group.core
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
groupAsReader.set(`child_${childGroup.id}`, "extend", "trusting");
expect(groupAsReader.get(`child_${childGroup.id}`)).toBeUndefined();
const groupAsAdminInvite = expectGroup(
group.core
.testWithDifferentAccount(
adminInvite,
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
)
.getCurrentContent(),
);
groupAsAdminInvite.set(`child_${childGroup.id}`, "extend", "trusting");
expect(groupAsAdminInvite.get(`child_${childGroup.id}`)).toBeUndefined();
const groupAsWriterInvite = expectGroup(
group.core
.testWithDifferentAccount(
writerInvite,
Crypto.newRandomSessionID(
writerInvite.currentAgentID()._unsafeUnwrap(),
),
)
.getCurrentContent(),
);
groupAsWriterInvite.set(`child_${childGroup.id}`, "extend", "trusting");
expect(groupAsWriterInvite.get(`child_${childGroup.id}`)).toBeUndefined();
const groupAsReaderInvite = expectGroup(
group.core
.testWithDifferentAccount(
readerInvite,
Crypto.newRandomSessionID(
readerInvite.currentAgentID()._unsafeUnwrap(),
),
)
.getCurrentContent(),
);
groupAsReaderInvite.set(`child_${childGroup.id}`, "extend", "trusting");
expect(groupAsReaderInvite.get(`child_${childGroup.id}`)).toBeUndefined();
});
test("Member roles are inherited by child groups (except invites)", () => {
const { group, node, admin } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
const writer = node.createAccount();
const reader = node.createAccount();
const adminInvite = node.createAccount();
const writerInvite = node.createAccount();
const readerInvite = node.createAccount();
parentGroup.addMember(writer, "writer");
parentGroup.addMember(reader, "reader");
parentGroup.addMember(adminInvite, "adminInvite");
parentGroup.addMember(writerInvite, "writerInvite");
parentGroup.addMember(readerInvite, "readerInvite");
expect(group.roleOfInternal(admin.id)).toEqual({
role: "admin",
via: undefined,
});
expect(group.roleOfInternal(writer.id)).toEqual({
role: "writer",
via: parentGroup.id,
});
expect(group.roleOf(writer.id)).toEqual("writer");
expect(group.roleOfInternal(reader.id)).toEqual({
role: "reader",
via: parentGroup.id,
});
expect(group.roleOf(reader.id)).toEqual("reader");
expect(group.roleOfInternal(adminInvite.id)).toEqual(undefined);
expect(group.roleOf(adminInvite.id)).toEqual(undefined);
expect(group.roleOfInternal(writerInvite.id)).toEqual(undefined);
expect(group.roleOf(writerInvite.id)).toEqual(undefined);
expect(group.roleOfInternal(readerInvite.id)).toEqual(undefined);
expect(group.roleOf(readerInvite.id)).toEqual(undefined);
});
test("Member roles are inherited by grand-children groups (except invites)", () => {
const { group, node, admin } = newGroupHighLevel();
const parentGroup = node.createGroup();
const grandParentGroup = node.createGroup();
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
parentGroup.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
const writer = node.createAccount();
const reader = node.createAccount();
const adminInvite = node.createAccount();
const writerInvite = node.createAccount();
const readerInvite = node.createAccount();
grandParentGroup.addMember(writer, "writer");
grandParentGroup.addMember(reader, "reader");
grandParentGroup.addMember(adminInvite, "adminInvite");
grandParentGroup.addMember(writerInvite, "writerInvite");
grandParentGroup.addMember(readerInvite, "readerInvite");
expect(group.roleOfInternal(admin.id)).toEqual({
role: "admin",
via: undefined,
});
expect(group.roleOfInternal(writer.id)).toEqual({
role: "writer",
via: parentGroup.id,
});
expect(group.roleOf(writer.id)).toEqual("writer");
expect(group.roleOfInternal(reader.id)).toEqual({
role: "reader",
via: parentGroup.id,
});
expect(group.roleOf(reader.id)).toEqual("reader");
expect(group.roleOfInternal(adminInvite.id)).toEqual(undefined);
expect(group.roleOf(adminInvite.id)).toEqual(undefined);
expect(group.roleOfInternal(writerInvite.id)).toEqual(undefined);
expect(group.roleOf(writerInvite.id)).toEqual(undefined);
expect(group.roleOfInternal(readerInvite.id)).toEqual(undefined);
expect(group.roleOf(readerInvite.id)).toEqual(undefined);
});
test("Admins can reveal parent read keys to child groups", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
const parentReadKeyID = parentGroup.get("readKey");
if (!parentReadKeyID) {
throw new Error("Can't get parent group read key");
}
const readKeyID = group.get("readKey");
if (!readKeyID) {
throw new Error("Can't get group read key");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const encrypted = "fake_encrypted_key_secret" as any;
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
expect(group.get(`${readKeyID}_for_${parentReadKeyID}`)).toEqual(encrypted);
});
test("Writers, readers and invites can't reveal parent read keys to child groups", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
const parentReadKeyID = parentGroup.get("readKey");
if (!parentReadKeyID) {
throw new Error("Can't get parent group read key");
}
const readKeyID = group.get("readKey");
if (!readKeyID) {
throw new Error("Can't get group read key");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const encrypted = "fake_encrypted_key_secret" as any;
const writer = node.createAccount();
const reader = node.createAccount();
const adminInvite = node.createAccount();
const writerInvite = node.createAccount();
const readerInvite = node.createAccount();
group.addMember(writer, "writer");
group.addMember(reader, "reader");
group.addMember(adminInvite, "adminInvite");
group.addMember(writerInvite, "writerInvite");
group.addMember(readerInvite, "readerInvite");
const groupAsWriter = expectGroup(
group.core
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
.getCurrentContent(),
);
groupAsWriter.set(
`${readKeyID}_for_${parentReadKeyID}`,
encrypted,
"trusting",
);
expect(
groupAsWriter.get(`${readKeyID}_for_${parentReadKeyID}`),
).toBeUndefined();
const groupAsReader = expectGroup(
group.core
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
groupAsReader.set(
`${readKeyID}_for_${parentReadKeyID}`,
encrypted,
"trusting",
);
expect(
groupAsReader.get(`${readKeyID}_for_${parentReadKeyID}`),
).toBeUndefined();
const groupAsAdminInvite = expectGroup(
group.core
.testWithDifferentAccount(
adminInvite,
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
)
.getCurrentContent(),
);
groupAsAdminInvite.set(
`${readKeyID}_for_${parentReadKeyID}`,
encrypted,
"trusting",
);
expect(
groupAsAdminInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
).toBeUndefined();
const groupAsWriterInvite = expectGroup(
group.core
.testWithDifferentAccount(
writerInvite,
Crypto.newRandomSessionID(
writerInvite.currentAgentID()._unsafeUnwrap(),
),
)
.getCurrentContent(),
);
groupAsWriterInvite.set(
`${readKeyID}_for_${parentReadKeyID}`,
encrypted,
"trusting",
);
expect(
groupAsWriterInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
).toBeUndefined();
const groupAsReaderInvite = expectGroup(
group.core
.testWithDifferentAccount(
readerInvite,
Crypto.newRandomSessionID(
readerInvite.currentAgentID()._unsafeUnwrap(),
),
)
.getCurrentContent(),
);
groupAsReaderInvite.set(
`${readKeyID}_for_${parentReadKeyID}`,
encrypted,
"trusting",
);
expect(
groupAsReaderInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
).toBeUndefined();
});
test("Writers and readers in a parent group can read from an object owned by a child group", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
const parentReadKeyID = parentGroup.get("readKey");
const parentKey =
parentReadKeyID && parentGroup.core.getReadKey(parentReadKeyID);
if (!parentReadKeyID || !parentKey) {
throw new Error("Can't get parent group read key");
}
const readKeyID = group.get("readKey");
const readKey = readKeyID && group.core.getReadKey(readKeyID);
if (!readKeyID || !readKey) {
throw new Error("Can't get group read key");
}
const encrypted = node.crypto.encryptKeySecret({
toEncrypt: {
id: readKeyID,
secret: readKey,
},
encrypting: {
id: parentReadKeyID,
secret: parentKey,
},
}).encrypted;
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
const writer = node.createAccount();
const reader = node.createAccount();
parentGroup.addMember(writer, "writer");
parentGroup.addMember(reader, "reader");
const childObject = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
const childContent = expectMap(childObject.getCurrentContent());
childContent.set("foo", "bar", "private");
expect(childContent.get("foo")).toEqual("bar");
const childContentAsWriter = expectMap(
childObject
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
.getCurrentContent(),
);
expect(childContentAsWriter.get("foo")).toEqual("bar");
const childContentAsReader = expectMap(
childObject
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
expect(childContentAsReader.get("foo")).toEqual("bar");
});
test("Writers in a parent group can write to an object owned by a child group", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
const parentReadKeyID = parentGroup.get("readKey");
const parentKey =
parentReadKeyID && parentGroup.core.getReadKey(parentReadKeyID);
if (!parentReadKeyID || !parentKey) {
throw new Error("Can't get parent group read key");
}
const readKeyID = group.get("readKey");
const readKey = readKeyID && group.core.getReadKey(readKeyID);
if (!readKeyID || !readKey) {
throw new Error("Can't get group read key");
}
const encrypted = node.crypto.encryptKeySecret({
toEncrypt: {
id: readKeyID,
secret: readKey,
},
encrypting: {
id: parentReadKeyID,
secret: parentKey,
},
}).encrypted;
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
const writer = node.createAccount();
parentGroup.addMember(writer, "writer");
const childObject = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
const childContentAsWriter = expectMap(
childObject
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
.getCurrentContent(),
);
childContentAsWriter.set("foo", "bar", "private");
expect(childContentAsWriter.get("foo")).toEqual("bar");
});
test("When rotating the key of a child group, the new child key is exposed to the parent group", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
const currentReadKeyID = group.get("readKey");
if (!currentReadKeyID) {
throw new Error("Can't get group read key");
}
group.rotateReadKey();
const newReadKeyID = group.get("readKey");
if (!newReadKeyID) {
throw new Error("Can't get new group read key");
}
expect(newReadKeyID).not.toEqual(currentReadKeyID);
const parentReadKeyID = parentGroup.get("readKey");
if (!parentReadKeyID) {
throw new Error("Can't get parent group read key");
}
console.log("Checking", `${newReadKeyID}_for_${parentReadKeyID}`);
expect(group.get(`${newReadKeyID}_for_${parentReadKeyID}`)).toBeDefined();
});
test("When rotating the key of a parent group, the keys of all child groups are also rotated", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
parentGroup.set(`child_${group.id}`, "extend", "trusting");
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
group.rotateReadKey();
const currentChildReadKeyID = group.get("readKey");
if (!currentChildReadKeyID) {
throw new Error("Can't get group read key");
}
console.log("child id", group.id);
parentGroup.rotateReadKey();
const newChildReadKeyID = expectGroup(group.core.getCurrentContent()).get(
"readKey",
);
if (!newChildReadKeyID) {
throw new Error("Can't get new group read key");
}
expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
});
test("When rotating the key of a grand-parent group, the keys of all child and grand-child groups are also rotated", () => {
const { group, node } = newGroupHighLevel();
const grandParentGroup = node.createGroup();
const parentGroup = node.createGroup();
grandParentGroup.set(`child_${parentGroup.id}`, "extend", "trusting");
parentGroup.set(`child_${group.id}`, "extend", "trusting");
parentGroup.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
group.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
const currentGrandParentReadKeyID = grandParentGroup.get("readKey");
if (!currentGrandParentReadKeyID) {
throw new Error("Can't get grand-parent group read key");
}
const currentParentReadKeyID = parentGroup.get("readKey");
if (!currentParentReadKeyID) {
throw new Error("Can't get parent group read key");
}
const currentChildReadKeyID = group.get("readKey");
if (!currentChildReadKeyID) {
throw new Error("Can't get group read key");
}
grandParentGroup.rotateReadKey();
const newGrandParentReadKeyID = grandParentGroup.get("readKey");
if (!newGrandParentReadKeyID) {
throw new Error("Can't get new grand-parent group read key");
}
expect(newGrandParentReadKeyID).not.toEqual(currentGrandParentReadKeyID);
const newParentReadKeyID = expectGroup(
parentGroup.core.getCurrentContent(),
).get("readKey");
if (!newParentReadKeyID) {
throw new Error("Can't get new parent group read key");
}
expect(newParentReadKeyID).not.toEqual(currentParentReadKeyID);
const newChildReadKeyID = expectGroup(group.core.getCurrentContent()).get(
"readKey",
);
if (!newChildReadKeyID) {
throw new Error("Can't get new group read key");
}
expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
});
test("Calling extend on group sets up parent and child references and reveals child key to parent", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.extend(parentGroup);
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
expect(parentGroup.get(`child_${group.id}`)).toEqual("extend");
const parentReadKeyID = parentGroup.get("readKey");
if (!parentReadKeyID) {
throw new Error("Can't get parent group read key");
}
const childReadKeyID = group.get("readKey");
if (!childReadKeyID) {
throw new Error("Can't get group read key");
}
expect(group.get(`${childReadKeyID}_for_${parentReadKeyID}`)).toBeDefined();
const reader = node.createAccount();
parentGroup.addMember(reader, "reader");
const childObject = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
const childMap = expectMap(childObject.getCurrentContent());
childMap.set("foo", "bar", "private");
const childContentAsReader = expectMap(
childObject
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
expect(childContentAsReader.get("foo")).toEqual("bar");
});
test("Calling extend to create grand-child groups parent and child references and reveals child key to parent(s)", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
const grandParentGroup = node.createGroup();
group.extend(parentGroup);
parentGroup.extend(grandParentGroup);
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
expect(parentGroup.get(`parent_${grandParentGroup.id}`)).toEqual("extend");
expect(parentGroup.get(`child_${group.id}`)).toEqual("extend");
expect(grandParentGroup.get(`child_${parentGroup.id}`)).toEqual("extend");
const reader = node.createAccount();
grandParentGroup.addMember(reader, "reader");
const childObject = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
const childMap = expectMap(childObject.getCurrentContent());
childMap.set("foo", "bar", "private");
const childContentAsReader = expectMap(
childObject
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
expect(childContentAsReader.get("foo")).toEqual("bar");
});
test("High-level permissions work correctly when a group is extended", () => {
const { group, node } = newGroupHighLevel();
const parentGroup = node.createGroup();
group.extend(parentGroup);
const reader = node.createAccount();
parentGroup.addMember(reader, "reader");
const mapCore = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
const map = expectMap(mapCore.getCurrentContent());
map.set("foo", "bar", "private");
const mapAsReader = expectMap(
mapCore
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
expect(mapAsReader.get("foo")).toEqual("bar");
const groupKeyBeforeRemove = group.core.getCurrentReadKey().id;
parentGroup.removeMember(reader);
const groupKeyAfterRemove = group.core.getCurrentReadKey().id;
expect(groupKeyAfterRemove).not.toEqual(groupKeyBeforeRemove);
map.set("foo", "baz", "private");
const mapAsReaderAfterRemove = expectMap(
mapCore
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
.getCurrentContent(),
);
expect(mapAsReaderAfterRemove.get("foo")).not.toEqual("baz");
});

View File

@@ -1,5 +1,14 @@
# jazz-browser-media-images
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-browser@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,14 +1,14 @@
{
"name": "jazz-browser-auth-clerk",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.8.18",
"jazz-browser": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18"
"cojson": "workspace:0.8.19-group-inheritance.0",
"jazz-browser": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0"
},
"scripts": {
"format-and-lint": "biome check .",

View File

@@ -1,5 +1,13 @@
# jazz-browser-media-images
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- jazz-browser@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser-media-images",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
@@ -8,8 +8,8 @@
"dependencies": {
"@types/image-blob-reduce": "^4.1.1",
"image-blob-reduce": "^4.1.0",
"jazz-browser": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"jazz-browser": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"pica": "^9.0.1",
"typescript": "^5.3.3"
},

View File

@@ -1,5 +1,15 @@
# jazz-browser
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- cojson-storage-indexeddb@0.8.19-group-inheritance.0
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- cojson-transport-ws@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,16 +1,16 @@
{
"name": "jazz-browser",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"@scure/bip39": "^1.3.0",
"cojson": "workspace:0.8.18",
"cojson-storage-indexeddb": "workspace:0.8.18",
"cojson-transport-ws": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"cojson-storage-indexeddb": "workspace:0.8.19-group-inheritance.0",
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"typescript": "^5.3.3"
},
"scripts": {

View File

@@ -1,5 +1,14 @@
# jazz-autosub
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- cojson-transport-ws@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -5,11 +5,11 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"dependencies": {
"cojson": "workspace:0.8.18",
"cojson-transport-ws": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"ws": "^8.14.2"
},
"devDependencies": {

View File

@@ -1,5 +1,15 @@
# jazz-browser-media-images
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-browser-auth-clerk@0.8.19-group-inheritance.0
- jazz-react@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,15 +1,15 @@
{
"name": "jazz-react-auth-clerk",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.tsx",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.8.18",
"jazz-browser-auth-clerk": "workspace:0.8.18",
"jazz-react": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18"
"cojson": "workspace:0.8.19-group-inheritance.0",
"jazz-browser-auth-clerk": "workspace:0.8.19-group-inheritance.0",
"jazz-react": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0"
},
"peerDependencies": {
"react": "^18.2.0"

View File

@@ -1,5 +1,12 @@
# jazz-browser-media-images
## 0.8.15-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
## 0.8.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-react-native-media-images",
"version": "0.8.14",
"version": "0.8.15-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,14 @@
# jazz-browser
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- cojson-transport-ws@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-react-native",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -1,5 +1,14 @@
# jazz-react
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-browser@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -1,15 +1,15 @@
{
"name": "jazz-react",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"@scure/bip39": "^1.3.0",
"cojson": "workspace:0.8.18",
"jazz-browser": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"jazz-browser": "workspace:0.8.19-group-inheritance.0",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"typescript": "^5.3.3"
},
"devDependencies": {

View File

@@ -1,5 +1,15 @@
# jazz-run
## 0.8.19-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- cojson-storage-sqlite@0.8.19-group-inheritance.0
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- cojson-transport-ws@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"scripts": {
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
@@ -18,11 +18,11 @@
"@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.8.18",
"cojson-storage-sqlite": "workspace:0.8.18",
"cojson-transport-ws": "workspace:0.8.18",
"cojson": "workspace:0.8.19-group-inheritance.0",
"cojson-storage-sqlite": "workspace:0.8.19-group-inheritance.0",
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.8.18",
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
"ws": "^8.14.2"
},
"devDependencies": {

View File

@@ -1,5 +1,13 @@
# jazz-tools
## 0.8.19-group-inheritance.0
### Patch Changes
- 8b87117: Implement Group Inheritance
- Updated dependencies [8b87117]
- cojson@0.8.19-group-inheritance.0
## 0.8.18
### Patch Changes

View File

@@ -19,7 +19,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.8.18",
"version": "0.8.19-group-inheritance.0",
"dependencies": {
"cojson": "workspace:*",
"fast-check": "^3.17.2"

View File

@@ -142,6 +142,11 @@ export class Group extends CoValueBase implements CoValue {
return this;
}
removeMember(member: Everyone | Account) {
this._raw.removeMember(member === "everyone" ? member : member._raw);
return this;
}
get members() {
return this._raw
.keys()
@@ -172,6 +177,11 @@ export class Group extends CoValueBase implements CoValue {
});
}
extend(parent: Group) {
this._raw.extend(parent._raw);
return this;
}
/** @category Subscription & Loading */
static load<G extends Group, Depth>(
this: CoValueClass<G>,

View File

@@ -1,3 +1,4 @@
import { RawGroup } from "cojson";
import { describe, expect, test } from "vitest";
import { Account, CoMap, Group, WasmCrypto, co } from "../index.web.js";
@@ -85,3 +86,95 @@ describe("Custom accounts and groups", async () => {
expect(map._owner.castAs(CustomGroup).nMembers).toBe(2);
});
});
describe("Group inheritance", () => {
class TestMap extends CoMap {
title = co.string;
}
test("Group inheritance", async () => {
const me = await Account.create({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto,
});
const parentGroup = Group.create({ owner: me });
const group = Group.create({ owner: me });
group.extend(parentGroup);
console.log(
group.id,
group._raw.core.getDependedOnCoValuesUncached(),
parentGroup.id,
);
console.log(
(group._raw.core.getCurrentContent() as RawGroup)
.keys()
.filter((k) => k.startsWith("parent_"))
.map((k) => k.replace("parent_", "")),
);
const reader = await Account.createAs(me, {
creationProps: { name: "Reader" },
});
parentGroup.addMember(reader, "reader");
const mapInChild = TestMap.create({ title: "In Child" }, { owner: group });
const mapAsReader = await TestMap.load(mapInChild.id, reader, {});
expect(mapAsReader?.title).toBe("In Child");
parentGroup.removeMember(reader);
mapInChild.title = "In Child (updated)";
const mapAsReaderAfterUpdate = await TestMap.load(
mapInChild.id,
reader,
{},
);
expect(mapAsReaderAfterUpdate?.title).toBe("In Child");
});
test("Group inheritance with grand-children", async () => {
const me = await Account.create({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto,
});
const grandParentGroup = Group.create({ owner: me });
const parentGroup = Group.create({ owner: me });
const group = Group.create({ owner: me });
group.extend(parentGroup);
parentGroup.extend(grandParentGroup);
const reader = await Account.createAs(me, {
creationProps: { name: "Reader" },
});
grandParentGroup.addMember(reader, "reader");
const mapInGrandChild = TestMap.create(
{ title: "In Grand Child" },
{ owner: group },
);
const mapAsReader = await TestMap.load(mapInGrandChild.id, reader, {});
expect(mapAsReader?.title).toBe("In Grand Child");
grandParentGroup.removeMember(reader);
mapInGrandChild.title = "In Grand Child (updated)";
const mapAsReaderAfterUpdate = await TestMap.load(
mapInGrandChild.id,
reader,
{},
);
expect(mapAsReaderAfterUpdate?.title).toBe("In Grand Child");
});
});

View File

@@ -1,5 +1,14 @@
# jazz-react
## 0.8.9-group-inheritance.0
### Patch Changes
- Updated dependencies [8b87117]
- jazz-tools@0.8.19-group-inheritance.0
- cojson@0.8.19-group-inheritance.0
- jazz-browser@0.8.19-group-inheritance.0
## 0.8.8
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-vue",
"version": "0.8.8",
"version": "0.8.9-group-inheritance.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",