From cd48685b72be8d077b1c11d4ba52f60619c188c1 Mon Sep 17 00:00:00 2001 From: Anselm Date: Mon, 14 Aug 2023 15:10:09 +0100 Subject: [PATCH 1/2] First step to make accounts teams --- src/account.ts | 115 +++++++++ src/coValue.test.ts | 48 ++-- src/coValue.ts | 176 ++++--------- src/contentType.test.ts | 63 ++--- src/crypto.ts | 49 +++- src/ids.ts | 10 +- src/index.ts | 37 +-- src/node.ts | 133 ++++++---- src/permissions.test.ts | 539 ++++++++++++++++------------------------ src/permissions.ts | 67 +++-- src/sync.test.ts | 394 ++++++++++++++--------------- src/testUtils.ts | 79 ++++++ 12 files changed, 854 insertions(+), 856 deletions(-) create mode 100644 src/account.ts create mode 100644 src/testUtils.ts diff --git a/src/account.ts b/src/account.ts new file mode 100644 index 000000000..551568ef2 --- /dev/null +++ b/src/account.ts @@ -0,0 +1,115 @@ +import { CoValueHeader } from './coValue.js'; +import { CoValueID } from './contentType.js'; +import { AgentSecret, RecipientID, RecipientSecret, SignatoryID, SignatorySecret, getAgentID, getAgentRecipientID, getAgentRecipientSecret, getAgentSignatoryID, getAgentSignatorySecret } from './crypto.js'; +import { RawAgentID } from './ids.js'; +import { CoMap, LocalNode } from './index.js'; +import { Team, TeamContent } from './permissions.js'; + +export function accountHeaderForInitialAgentSecret(agentSecret: AgentSecret): CoValueHeader { + const rawAgentID = getAgentID(agentSecret); + return { + type: "comap", + ruleset: {type: "team", initialAdmin: rawAgentID}, + meta: { + type: "account" + }, + createdAt: null, + uniqueness: null, + } +} + +export class Account extends Team { + get id(): AccountID { + return this.teamMap.id; + } + + getCurrentAgentID(): RawAgentID { + const agents = this.teamMap.keys().filter((k): k is RawAgentID => k.startsWith("recipient_")); + + if (agents.length !== 1) { + throw new Error("Expected exactly one agent in account, got " + agents.length); + } + + return agents[0]!; + } +} + +export interface GeneralizedControlledAccount { + id: AccountIDOrAgentID; + agentSecret: AgentSecret; + + currentAgentID: () => RawAgentID; + currentSignatoryID: () => SignatoryID; + currentSignatorySecret: () => SignatorySecret; + currentRecipientID: () => RecipientID; + currentRecipientSecret: () => RecipientSecret; +} + +export class ControlledAccount extends Account implements GeneralizedControlledAccount { + agentSecret: AgentSecret; + + constructor(agentSecret: AgentSecret, teamMap: CoMap, node: LocalNode) { + super(teamMap, node); + + this.agentSecret = agentSecret; + } + + currentAgentID(): RawAgentID { + return getAgentID(this.agentSecret); + } + + currentSignatoryID(): SignatoryID { + return getAgentSignatoryID(this.currentAgentID()); + } + + currentSignatorySecret(): SignatorySecret { + return getAgentSignatorySecret(this.agentSecret); + } + + currentRecipientID(): RecipientID { + return getAgentRecipientID(this.currentAgentID()); + } + + currentRecipientSecret(): RecipientSecret { + return getAgentRecipientSecret(this.agentSecret); + } +} + +export class AnonymousControlledAccount implements GeneralizedControlledAccount { + agentSecret: AgentSecret; + + constructor(agentSecret: AgentSecret) { + this.agentSecret = agentSecret; + } + + get id(): RawAgentID { + return getAgentID(this.agentSecret); + } + + currentAgentID(): RawAgentID { + return getAgentID(this.agentSecret); + } + + currentSignatoryID(): SignatoryID { + return getAgentSignatoryID(this.currentAgentID()); + } + + currentSignatorySecret(): SignatorySecret { + return getAgentSignatorySecret(this.agentSecret); + } + + currentRecipientID(): RecipientID { + return getAgentRecipientID(this.currentAgentID()); + } + + currentRecipientSecret(): RecipientSecret { + return getAgentRecipientSecret(this.agentSecret); + } +} + +export type AccountMeta = {type: "account"}; +export type AccountID = CoValueID>; + +export type AccountIDOrAgentID = RawAgentID | AccountID; +export type AccountOrAgentID = RawAgentID | Account; +export type AccountOrAgentSecret = AgentSecret | Account; diff --git a/src/coValue.test.ts b/src/coValue.test.ts index 340afb2b8..5d4f8a88d 100644 --- a/src/coValue.test.ts +++ b/src/coValue.test.ts @@ -1,25 +1,17 @@ -import { - Transaction, - getAgent, - getAgentID, - newRandomAgentCredential, - newRandomSessionID, -} from './coValue.js'; -import { LocalNode } from './node.js'; -import { createdNowUnique, sign } from './crypto.js'; +import { Transaction } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { createdNowUnique, getAgentSignatorySecret, newRandomAgentSecret, sign } from "./crypto.js"; +import { randomAnonymousAccountAndSessionID } from "./testUtils.js"; test("Can create coValue with new agent credentials and add transaction to it", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const [account, sessionID] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(account, sessionID); const coValue = node.createCoValue({ type: "costream", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const transaction: Transaction = { @@ -42,24 +34,21 @@ test("Can create coValue with new agent credentials and add transaction to it", node.ownSessionID, [transaction], expectedNewHash, - sign(agentCredential.signatorySecret, expectedNewHash) + sign(account.currentSignatorySecret(), expectedNewHash) ) ).toBe(true); }); test("transactions with wrong signature are rejected", () => { - const wrongAgent = newRandomAgentCredential("wrongAgent"); - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const wrongAgent = newRandomAgentSecret(); + const [agentSecret, sessionID] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(agentSecret, sessionID); const coValue = node.createCoValue({ type: "costream", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const transaction: Transaction = { @@ -82,23 +71,20 @@ test("transactions with wrong signature are rejected", () => { node.ownSessionID, [transaction], expectedNewHash, - sign(wrongAgent.signatorySecret, expectedNewHash) + sign(getAgentSignatorySecret(wrongAgent), expectedNewHash) ) ).toBe(false); }); test("transactions with correctly signed, but wrong hash are rejected", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const [account, sessionID] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(account, sessionID); const coValue = node.createCoValue({ type: "costream", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const transaction: Transaction = { @@ -131,7 +117,7 @@ test("transactions with correctly signed, but wrong hash are rejected", () => { node.ownSessionID, [transaction], expectedNewHash, - sign(agentCredential.signatorySecret, expectedNewHash) + sign(account.currentSignatorySecret(), expectedNewHash) ) ).toBe(false); }); diff --git a/src/coValue.ts b/src/coValue.ts index a8ff227b0..e1967e9ac 100644 --- a/src/coValue.ts +++ b/src/coValue.ts @@ -1,22 +1,14 @@ import { randomBytes } from "@noble/hashes/utils"; -import { ContentType } from './contentType.js'; -import { Static } from './contentTypes/static.js'; -import { CoStream } from './contentTypes/coStream.js'; -import { CoMap } from './contentTypes/coMap.js'; +import { ContentType } from "./contentType.js"; +import { Static } from "./contentTypes/static.js"; +import { CoStream } from "./contentTypes/coStream.js"; +import { CoMap } from "./contentTypes/coMap.js"; import { Encrypted, Hash, KeySecret, - RecipientID, - RecipientSecret, - SignatoryID, - SignatorySecret, Signature, StreamingHash, - getRecipientID, - getSignatoryID, - newRandomRecipient, - newRandomSignatory, openAs, shortHash, sign, @@ -25,34 +17,37 @@ import { decryptForTransaction, KeyID, unsealKeySecret, - signatorySecretToBytes, - recipientSecretToBytes, - signatorySecretFromBytes, - recipientSecretFromBytes, -} from './crypto.js'; -import { JsonValue } from './jsonValue.js'; + getAgentSignatoryID, + getAgentRecipientID, +} from "./crypto.js"; +import { JsonObject, JsonValue } from "./jsonValue.js"; import { base58 } from "@scure/base"; import { PermissionsDef as RulesetDef, Team, determineValidTransactions, expectTeamContent, -} from './permissions.js'; -import { LocalNode } from './node.js'; -import { CoValueKnownState, NewContentMessage } from './sync.js'; -import { AgentID, RawCoValueID, SessionID, TransactionID } from './ids.js'; -import { CoList } from './contentTypes/coList.js'; +} from "./permissions.js"; +import { LocalNode } from "./node.js"; +import { CoValueKnownState, NewContentMessage } from "./sync.js"; +import { RawCoValueID, SessionID, TransactionID } from "./ids.js"; +import { CoList } from "./contentTypes/coList.js"; +import { + AccountID, + AccountIDOrAgentID, + GeneralizedControlledAccount, +} from "./account.js"; export type CoValueHeader = { type: ContentType["type"]; ruleset: RulesetDef; - meta: JsonValue; + meta: JsonObject | null; createdAt: `2${string}` | null; uniqueness: `z${string}` | null; publicNickname?: string; }; -function coValueIDforHeader(header: CoValueHeader): RawCoValueID { +export function coValueIDforHeader(header: CoValueHeader): RawCoValueID { const hash = shortHash(header); if (header.publicNickname) { return `co_${header.publicNickname}_z${hash.slice( @@ -63,12 +58,14 @@ function coValueIDforHeader(header: CoValueHeader): RawCoValueID { } } -export function agentIDfromSessionID(sessionID: SessionID): AgentID { - return sessionID.split("_session")[0] as AgentID; +export function accountOrAgentIDfromSessionID( + sessionID: SessionID +): AccountIDOrAgentID { + return sessionID.split("_session")[0] as AccountIDOrAgentID; } -export function newRandomSessionID(agentID: AgentID): SessionID { - return `${agentID}_session_z${base58.encode(randomBytes(8))}`; +export function newRandomSessionID(accountID: AccountIDOrAgentID): SessionID { + return `${accountID}_session_z${base58.encode(randomBytes(8))}`; } type SessionLog = { @@ -117,12 +114,12 @@ export class CoValue { this.node = node; } - testWithDifferentCredentials( - agentCredential: AgentCredential, + testWithDifferentAccount( + account: GeneralizedControlledAccount, ownSessionID: SessionID ): CoValue { - const newNode = this.node.testWithDifferentCredentials( - agentCredential, + const newNode = this.node.testWithDifferentAccount( + account, ownSessionID ); @@ -160,13 +157,18 @@ export class CoValue { newHash: Hash, newSignature: Signature ): boolean { - const signatoryID = this.node.expectAgentLoaded( - agentIDfromSessionID(sessionID), - "Expected to know signatory of transaction" - ).signatoryID; + const signatoryID = getAgentSignatoryID( + this.node.resolveAccount( + accountOrAgentIDfromSessionID(sessionID), + "Expected to know signatory of transaction" + ) + ); if (!signatoryID) { - console.warn("Unknown agent", agentIDfromSessionID(sessionID)); + console.warn( + "Unknown agent", + accountOrAgentIDfromSessionID(sessionID) + ); return false; } @@ -281,7 +283,7 @@ export class CoValue { ]); const signature = sign( - this.node.agentCredential.signatorySecret, + this.node.account.currentSignatorySecret(), expectedNewHash ); @@ -407,16 +409,18 @@ export class CoValue { for (const entry of readKeyHistory) { if (entry.value?.keyID === keyID) { - const revealer = agentIDfromSessionID(entry.txID.sessionID); - const revealerAgent = this.node.expectAgentLoaded( + const revealer = accountOrAgentIDfromSessionID( + entry.txID.sessionID + ); + const revealerAgent = this.node.resolveAccount( revealer, "Expected to know revealer" ); const secret = openAs( entry.value.revelation, - this.node.agentCredential.recipientSecret, - revealerAgent.recipientID, + this.node.account.currentRecipientSecret(), + getAgentRecipientID(revealerAgent), { in: this.id, tx: entry.txID, @@ -542,93 +546,11 @@ export class CoValue { return this.header.ruleset.type === "team" ? expectTeamContent(this.getCurrentContent()) .keys() - .filter((k): k is AgentID => k.startsWith("co_agent")) + .filter((k): k is AccountID => k.startsWith("co_")) : this.header.ruleset.type === "ownedByTeam" ? [this.header.ruleset.team] : []; } } -export type Agent = { - signatoryID: SignatoryID; - recipientID: RecipientID; - publicNickname?: string; -}; - -export function getAgent(agentCredential: AgentCredential) { - return { - signatoryID: getSignatoryID(agentCredential.signatorySecret), - recipientID: getRecipientID(agentCredential.recipientSecret), - publicNickname: agentCredential.publicNickname, - }; -} - -export function getAgentCoValueHeader(agent: Agent): CoValueHeader { - return { - type: "comap", - ruleset: { - type: "agent", - initialSignatoryID: agent.signatoryID, - initialRecipientID: agent.recipientID, - }, - meta: null, - createdAt: null, - uniqueness: null, - publicNickname: - "agent" + (agent.publicNickname ? `-${agent.publicNickname}` : ""), - }; -} - -export function getAgentID(agent: Agent): AgentID { - return coValueIDforHeader(getAgentCoValueHeader(agent)) as AgentID; -} - -export type AgentCredential = { - signatorySecret: SignatorySecret; - recipientSecret: RecipientSecret; - publicNickname?: string; -}; - -export function newRandomAgentCredential( - publicNickname?: string -): AgentCredential { - const signatorySecret = newRandomSignatory(); - const recipientSecret = newRandomRecipient(); - return { signatorySecret, recipientSecret, publicNickname }; -} - -export function agentCredentialToBytes(cred: AgentCredential): Uint8Array { - if (cred.publicNickname) { - throw new Error("Can't convert agent credential with publicNickname"); - } - const bytes = new Uint8Array(64); - const signatorySecretBytes = signatorySecretToBytes(cred.signatorySecret); - if (signatorySecretBytes.length !== 32) { - throw new Error("Invalid signatorySecret length"); - } - bytes.set(signatorySecretBytes); - const recipientSecretBytes = recipientSecretToBytes(cred.recipientSecret); - if (recipientSecretBytes.length !== 32) { - throw new Error("Invalid recipientSecret length"); - } - bytes.set(recipientSecretBytes, 32); - - return bytes; -} - -export function agentCredentialFromBytes( - bytes: Uint8Array -): AgentCredential | undefined { - if (bytes.length !== 64) { - return undefined; - } - - const signatorySecret = signatorySecretFromBytes(bytes.slice(0, 32)); - const recipientSecret = recipientSecretFromBytes(bytes.slice(32)); - - return { signatorySecret, recipientSecret }; -} - -// type Role = "admin" | "writer" | "reader"; - -// type PermissionsDef = CJMap; +export { SessionID }; diff --git a/src/contentType.test.ts b/src/contentType.test.ts index 3979c0316..5cdb4988e 100644 --- a/src/contentType.test.ts +++ b/src/contentType.test.ts @@ -1,25 +1,16 @@ -import { - agentIDfromSessionID, - getAgent, - getAgentID, - newRandomAgentCredential, - newRandomSessionID, -} from './coValue.js'; +import { accountOrAgentIDfromSessionID } from "./coValue.js"; import { createdNowUnique } from "./crypto.js"; import { LocalNode } from "./node.js"; +import { randomAnonymousAccountAndSessionID } from "./testUtils.js"; test("Empty COJSON Map works", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); const coValue = node.createCoValue({ type: "comap", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const content = coValue.getCurrentContent(); @@ -34,17 +25,13 @@ test("Empty COJSON Map works", () => { }); test("Can insert and delete Map entries in edit()", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); const coValue = node.createCoValue({ type: "comap", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const content = coValue.getCurrentContent(); @@ -67,17 +54,13 @@ test("Can insert and delete Map entries in edit()", () => { }); test("Can get map entry values at different points in time", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); const coValue = node.createCoValue({ type: "comap", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const content = coValue.getCurrentContent(); @@ -90,13 +73,13 @@ test("Can get map entry values at different points in time", () => { content.edit((editable) => { const beforeA = Date.now(); - while(Date.now() < beforeA + 10){} + while (Date.now() < beforeA + 10) {} editable.set("hello", "A", "trusting"); const beforeB = Date.now(); - while(Date.now() < beforeB + 10){} + while (Date.now() < beforeB + 10) {} editable.set("hello", "B", "trusting"); const beforeC = Date.now(); - while(Date.now() < beforeC + 10){} + while (Date.now() < beforeC + 10) {} editable.set("hello", "C", "trusting"); expect(editable.get("hello")).toEqual("C"); expect(editable.getAtTime("hello", Date.now())).toEqual("C"); @@ -107,17 +90,13 @@ test("Can get map entry values at different points in time", () => { }); test("Can get all historic values of key", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); const coValue = node.createCoValue({ type: "comap", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const content = coValue.getCurrentContent(); @@ -137,9 +116,7 @@ test("Can get all historic values of key", () => { const txDel = editable.getLastTxID("hello"); editable.set("hello", "C", "trusting"); const txC = editable.getLastTxID("hello"); - expect( - editable.getHistory("hello") - ).toEqual([ + expect(editable.getHistory("hello")).toEqual([ { txID: txA, value: "A", @@ -165,17 +142,13 @@ test("Can get all historic values of key", () => { }); test("Can get last tx ID for a key", () => { - const agentCredential = newRandomAgentCredential("agent1"); - const node = new LocalNode( - agentCredential, - newRandomSessionID(getAgentID(getAgent(agentCredential))) - ); + const node = new LocalNode(...randomAnonymousAccountAndSessionID()); const coValue = node.createCoValue({ type: "comap", ruleset: { type: "unsafeAllowAll" }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const content = coValue.getCurrentContent(); @@ -190,8 +163,8 @@ test("Can get last tx ID for a key", () => { expect(editable.getLastTxID("hello")).toEqual(undefined); editable.set("hello", "A", "trusting"); const sessionID = editable.getLastTxID("hello")?.sessionID; - expect(sessionID && agentIDfromSessionID(sessionID)).toEqual( - getAgentID(getAgent(agentCredential)) + expect(sessionID && accountOrAgentIDfromSessionID(sessionID)).toEqual( + node.account.id ); expect(editable.getLastTxID("hello")?.txIndex).toEqual(0); editable.set("hello", "B", "trusting"); diff --git a/src/crypto.ts b/src/crypto.ts index d8e23c569..9553207d8 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -5,7 +5,7 @@ import { base58, base64url } from "@scure/base"; import stableStringify from "fast-json-stable-stringify"; import { blake3 } from "@noble/hashes/blake3"; import { randomBytes } from "@noble/ciphers/webcrypto/utils"; -import { RawCoValueID, TransactionID } from './ids.js'; +import { RawAgentID, RawCoValueID, TransactionID } from './ids.js'; export type SignatorySecret = `signatorySecret_z${string}`; export type SignatoryID = `signatory_z${string}`; @@ -15,6 +15,8 @@ export type RecipientSecret = `recipientSecret_z${string}`; export type RecipientID = `recipient_z${string}`; export type Sealed = `sealed_U${string}` & { __type: T }; +export type AgentSecret = `${RecipientSecret}/${SignatorySecret}`; + const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); @@ -80,6 +82,51 @@ export function getRecipientID(secret: RecipientSecret): RecipientID { )}`; } +export function newRandomAgentSecret(): AgentSecret { + return `${newRandomRecipient()}/${newRandomSignatory()}`; +} + +export function agentSecretToBytes(secret: AgentSecret): Uint8Array { + const [recipientSecret, signatorySecret] = secret.split("/"); + return new Uint8Array([ + ...recipientSecretToBytes(recipientSecret as RecipientSecret), + ...signatorySecretToBytes(signatorySecret as SignatorySecret), + ]); +} + +export function agentSecretFromBytes(bytes: Uint8Array): AgentSecret { + const recipientSecret = recipientSecretFromBytes( + bytes.slice(0, 32) + ); + const signatorySecret = signatorySecretFromBytes( + bytes.slice(32) + ); + return `${recipientSecret}/${signatorySecret}`; +} + +export function getAgentID(secret: AgentSecret): RawAgentID { + const [recipientSecret, signatorySecret] = secret.split("/"); + return `${getRecipientID( + recipientSecret as RecipientSecret + )}/${getSignatoryID(signatorySecret as SignatorySecret)}`; +} + +export function getAgentSignatoryID(agentId: RawAgentID): SignatoryID { + return agentId.split("/")[1] as SignatoryID; +} + +export function getAgentSignatorySecret(agentSecret: AgentSecret): SignatorySecret { + return agentSecret.split("/")[1] as SignatorySecret; +} + +export function getAgentRecipientID(agentId: RawAgentID): RecipientID { + return agentId.split("/")[0] as RecipientID; +} + +export function getAgentRecipientSecret(agentSecret: AgentSecret): RecipientSecret { + return agentSecret.split("/")[0] as RecipientSecret; +} + export type SealedSet = { [recipient: RecipientID]: Sealed; }; diff --git a/src/ids.ts b/src/ids.ts index 8d1b6c90e..92c7d39ca 100644 --- a/src/ids.ts +++ b/src/ids.ts @@ -1,7 +1,13 @@ +import { AccountIDOrAgentID } from './account.js'; + export type RawCoValueID = `co_z${string}` | `co_${string}_z${string}`; export type TransactionID = { sessionID: SessionID; txIndex: number }; -export type AgentID = `co_agent${string}_z${string}`; +export type RawAgentID = `recipient_z${string}/signatory_z${string}`; -export type SessionID = `${AgentID}_session_z${string}`; +export function isRawAgentID(id: string): id is RawAgentID { + return typeof id === "string" && id.startsWith("recipient_") && id.includes("/signatory_"); +} + +export type SessionID = `${AccountIDOrAgentID}_session_z${string}`; diff --git a/src/index.ts b/src/index.ts index 6fb7864d5..4fcfcae34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,29 +1,19 @@ -import { - CoValue, - agentCredentialFromBytes, - agentCredentialToBytes, - getAgent, - getAgentID, - newRandomAgentCredential, - newRandomSessionID, -} from './coValue.js'; -import { LocalNode } from './node.js'; -import { CoMap } from './contentTypes/coMap.js'; +import { CoValue, newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { CoMap } from "./contentTypes/coMap.js"; +import { agentSecretFromBytes, agentSecretToBytes } from "./crypto.js"; -import type { AgentCredential } from './coValue.js'; -import type { AgentID, SessionID } from './ids.js'; -import type { CoValueID, ContentType } from './contentType.js'; -import type { JsonValue } from './jsonValue.js'; -import type { SyncMessage } from './sync.js'; +import type { SessionID } from "./ids.js"; +import type { CoValueID, ContentType } from "./contentType.js"; +import type { JsonValue } from "./jsonValue.js"; +import type { SyncMessage } from "./sync.js"; +import type { AgentSecret } from "./crypto.js"; type Value = JsonValue | ContentType; const internals = { - agentCredentialToBytes, - agentCredentialFromBytes, - getAgent, - getAgentID, - newRandomAgentCredential, + agentSecretFromBytes, + agentSecretToBytes, newRandomSessionID, }; @@ -34,8 +24,7 @@ export type { JsonValue, ContentType, CoValueID, - AgentCredential, + AgentSecret, SessionID, - AgentID, - SyncMessage + SyncMessage, }; diff --git a/src/node.ts b/src/node.ts index 3810dea1f..b85992e75 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,38 +1,40 @@ -import { createdNowUnique, newRandomKeySecret, seal } from './crypto.js'; import { - CoValue, - AgentCredential, - Agent, - getAgent, + createdNowUnique, getAgentID, - getAgentCoValueHeader, - CoValueHeader, - newRandomAgentCredential, -} from './coValue.js'; -import { Team, expectTeamContent } from './permissions.js'; -import { SyncManager } from './sync.js'; -import { AgentID, RawCoValueID, SessionID } from './ids.js'; -import { CoValueID, ContentType } from './contentType.js'; + getAgentRecipientID, + getAgentRecipientSecret, + newRandomAgentSecret, + newRandomKeySecret, + seal, +} from "./crypto.js"; +import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js"; +import { Team, TeamContent, expectTeamContent } from "./permissions.js"; +import { SyncManager } from "./sync.js"; +import { RawAgentID, RawCoValueID, SessionID, isRawAgentID } from "./ids.js"; +import { CoValueID, ContentType } from "./contentType.js"; +import { + Account, + AccountMeta, + AccountIDOrAgentID, + accountHeaderForInitialAgentSecret, + GeneralizedControlledAccount, + ControlledAccount, + AnonymousControlledAccount, +} from "./account.js"; +import { CoMap } from "./index.js"; export class LocalNode { coValues: { [key: RawCoValueID]: CoValueState } = {}; - agentCredential: AgentCredential; - agentID: AgentID; + account: GeneralizedControlledAccount; ownSessionID: SessionID; sync = new SyncManager(this); - constructor(agentCredential: AgentCredential, ownSessionID: SessionID) { - this.agentCredential = agentCredential; - const agent = getAgent(agentCredential); - const agentID = getAgentID(agent); - this.agentID = agentID; + constructor( + account: GeneralizedControlledAccount, + ownSessionID: SessionID + ) { + this.account = account; this.ownSessionID = ownSessionID; - - const agentCoValue = new CoValue(getAgentCoValueHeader(agent), this); - this.coValues[agentCoValue.id] = { - state: "loaded", - coValue: agentCoValue, - }; } createCoValue(header: CoValueHeader): CoValue { @@ -80,39 +82,72 @@ export class LocalNode { return entry.coValue; } - createAgent(publicNickname: string): AgentCredential { - const agentCredential = newRandomAgentCredential(publicNickname); + createAccount(_publicNickname: string): ControlledAccount { + const agentSecret = newRandomAgentSecret(); - this.createCoValue(getAgentCoValueHeader(getAgent(agentCredential))); + const account = this.createCoValue( + accountHeaderForInitialAgentSecret(agentSecret) + ).testWithDifferentAccount(new AnonymousControlledAccount(agentSecret), newRandomSessionID(getAgentID(agentSecret))); - return agentCredential; + expectTeamContent(account.getCurrentContent()).edit((editable) => { + editable.set(getAgentID(agentSecret), "admin", "trusting"); + + const readKey = newRandomKeySecret(); + const revelation = seal( + readKey.secret, + getAgentRecipientSecret(agentSecret), + new Set([getAgentRecipientID(getAgentID(agentSecret))]), + { + in: account.id, + tx: account.nextTransactionID(), + } + ); + + editable.set( + "readKey", + { keyID: readKey.id, revelation }, + "trusting" + ); + }); + + return new ControlledAccount( + agentSecret, + account.getCurrentContent() as CoMap, + this + ); } - expectAgentLoaded(id: AgentID, expectation?: string): Agent { - const coValue = this.expectCoValueLoaded( - id, - expectation - ); + resolveAccount(id: AccountIDOrAgentID, expectation?: string): RawAgentID { + if (isRawAgentID(id)) { + return id; + } - if (coValue.header.type !== "comap" || coValue.header.ruleset.type !== "agent") { + const coValue = this.expectCoValueLoaded(id, expectation); + + if ( + coValue.header.type !== "comap" || + coValue.header.ruleset.type !== "team" || + !coValue.header.meta || + !("type" in coValue.header.meta) || + coValue.header.meta.type !== "account" + ) { throw new Error( `${ expectation ? expectation + ": " : "" - }CoValue ${id} is not an agent` + }CoValue ${id} is not an account` ); } - return { - recipientID: coValue.header.ruleset.initialRecipientID, - signatoryID: coValue.header.ruleset.initialSignatoryID, - publicNickname: coValue.header.publicNickname?.replace("agent-", ""), - } + return new Account( + coValue.getCurrentContent() as CoMap, + this + ).getCurrentAgentID(); } createTeam(): Team { const teamCoValue = this.createCoValue({ type: "comap", - ruleset: { type: "team", initialAdmin: this.agentID }, + ruleset: { type: "team", initialAdmin: this.account.id }, meta: null, ...createdNowUnique(), publicNickname: "team", @@ -121,13 +156,13 @@ export class LocalNode { let teamContent = expectTeamContent(teamCoValue.getCurrentContent()); teamContent = teamContent.edit((editable) => { - editable.set(this.agentID, "admin", "trusting"); + editable.set(this.account.id, "admin", "trusting"); const readKey = newRandomKeySecret(); const revelation = seal( readKey.secret, - this.agentCredential.recipientSecret, - new Set([getAgent(this.agentCredential).recipientID]), + this.account.currentRecipientSecret(), + new Set([this.account.currentRecipientID()]), { in: teamCoValue.id, tx: teamCoValue.nextTransactionID(), @@ -144,11 +179,11 @@ export class LocalNode { return new Team(teamContent, this); } - testWithDifferentCredentials( - agentCredential: AgentCredential, + testWithDifferentAccount( + account: GeneralizedControlledAccount, ownSessionID: SessionID ): LocalNode { - const newNode = new LocalNode(agentCredential, ownSessionID); + const newNode = new LocalNode(account, ownSessionID); newNode.coValues = Object.fromEntries( Object.entries(this.coValues) diff --git a/src/permissions.test.ts b/src/permissions.test.ts index ea25a66c7..07beca442 100644 --- a/src/permissions.test.ts +++ b/src/permissions.test.ts @@ -1,155 +1,84 @@ -import { - getAgent, - getAgentID, - newRandomAgentCredential, - newRandomSessionID, -} from './coValue.js'; -import { LocalNode } from './node.js'; -import { expectMap } from './contentType.js'; -import { expectTeamContent } from './permissions.js'; +import { newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { expectMap } from "./contentType.js"; +import { expectTeamContent } from "./permissions.js"; import { createdNowUnique, getRecipientID, newRandomKeySecret, seal, sealKeySecret, -} from './crypto.js'; - -function teamWithTwoAdmins() { - const { team, admin, adminID, node } = newTeam(); - - const otherAdmin = node.createAgent("otherAdmin"); - const otherAdminID = getAgentID(getAgent(otherAdmin)); - - let content = expectTeamContent(team.getCurrentContent()); - - content.edit((editable) => { - editable.set(otherAdminID, "admin", "trusting"); - expect(editable.get(otherAdminID)).toEqual("admin"); - }); - - content = expectTeamContent(team.getCurrentContent()); - - if (content.type !== "comap") { - throw new Error("Expected map"); - } - - expect(content.get(otherAdminID)).toEqual("admin"); - return { team, admin, adminID, otherAdmin, otherAdminID, node }; -} - -function newTeam() { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); - - const team = node.createCoValue({ - type: "comap", - ruleset: { type: "team", initialAdmin: adminID }, - meta: null, - ...createdNowUnique(), - publicNickname: "team", - }); - - const teamContent = expectTeamContent(team.getCurrentContent()); - - teamContent.edit((editable) => { - editable.set(adminID, "admin", "trusting"); - expect(editable.get(adminID)).toEqual("admin"); - }); - - return { node, team, admin, adminID }; -} +} from "./crypto.js"; +import { + newTeam, + newTeamHighLevel, + teamWithTwoAdmins, + teamWithTwoAdminsHighLevel, +} from "./testUtils.js"; test("Initial admin can add another admin to a team", () => { teamWithTwoAdmins(); }); -function newTeamHighLevel() { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); - - const team = node.createTeam(); - - return { admin, adminID, node, team }; -} - -function teamWithTwoAdminsHighLevel() { - const { admin, adminID, node, team } = newTeamHighLevel(); - - const otherAdmin = node.createAgent("otherAdmin"); - const otherAdminID = getAgentID(getAgent(otherAdmin)); - - team.addMember(otherAdminID, "admin"); - - return { admin, adminID, node, team, otherAdmin, otherAdminID }; -} - test("Initial admin can add another admin to a team (high level)", () => { teamWithTwoAdminsHighLevel(); }); test("Added admin can add a third admin to a team", () => { - const { team, otherAdmin, otherAdminID, node } = teamWithTwoAdmins(); + const { team, otherAdmin, node } = teamWithTwoAdmins(); - const teamAsOtherAdmin = team.testWithDifferentCredentials( + const teamAsOtherAdmin = team.testWithDifferentAccount( otherAdmin, - newRandomSessionID(otherAdminID) + newRandomSessionID(otherAdmin.id) ); let otherContent = expectTeamContent(teamAsOtherAdmin.getCurrentContent()); - expect(otherContent.get(otherAdminID)).toEqual("admin"); + expect(otherContent.get(otherAdmin.id)).toEqual("admin"); - const thirdAdmin = node.createAgent("thirdAdmin"); - const thirdAdminID = getAgentID(getAgent(thirdAdmin)); + const thirdAdmin = node.createAccount("thirdAdmin"); otherContent.edit((editable) => { - editable.set(thirdAdminID, "admin", "trusting"); - expect(editable.get(thirdAdminID)).toEqual("admin"); + editable.set(thirdAdmin.id, "admin", "trusting"); + expect(editable.get(thirdAdmin.id)).toEqual("admin"); }); otherContent = expectTeamContent(teamAsOtherAdmin.getCurrentContent()); - expect(otherContent.get(thirdAdminID)).toEqual("admin"); + expect(otherContent.get(thirdAdmin.id)).toEqual("admin"); }); test("Added adming can add a third admin to a team (high level)", () => { - const { team, otherAdmin, otherAdminID, node } = - teamWithTwoAdminsHighLevel(); + const { team, otherAdmin, node } = teamWithTwoAdminsHighLevel(); - const teamAsOtherAdmin = team.testWithDifferentCredentials( + const teamAsOtherAdmin = team.testWithDifferentAccount( otherAdmin, - newRandomSessionID(otherAdminID) + newRandomSessionID(otherAdmin.id) ); - const thirdAdmin = node.createAgent("thirdAdmin"); - const thirdAdminID = getAgentID(getAgent(thirdAdmin)); + const thirdAdmin = node.createAccount("thirdAdmin"); - teamAsOtherAdmin.addMember(thirdAdminID, "admin"); + teamAsOtherAdmin.addMember(thirdAdmin.id, "admin"); - expect(teamAsOtherAdmin.teamMap.get(thirdAdminID)).toEqual("admin"); + expect(teamAsOtherAdmin.teamMap.get(thirdAdmin.id)).toEqual("admin"); }); test("Admins can't demote other admins in a team", () => { - const { team, adminID, otherAdmin, otherAdminID } = teamWithTwoAdmins(); + const { team, admin, otherAdmin } = teamWithTwoAdmins(); let teamContent = expectTeamContent(team.getCurrentContent()); teamContent.edit((editable) => { - editable.set(otherAdminID, "writer", "trusting"); - expect(editable.get(otherAdminID)).toEqual("admin"); + editable.set(otherAdmin.id, "writer", "trusting"); + expect(editable.get(otherAdmin.id)).toEqual("admin"); }); teamContent = expectTeamContent(team.getCurrentContent()); - expect(teamContent.get(otherAdminID)).toEqual("admin"); + expect(teamContent.get(otherAdmin.id)).toEqual("admin"); - const teamAsOtherAdmin = team.testWithDifferentCredentials( + const teamAsOtherAdmin = team.testWithDifferentAccount( otherAdmin, - newRandomSessionID(otherAdminID) + newRandomSessionID(otherAdmin.id) ); let teamContentAsOtherAdmin = expectTeamContent( @@ -157,185 +86,176 @@ test("Admins can't demote other admins in a team", () => { ); teamContentAsOtherAdmin.edit((editable) => { - editable.set(adminID, "writer", "trusting"); - expect(editable.get(adminID)).toEqual("admin"); + editable.set(admin.id, "writer", "trusting"); + expect(editable.get(admin.id)).toEqual("admin"); }); teamContentAsOtherAdmin = expectTeamContent( teamAsOtherAdmin.getCurrentContent() ); - expect(teamContentAsOtherAdmin.get(adminID)).toEqual("admin"); + expect(teamContentAsOtherAdmin.get(admin.id)).toEqual("admin"); }); test("Admins can't demote other admins in a team (high level)", () => { - const { team, adminID, otherAdmin, otherAdminID } = - teamWithTwoAdminsHighLevel(); + const { team, admin, otherAdmin } = teamWithTwoAdminsHighLevel(); - const teamAsOtherAdmin = team.testWithDifferentCredentials( + const teamAsOtherAdmin = team.testWithDifferentAccount( otherAdmin, - newRandomSessionID(otherAdminID) + newRandomSessionID(otherAdmin.id) ); - expect(() => teamAsOtherAdmin.addMember(adminID, "writer")).toThrow( + expect(() => teamAsOtherAdmin.addMember(admin.id, "writer")).toThrow( "Failed to set role" ); - expect(teamAsOtherAdmin.teamMap.get(adminID)).toEqual("admin"); + expect(teamAsOtherAdmin.teamMap.get(admin.id)).toEqual("admin"); }); test("Admins an add writers to a team, who can't add admins, writers, or readers", () => { const { team, node } = newTeam(); - const writer = node.createAgent("writer"); - const writerID = getAgentID(getAgent(writer)); + const writer = node.createAccount("writer"); let teamContent = expectTeamContent(team.getCurrentContent()); teamContent.edit((editable) => { - editable.set(writerID, "writer", "trusting"); - expect(editable.get(writerID)).toEqual("writer"); + editable.set(writer.id, "writer", "trusting"); + expect(editable.get(writer.id)).toEqual("writer"); }); teamContent = expectTeamContent(team.getCurrentContent()); - expect(teamContent.get(writerID)).toEqual("writer"); + expect(teamContent.get(writer.id)).toEqual("writer"); - const teamAsWriter = team.testWithDifferentCredentials( + const teamAsWriter = team.testWithDifferentAccount( writer, - newRandomSessionID(writerID) + newRandomSessionID(writer.id) ); let teamContentAsWriter = expectTeamContent( teamAsWriter.getCurrentContent() ); - expect(teamContentAsWriter.get(writerID)).toEqual("writer"); + expect(teamContentAsWriter.get(writer.id)).toEqual("writer"); - const otherAgent = node.createAgent("otherAgent"); - const otherAgentID = getAgentID(getAgent(otherAgent)); + const otherAgent = node.createAccount("otherAgent"); teamContentAsWriter.edit((editable) => { - editable.set(otherAgentID, "admin", "trusting"); - expect(editable.get(otherAgentID)).toBeUndefined(); + editable.set(otherAgent.id, "admin", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); - editable.set(otherAgentID, "writer", "trusting"); - expect(editable.get(otherAgentID)).toBeUndefined(); + editable.set(otherAgent.id, "writer", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); - editable.set(otherAgentID, "reader", "trusting"); - expect(editable.get(otherAgentID)).toBeUndefined(); + editable.set(otherAgent.id, "reader", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); }); teamContentAsWriter = expectTeamContent(teamAsWriter.getCurrentContent()); - expect(teamContentAsWriter.get(otherAgentID)).toBeUndefined(); + expect(teamContentAsWriter.get(otherAgent.id)).toBeUndefined(); }); test("Admins an add writers to a team, who can't add admins, writers, or readers (high level)", () => { const { team, node } = newTeamHighLevel(); - const writer = node.createAgent("writer"); - const writerID = getAgentID(getAgent(writer)); + const writer = node.createAccount("writer"); - team.addMember(writerID, "writer"); - expect(team.teamMap.get(writerID)).toEqual("writer"); + team.addMember(writer.id, "writer"); + expect(team.teamMap.get(writer.id)).toEqual("writer"); - const teamAsWriter = team.testWithDifferentCredentials( + const teamAsWriter = team.testWithDifferentAccount( writer, - newRandomSessionID(writerID) + newRandomSessionID(writer.id) ); - expect(teamAsWriter.teamMap.get(writerID)).toEqual("writer"); + expect(teamAsWriter.teamMap.get(writer.id)).toEqual("writer"); - const otherAgent = node.createAgent("otherAgent"); - const otherAgentID = getAgentID(getAgent(otherAgent)); + const otherAgent = node.createAccount("otherAgent"); - expect(() => teamAsWriter.addMember(otherAgentID, "admin")).toThrow( + expect(() => teamAsWriter.addMember(otherAgent.id, "admin")).toThrow( "Failed to set role" ); - expect(() => teamAsWriter.addMember(otherAgentID, "writer")).toThrow( + expect(() => teamAsWriter.addMember(otherAgent.id, "writer")).toThrow( "Failed to set role" ); - expect(() => teamAsWriter.addMember(otherAgentID, "reader")).toThrow( + expect(() => teamAsWriter.addMember(otherAgent.id, "reader")).toThrow( "Failed to set role" ); - expect(teamAsWriter.teamMap.get(otherAgentID)).toBeUndefined(); + expect(teamAsWriter.teamMap.get(otherAgent.id)).toBeUndefined(); }); test("Admins can add readers to a team, who can't add admins, writers, or readers", () => { const { team, node } = newTeam(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); let teamContent = expectTeamContent(team.getCurrentContent()); teamContent.edit((editable) => { - editable.set(readerID, "reader", "trusting"); - expect(editable.get(readerID)).toEqual("reader"); + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); }); teamContent = expectTeamContent(team.getCurrentContent()); - expect(teamContent.get(readerID)).toEqual("reader"); + expect(teamContent.get(reader.id)).toEqual("reader"); - const teamAsReader = team.testWithDifferentCredentials( + const teamAsReader = team.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); let teamContentAsReader = expectTeamContent( teamAsReader.getCurrentContent() ); - expect(teamContentAsReader.get(readerID)).toEqual("reader"); + expect(teamContentAsReader.get(reader.id)).toEqual("reader"); - const otherAgent = node.createAgent("otherAgent"); - const otherAgentID = getAgentID(getAgent(otherAgent)); + const otherAgent = node.createAccount("otherAgent"); teamContentAsReader.edit((editable) => { - editable.set(otherAgentID, "admin", "trusting"); - expect(editable.get(otherAgentID)).toBeUndefined(); + editable.set(otherAgent.id, "admin", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); - editable.set(otherAgentID, "writer", "trusting"); - expect(editable.get(otherAgentID)).toBeUndefined(); + editable.set(otherAgent.id, "writer", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); - editable.set(otherAgentID, "reader", "trusting"); - expect(editable.get(otherAgentID)).toBeUndefined(); + editable.set(otherAgent.id, "reader", "trusting"); + expect(editable.get(otherAgent.id)).toBeUndefined(); }); teamContentAsReader = expectTeamContent(teamAsReader.getCurrentContent()); - expect(teamContentAsReader.get(otherAgentID)).toBeUndefined(); + expect(teamContentAsReader.get(otherAgent.id)).toBeUndefined(); }); test("Admins can add readers to a team, who can't add admins, writers, or readers (high level)", () => { const { team, node } = newTeamHighLevel(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); - team.addMember(readerID, "reader"); - expect(team.teamMap.get(readerID)).toEqual("reader"); + team.addMember(reader.id, "reader"); + expect(team.teamMap.get(reader.id)).toEqual("reader"); - const teamAsReader = team.testWithDifferentCredentials( + const teamAsReader = team.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); - expect(teamAsReader.teamMap.get(readerID)).toEqual("reader"); + expect(teamAsReader.teamMap.get(reader.id)).toEqual("reader"); - const otherAgent = node.createAgent("otherAgent"); - const otherAgentID = getAgentID(getAgent(otherAgent)); + const otherAgent = node.createAccount("otherAgent"); - expect(() => teamAsReader.addMember(otherAgentID, "admin")).toThrow( + expect(() => teamAsReader.addMember(otherAgent.id, "admin")).toThrow( "Failed to set role" ); - expect(() => teamAsReader.addMember(otherAgentID, "writer")).toThrow( + expect(() => teamAsReader.addMember(otherAgent.id, "writer")).toThrow( "Failed to set role" ); - expect(() => teamAsReader.addMember(otherAgentID, "reader")).toThrow( + expect(() => teamAsReader.addMember(otherAgent.id, "reader")).toThrow( "Failed to set role" ); - expect(teamAsReader.teamMap.get(otherAgentID)).toBeUndefined(); + expect(teamAsReader.teamMap.get(otherAgent.id)).toBeUndefined(); }); test("Admins can write to an object that is owned by their team", () => { @@ -377,12 +297,11 @@ test("Admins can write to an object that is owned by their team (high level)", ( test("Writers can write to an object that is owned by their team", () => { const { node, team } = newTeam(); - const writer = node.createAgent("writer"); - const writerID = getAgentID(getAgent(writer)); + const writer = node.createAccount("writer"); expectTeamContent(team.getCurrentContent()).edit((editable) => { - editable.set(writerID, "writer", "trusting"); - expect(editable.get(writerID)).toEqual("writer"); + editable.set(writer.id, "writer", "trusting"); + expect(editable.get(writer.id)).toEqual("writer"); }); const childObject = node.createCoValue({ @@ -393,9 +312,9 @@ test("Writers can write to an object that is owned by their team", () => { publicNickname: "childObject", }); - const childObjectAsWriter = childObject.testWithDifferentCredentials( + const childObjectAsWriter = childObject.testWithDifferentAccount( writer, - newRandomSessionID(writerID) + newRandomSessionID(writer.id) ); let childContentAsWriter = expectMap( @@ -415,16 +334,15 @@ test("Writers can write to an object that is owned by their team", () => { test("Writers can write to an object that is owned by their team (high level)", () => { const { node, team } = newTeamHighLevel(); - const writer = node.createAgent("writer"); - const writerID = getAgentID(getAgent(writer)); + const writer = node.createAccount("writer"); - team.addMember(writerID, "writer"); + team.addMember(writer.id, "writer"); const childObject = team.createMap(); let childObjectAsWriter = expectMap( childObject.coValue - .testWithDifferentCredentials(writer, newRandomSessionID(writerID)) + .testWithDifferentAccount(writer, newRandomSessionID(writer.id)) .getCurrentContent() ); @@ -439,12 +357,11 @@ test("Writers can write to an object that is owned by their team (high level)", test("Readers can not write to an object that is owned by their team", () => { const { node, team } = newTeam(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); expectTeamContent(team.getCurrentContent()).edit((editable) => { - editable.set(readerID, "reader", "trusting"); - expect(editable.get(readerID)).toEqual("reader"); + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); }); const childObject = node.createCoValue({ @@ -455,9 +372,9 @@ test("Readers can not write to an object that is owned by their team", () => { publicNickname: "childObject", }); - const childObjectAsReader = childObject.testWithDifferentCredentials( + const childObjectAsReader = childObject.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); let childContentAsReader = expectMap( @@ -477,16 +394,15 @@ test("Readers can not write to an object that is owned by their team", () => { test("Readers can not write to an object that is owned by their team (high level)", () => { const { node, team } = newTeamHighLevel(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); - team.addMember(readerID, "reader"); + team.addMember(reader.id, "reader"); const childObject = team.createMap(); let childObjectAsReader = expectMap( childObject.coValue - .testWithDifferentCredentials(reader, newRandomSessionID(readerID)) + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) .getCurrentContent() ); @@ -499,7 +415,7 @@ test("Readers can not write to an object that is owned by their team (high level }); test("Admins can set team read key and then use it to create and read private transactions in owned objects", () => { - const { node, team, admin, adminID } = newTeam(); + const { node, team, admin } = newTeam(); const teamContent = expectTeamContent(team.getCurrentContent()); @@ -507,8 +423,8 @@ test("Admins can set team read key and then use it to create and read private tr const { secret: readKey, id: readKeyID } = newRandomKeySecret(); const revelation = seal( readKey, - admin.recipientSecret, - new Set([getRecipientID(admin.recipientSecret)]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -557,23 +473,20 @@ test("Admins can set team read key and then use it to create and read private tr test("Admins can set team read key and then writers can use it to create and read private transactions in owned objects", () => { const { node, team, admin } = newTeam(); - const writer = node.createAgent("writer"); - const writerID = getAgentID(getAgent(writer)); + const writer = node.createAccount("writer"); + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); const teamContent = expectTeamContent(team.getCurrentContent()); teamContent.edit((editable) => { - editable.set(writerID, "writer", "trusting"); - expect(editable.get(writerID)).toEqual("writer"); + editable.set(writer.id, "writer", "trusting"); + expect(editable.get(writer.id)).toEqual("writer"); const revelation = seal( readKey, - admin.recipientSecret, - new Set([ - getRecipientID(admin.recipientSecret), - getRecipientID(writer.recipientSecret), - ]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID(), writer.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -590,9 +503,9 @@ test("Admins can set team read key and then writers can use it to create and rea publicNickname: "childObject", }); - const childObjectAsWriter = childObject.testWithDifferentCredentials( + const childObjectAsWriter = childObject.testWithDifferentAccount( writer, - newRandomSessionID(writerID) + newRandomSessionID(writer.id) ); expect(childObject.getCurrentReadKey().secret).toEqual(readKey); @@ -614,16 +527,15 @@ test("Admins can set team read key and then writers can use it to create and rea test("Admins can set team read key and then writers can use it to create and read private transactions in owned objects (high level)", () => { const { node, team, admin } = newTeamHighLevel(); - const writer = node.createAgent("writer"); - const writerID = getAgentID(getAgent(writer)); + const writer = node.createAccount("writer"); - team.addMember(writerID, "writer"); + team.addMember(writer.id, "writer"); const childObject = team.createMap(); let childObjectAsWriter = expectMap( childObject.coValue - .testWithDifferentCredentials(writer, newRandomSessionID(writerID)) + .testWithDifferentAccount(writer, newRandomSessionID(writer.id)) .getCurrentContent() ); @@ -638,23 +550,20 @@ test("Admins can set team read key and then writers can use it to create and rea test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read", () => { const { node, team, admin } = newTeam(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); const teamContent = expectTeamContent(team.getCurrentContent()); teamContent.edit((editable) => { - editable.set(readerID, "reader", "trusting"); - expect(editable.get(readerID)).toEqual("reader"); + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); const revelation = seal( readKey, - admin.recipientSecret, - new Set([ - getRecipientID(admin.recipientSecret), - getRecipientID(reader.recipientSecret), - ]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID(), reader.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -676,9 +585,9 @@ test("Admins can set team read key and then use it to create private transaction expect(editable.get("foo")).toEqual("bar"); }); - const childObjectAsReader = childObject.testWithDifferentCredentials( + const childObjectAsReader = childObject.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); expect(childObjectAsReader.getCurrentReadKey().secret).toEqual(readKey); @@ -693,10 +602,9 @@ test("Admins can set team read key and then use it to create private transaction test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read (high level)", () => { const { node, team, admin } = newTeamHighLevel(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); - team.addMember(readerID, "reader"); + team.addMember(reader.id, "reader"); let childObject = team.createMap(); @@ -707,7 +615,7 @@ test("Admins can set team read key and then use it to create private transaction const childContentAsReader = expectMap( childObject.coValue - .testWithDifferentCredentials(reader, newRandomSessionID(readerID)) + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) .getCurrentContent() ); @@ -717,25 +625,22 @@ test("Admins can set team read key and then use it to create private transaction test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read, even with a separate later revelation for the same read key", () => { const { node, team, admin } = newTeam(); - const reader1 = node.createAgent("reader1"); - const reader1ID = getAgentID(getAgent(reader1)); - const reader2 = node.createAgent("reader2"); - const reader2ID = getAgentID(getAgent(reader2)); + const reader1 = node.createAccount("reader1"); + + const reader2 = node.createAccount("reader2"); + const { secret: readKey, id: readKeyID } = newRandomKeySecret(); const teamContent = expectTeamContent(team.getCurrentContent()); teamContent.edit((editable) => { - editable.set(reader1ID, "reader", "trusting"); - expect(editable.get(reader1ID)).toEqual("reader"); + editable.set(reader1.id, "reader", "trusting"); + expect(editable.get(reader1.id)).toEqual("reader"); const revelation1 = seal( readKey, - admin.recipientSecret, - new Set([ - getRecipientID(admin.recipientSecret), - getRecipientID(reader1.recipientSecret), - ]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID(), reader1.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -749,8 +654,8 @@ test("Admins can set team read key and then use it to create private transaction const revelation2 = seal( readKey, - admin.recipientSecret, - new Set([getRecipientID(reader2.recipientSecret)]), + admin.currentRecipientSecret(), + new Set([reader2.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -776,9 +681,9 @@ test("Admins can set team read key and then use it to create private transaction expect(editable.get("foo")).toEqual("bar"); }); - const childObjectAsReader1 = childObject.testWithDifferentCredentials( + const childObjectAsReader1 = childObject.testWithDifferentAccount( reader1, - newRandomSessionID(reader1ID) + newRandomSessionID(reader1.id) ); expect(childObjectAsReader1.getCurrentReadKey().secret).toEqual(readKey); @@ -789,9 +694,9 @@ test("Admins can set team read key and then use it to create private transaction expect(childContentAsReader1.get("foo")).toEqual("bar"); - const childObjectAsReader2 = childObject.testWithDifferentCredentials( + const childObjectAsReader2 = childObject.testWithDifferentAccount( reader2, - newRandomSessionID(reader2ID) + newRandomSessionID(reader2.id) ); expect(childObjectAsReader2.getCurrentReadKey().secret).toEqual(readKey); @@ -806,12 +711,11 @@ test("Admins can set team read key and then use it to create private transaction test("Admins can set team read key and then use it to create private transactions in owned objects, which readers can read, even with a separate later revelation for the same read key (high level)", () => { const { node, team, admin } = newTeamHighLevel(); - const reader1 = node.createAgent("reader1"); - const reader1ID = getAgentID(getAgent(reader1)); - const reader2 = node.createAgent("reader2"); - const reader2ID = getAgentID(getAgent(reader2)); + const reader1 = node.createAccount("reader1"); - team.addMember(reader1ID, "reader"); + const reader2 = node.createAccount("reader2"); + + team.addMember(reader1.id, "reader"); let childObject = team.createMap(); @@ -822,23 +726,17 @@ test("Admins can set team read key and then use it to create private transaction const childContentAsReader1 = expectMap( childObject.coValue - .testWithDifferentCredentials( - reader1, - newRandomSessionID(reader1ID) - ) + .testWithDifferentAccount(reader1, newRandomSessionID(reader1.id)) .getCurrentContent() ); expect(childContentAsReader1.get("foo")).toEqual("bar"); - team.addMember(reader2ID, "reader"); + team.addMember(reader2.id, "reader"); const childContentAsReader2 = expectMap( childObject.coValue - .testWithDifferentCredentials( - reader2, - newRandomSessionID(reader2ID) - ) + .testWithDifferentAccount(reader2, newRandomSessionID(reader2.id)) .getCurrentContent() ); @@ -846,7 +744,7 @@ test("Admins can set team read key and then use it to create private transaction }); test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, make another private transaction, and both can be read by the admin", () => { - const { node, team, admin, adminID } = newTeam(); + const { node, team, admin } = newTeam(); const teamContent = expectTeamContent(team.getCurrentContent()); @@ -854,8 +752,8 @@ test("Admins can set team read key, make a private transaction in an owned objec const { secret: readKey, id: readKeyID } = newRandomKeySecret(); const revelation = seal( readKey, - admin.recipientSecret, - new Set([getRecipientID(admin.recipientSecret)]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -892,8 +790,8 @@ test("Admins can set team read key, make a private transaction in an owned objec const revelation = seal( readKey2, - admin.recipientSecret, - new Set([getRecipientID(admin.recipientSecret)]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -921,7 +819,7 @@ test("Admins can set team read key, make a private transaction in an owned objec }); test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, make another private transaction, and both can be read by the admin (high level)", () => { - const { node, team, admin, adminID } = newTeamHighLevel(); + const { team } = newTeamHighLevel(); let childObject = team.createMap(); @@ -948,7 +846,7 @@ test("Admins can set team read key, make a private transaction in an owned objec }); test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, add a reader, make another private transaction in the owned object, and both can be read by the reader", () => { - const { node, team, admin, adminID } = newTeam(); + const { node, team, admin } = newTeam(); const childObject = node.createCoValue({ type: "comap", @@ -964,8 +862,8 @@ test("Admins can set team read key, make a private transaction in an owned objec teamContent.edit((editable) => { const revelation = seal( readKey, - admin.recipientSecret, - new Set([getRecipientID(admin.recipientSecret)]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -989,18 +887,15 @@ test("Admins can set team read key, make a private transaction in an owned objec childContent = expectMap(childObject.getCurrentContent()); expect(childContent.get("foo")).toEqual("bar"); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); + const { secret: readKey2, id: readKeyID2 } = newRandomKeySecret(); teamContent.edit((editable) => { const revelation = seal( readKey2, - admin.recipientSecret, - new Set([ - getRecipientID(admin.recipientSecret), - getRecipientID(reader.recipientSecret), - ]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID(), reader.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -1027,8 +922,8 @@ test("Admins can set team read key, make a private transaction in an owned objec }); expect(team.getCurrentReadKey().secret).toEqual(readKey2); - editable.set(readerID, "reader", "trusting"); - expect(editable.get(readerID)).toEqual("reader"); + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); }); childContent.edit((editable) => { @@ -1036,9 +931,9 @@ test("Admins can set team read key, make a private transaction in an owned objec expect(editable.get("foo2")).toEqual("bar2"); }); - const childObjectAsReader = childObject.testWithDifferentCredentials( + const childObjectAsReader = childObject.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); expect(childObjectAsReader.getCurrentReadKey().secret).toEqual(readKey2); @@ -1052,7 +947,7 @@ test("Admins can set team read key, make a private transaction in an owned objec }); test("Admins can set team read key, make a private transaction in an owned object, rotate the read key, add a reader, make another private transaction in the owned object, and both can be read by the reader (high level)", () => { - const { node, team, admin, adminID } = newTeamHighLevel(); + const { node, team } = newTeamHighLevel(); let childObject = team.createMap(); @@ -1069,10 +964,9 @@ test("Admins can set team read key, make a private transaction in an owned objec expect(childObject.coValue.getCurrentReadKey()).not.toEqual(firstReadKey); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); - team.addMember(readerID, "reader"); + team.addMember(reader.id, "reader"); childObject = childObject.edit((editable) => { editable.set("foo2", "bar2", "private"); @@ -1081,7 +975,7 @@ test("Admins can set team read key, make a private transaction in an owned objec const childContentAsReader = expectMap( childObject.coValue - .testWithDifferentCredentials(reader, newRandomSessionID(readerID)) + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) .getCurrentContent() ); @@ -1090,7 +984,7 @@ test("Admins can set team read key, make a private transaction in an owned objec }); test("Admins can set team read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions", () => { - const { node, team, admin, adminID } = newTeam(); + const { node, team, admin } = newTeam(); const childObject = node.createCoValue({ type: "comap", @@ -1102,19 +996,18 @@ test("Admins can set team read rey, make a private transaction in an owned objec const teamContent = expectTeamContent(team.getCurrentContent()); const { secret: readKey, id: readKeyID } = newRandomKeySecret(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); - const reader2 = node.createAgent("reader2"); - const reader2ID = getAgentID(getAgent(reader)); + const reader = node.createAccount("reader"); + + const reader2 = node.createAccount("reader2"); teamContent.edit((editable) => { const revelation = seal( readKey, - admin.recipientSecret, + admin.currentRecipientSecret(), new Set([ - getRecipientID(admin.recipientSecret), - getRecipientID(reader.recipientSecret), - getRecipientID(reader2.recipientSecret), + admin.currentRecipientID(), + reader.currentRecipientID(), + reader2.currentRecipientID(), ]), { in: team.id, @@ -1128,10 +1021,10 @@ test("Admins can set team read rey, make a private transaction in an owned objec }); expect(team.getCurrentReadKey().secret).toEqual(readKey); - editable.set(readerID, "reader", "trusting"); - expect(editable.get(readerID)).toEqual("reader"); - editable.set(reader2ID, "reader", "trusting"); - expect(editable.get(reader2ID)).toEqual("reader"); + editable.set(reader.id, "reader", "trusting"); + expect(editable.get(reader.id)).toEqual("reader"); + editable.set(reader2.id, "reader", "trusting"); + expect(editable.get(reader2.id)).toEqual("reader"); }); let childContent = expectMap(childObject.getCurrentContent()); @@ -1144,18 +1037,18 @@ test("Admins can set team read rey, make a private transaction in an owned objec childContent = expectMap(childObject.getCurrentContent()); expect(childContent.get("foo")).toEqual("bar"); - let childObjectAsReader = childObject.testWithDifferentCredentials( + let childObjectAsReader = childObject.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); expect( expectMap(childObjectAsReader.getCurrentContent()).get("foo") ).toEqual("bar"); - let childObjectAsReader2 = childObject.testWithDifferentCredentials( + let childObjectAsReader2 = childObject.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); expect( @@ -1167,11 +1060,8 @@ test("Admins can set team read rey, make a private transaction in an owned objec teamContent.edit((editable) => { const revelation = seal( readKey2, - admin.recipientSecret, - new Set([ - getRecipientID(admin.recipientSecret), - getRecipientID(reader2.recipientSecret), - ]), + admin.currentRecipientSecret(), + new Set([admin.currentRecipientID(), reader2.currentRecipientID()]), { in: team.id, tx: team.nextTransactionID(), @@ -1184,8 +1074,8 @@ test("Admins can set team read rey, make a private transaction in an owned objec }); expect(team.getCurrentReadKey().secret).toEqual(readKey2); - editable.set(readerID, "revoked", "trusting"); - // expect(editable.get(readerID)).toEqual("revoked"); + editable.set(reader.id, "revoked", "trusting"); + // expect(editable.get(reader.id)).toEqual("revoked"); }); expect(childObject.getCurrentReadKey().secret).toEqual(readKey2); @@ -1197,13 +1087,13 @@ test("Admins can set team read rey, make a private transaction in an owned objec }); // TODO: make sure these instances of coValues sync between each other so this isn't necessary? - childObjectAsReader = childObject.testWithDifferentCredentials( + childObjectAsReader = childObject.testWithDifferentAccount( reader, - newRandomSessionID(readerID) + newRandomSessionID(reader.id) ); - childObjectAsReader2 = childObject.testWithDifferentCredentials( + childObjectAsReader2 = childObject.testWithDifferentAccount( reader2, - newRandomSessionID(reader2ID) + newRandomSessionID(reader2.id) ); expect( @@ -1215,7 +1105,7 @@ test("Admins can set team read rey, make a private transaction in an owned objec }); test("Admins can set team read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions (high level)", () => { - const { node, team, admin, adminID } = newTeamHighLevel(); + const { node, team } = newTeamHighLevel(); let childObject = team.createMap(); @@ -1230,13 +1120,12 @@ test("Admins can set team read rey, make a private transaction in an owned objec const secondReadKey = childObject.coValue.getCurrentReadKey(); - const reader = node.createAgent("reader"); - const readerID = getAgentID(getAgent(reader)); - const reader2 = node.createAgent("reader2"); - const reader2ID = getAgentID(getAgent(reader2)); + const reader = node.createAccount("reader"); - team.addMember(readerID, "reader"); - team.addMember(reader2ID, "reader"); + const reader2 = node.createAccount("reader2"); + + team.addMember(reader.id, "reader"); + team.addMember(reader2.id, "reader"); childObject = childObject.edit((editable) => { editable.set("foo2", "bar2", "private"); @@ -1246,7 +1135,7 @@ test("Admins can set team read rey, make a private transaction in an owned objec expect(childObject.get("foo")).toEqual("bar"); expect(childObject.get("foo2")).toEqual("bar2"); - team.removeMember(readerID); + team.removeMember(reader.id); expect(childObject.coValue.getCurrentReadKey()).not.toEqual(secondReadKey); @@ -1257,10 +1146,7 @@ test("Admins can set team read rey, make a private transaction in an owned objec const childContentAsReader2 = expectMap( childObject.coValue - .testWithDifferentCredentials( - reader2, - newRandomSessionID(reader2ID) - ) + .testWithDifferentAccount(reader2, newRandomSessionID(reader2.id)) .getCurrentContent() ); @@ -1271,31 +1157,28 @@ test("Admins can set team read rey, make a private transaction in an owned objec expect( expectMap( childObject.coValue - .testWithDifferentCredentials( - reader, - newRandomSessionID(readerID) - ) + .testWithDifferentAccount(reader, newRandomSessionID(reader.id)) .getCurrentContent() ).get("foo3") ).toBeUndefined(); }); test("Can create two owned objects in the same team and they will have different ids", () => { - const { node, team, admin, adminID } = newTeam(); + const { node, team } = newTeam(); const childObject1 = node.createCoValue({ type: "comap", ruleset: { type: "ownedByTeam", team: team.id }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); const childObject2 = node.createCoValue({ type: "comap", ruleset: { type: "ownedByTeam", team: team.id }, meta: null, - ...createdNowUnique() + ...createdNowUnique(), }); expect(childObject1.id).not.toEqual(childObject2.id); -}); \ No newline at end of file +}); diff --git a/src/permissions.ts b/src/permissions.ts index d1af46acc..a399d7c54 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,36 +1,30 @@ -import { ContentType } from './contentType.js'; +import { CoValueID, ContentType } from './contentType.js'; import { CoMap, MapOpPayload } from './contentTypes/coMap.js'; import { JsonValue } from './jsonValue.js'; import { Encrypted, KeyID, KeySecret, - RecipientID, SealedSet, - SignatoryID, createdNowUnique, newRandomKeySecret, seal, sealKeySecret, + getAgentRecipientID } from './crypto.js'; import { - AgentCredential, CoValue, Transaction, TrustingTransaction, - agentIDfromSessionID, + accountOrAgentIDfromSessionID, } from './coValue.js'; import { LocalNode } from "./node.js"; -import { AgentID, RawCoValueID, SessionID, TransactionID } from './ids.js'; +import { RawCoValueID, SessionID, TransactionID, isRawAgentID } from './ids.js'; +import { AccountIDOrAgentID, GeneralizedControlledAccount } from './account.js'; export type PermissionsDef = - | { type: "team"; initialAdmin: AgentID; parentTeams?: RawCoValueID[] } + | { type: "team"; initialAdmin: AccountIDOrAgentID; } | { type: "ownedByTeam"; team: RawCoValueID } - | { - type: "agent"; - initialSignatoryID: SignatoryID; - initialRecipientID: RecipientID; - } | { type: "unsafeAllowAll" }; export type Role = "reader" | "writer" | "admin" | "revoked"; @@ -68,7 +62,7 @@ export function determineValidTransactions( throw new Error("Team must have initialAdmin"); } - const memberState: { [agent: AgentID]: Role } = {}; + const memberState: { [agent: AccountIDOrAgentID]: Role } = {}; const validTransactions: { txID: TransactionID; tx: Transaction }[] = []; @@ -79,10 +73,10 @@ export function determineValidTransactions( tx, } of allTrustingTransactionsSorted) { // console.log("before", { memberState, validTransactions }); - const transactor = agentIDfromSessionID(sessionID); + const transactor = accountOrAgentIDfromSessionID(sessionID); const change = tx.changes[0] as - | MapOpPayload + | MapOpPayload | MapOpPayload<"readKey", JsonValue>; if (tx.changes.length !== 1) { console.warn("Team transaction must have exactly one change"); @@ -164,7 +158,7 @@ export function determineValidTransactions( return Object.entries(coValue.sessions).flatMap( ([sessionID, sessionLog]) => { - const transactor = agentIDfromSessionID(sessionID as SessionID); + const transactor = accountOrAgentIDfromSessionID(sessionID as SessionID); return sessionLog.transactions .filter((tx) => { const transactorRoleAtTxTime = teamContent.getAtTime( @@ -192,15 +186,12 @@ export function determineValidTransactions( })); } ); - } else if (coValue.header.ruleset.type === "agent") { - // TODO - return []; } else { throw new Error("Unknown ruleset type " + (coValue.header.ruleset as any).type); } } -export type TeamContent = { [key: AgentID]: Role } & { +export type TeamContent = { [key: AccountIDOrAgentID]: Role } & { readKey: { keyID: KeyID; revelation: SealedSet; @@ -230,20 +221,20 @@ export class Team { this.node = node; } - get id(): RawCoValueID { + get id(): CoValueID> { return this.teamMap.id; } - addMember(agentID: AgentID, role: Role) { + addMember(accountID: AccountIDOrAgentID, role: Role) { this.teamMap = this.teamMap.edit((map) => { - const agent = this.node.expectAgentLoaded(agentID, "Expected to know agent to add them to team"); + const agent = this.node.resolveAccount(accountID, "Expected to know agent to add them to team"); if (!agent) { - throw new Error("Unknown agent " + agentID); + throw new Error("Unknown account/agent " + accountID); } - map.set(agentID, role, "trusting"); - if (map.get(agentID) !== role) { + map.set(accountID, role, "trusting"); + if (map.get(accountID) !== role) { throw new Error("Failed to set role"); } @@ -255,8 +246,8 @@ export class Team { const revelation = seal( currentReadKey.secret, - this.teamMap.coValue.node.agentCredential.recipientSecret, - new Set([agent.recipientID]), + this.teamMap.coValue.node.account.currentRecipientSecret(), + new Set([getAgentRecipientID(agent)]), { in: this.teamMap.coValue.id, tx: this.teamMap.coValue.nextTransactionID(), @@ -273,7 +264,7 @@ export class Team { rotateReadKey() { const currentlyPermittedReaders = this.teamMap.keys().filter((key) => { - if (key.startsWith("co_agent")) { + if (key.startsWith("co_") || isRawAgentID(key)) { const role = this.teamMap.get(key); return ( role === "admin" || role === "writer" || role === "reader" @@ -281,7 +272,7 @@ export class Team { } else { return false; } - }) as AgentID[]; + }) as AccountIDOrAgentID[]; const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey(); @@ -298,15 +289,15 @@ export class Team { const newReadKeyRevelation = seal( newReadKey.secret, - this.teamMap.coValue.node.agentCredential.recipientSecret, + this.teamMap.coValue.node.account.currentRecipientSecret(), new Set( currentlyPermittedReaders.map( (reader) => { - const readerAgent = this.node.expectAgentLoaded(reader, "Expected to know currently permitted reader"); + const readerAgent = this.node.resolveAccount(reader, "Expected to know currently permitted reader"); if (!readerAgent) { throw new Error("Unknown agent " + reader); } - return readerAgent.recipientID + return getAgentRecipientID(readerAgent) } ) ), @@ -334,9 +325,9 @@ export class Team { }); } - removeMember(agentID: AgentID) { + removeMember(accountID: AccountIDOrAgentID) { this.teamMap = this.teamMap.edit((map) => { - map.set(agentID, "revoked", "trusting"); + map.set(accountID, "revoked", "trusting"); }); this.rotateReadKey(); @@ -359,14 +350,14 @@ export class Team { .getCurrentContent() as CoMap; } - testWithDifferentCredentials( - credential: AgentCredential, + testWithDifferentAccount( + account: GeneralizedControlledAccount, sessionId: SessionID ): Team { return new Team( expectTeamContent( this.teamMap.coValue - .testWithDifferentCredentials(credential, sessionId) + .testWithDifferentAccount(account, sessionId) .getCurrentContent() ), this.node diff --git a/src/sync.test.ts b/src/sync.test.ts index 1a2e65b7d..874fd4bdd 100644 --- a/src/sync.test.ts +++ b/src/sync.test.ts @@ -1,115 +1,103 @@ -import { - getAgent, - getAgentID, - newRandomAgentCredential, - newRandomSessionID, -} from './coValue.js'; -import { LocalNode } from './node.js'; -import { Peer, PeerID, SyncMessage } from './sync.js'; -import { expectMap } from './contentType.js'; -import { MapOpPayload } from './contentTypes/coMap.js'; -import { Team } from './permissions.js'; +import { newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { Peer, PeerID, SyncMessage } from "./sync.js"; +import { expectMap } from "./contentType.js"; +import { MapOpPayload } from "./contentTypes/coMap.js"; +import { Team } from "./permissions.js"; import { ReadableStream, WritableStream, TransformStream, } from "isomorphic-streams"; -import { AgentID } from './ids.js'; +import { randomAnonymousAccountAndSessionID } from "./testUtils.js"; +import { AccountID } from "./account.js"; -test( - "Node replies with initial tx and header to empty subscribe", - async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); +test("Node replies with initial tx and header to empty subscribe", async () => { + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const team = node.createTeam(); - const team = node.createTeam(); + const map = team.createMap(); - const map = team.createMap(); + map.edit((editable) => { + editable.set("hello", "world", "trusting"); + }); - map.edit((editable) => { - editable.set("hello", "world", "trusting"); - }); + const [inRx, inTx] = newStreamPair(); + const [outRx, outTx] = newStreamPair(); - const [inRx, inTx] = newStreamPair(); - const [outRx, outTx] = newStreamPair(); + node.sync.addPeer({ + id: "test", + incoming: inRx, + outgoing: outTx, + role: "peer", + }); - node.sync.addPeer({ - id: "test", - incoming: inRx, - outgoing: outTx, - role: "peer", - }); + const writer = inTx.getWriter(); - const writer = inTx.getWriter(); + await writer.write({ + action: "subscribe", + coValueID: map.coValue.id, + header: false, + sessions: {}, + }); - await writer.write({ - action: "subscribe", - coValueID: map.coValue.id, - header: false, - sessions: {}, - }); + const reader = outRx.getReader(); - const reader = outRx.getReader(); + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamStateEx(team)); - expect((await reader.read()).value).toMatchObject(admStateEx(adminID)); - expect((await reader.read()).value).toMatchObject(teamStateEx(team)); + const mapTellKnownStateMsg = await reader.read(); + expect(mapTellKnownStateMsg.value).toEqual({ + action: "tellKnownState", + ...map.coValue.knownState(), + } satisfies SyncMessage); - const mapTellKnownStateMsg = await reader.read(); - expect(mapTellKnownStateMsg.value).toEqual({ - action: "tellKnownState", - ...map.coValue.knownState(), - } satisfies SyncMessage); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); + expect((await reader.read()).value).toMatchObject(teamContentEx(team)); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); - expect((await reader.read()).value).toMatchObject(teamContentEx(team)); + const newContentMsg = await reader.read(); - const newContentMsg = await reader.read(); - - expect(newContentMsg.value).toEqual({ - action: "newContent", - coValueID: map.coValue.id, - header: { - type: "comap", - ruleset: { type: "ownedByTeam", team: team.id }, - meta: null, - createdAt: map.coValue.header.createdAt, - uniqueness: map.coValue.header.uniqueness, - publicNickname: "map", + expect(newContentMsg.value).toEqual({ + action: "newContent", + coValueID: map.coValue.id, + header: { + type: "comap", + ruleset: { type: "ownedByTeam", team: team.id }, + meta: null, + createdAt: map.coValue.header.createdAt, + uniqueness: map.coValue.header.uniqueness, + publicNickname: "map", + }, + newContent: { + [node.ownSessionID]: { + after: 0, + newTransactions: [ + { + privacy: "trusting" as const, + madeAt: map.coValue.sessions[node.ownSessionID]! + .transactions[0]!.madeAt, + changes: [ + { + op: "insert", + key: "hello", + value: "world", + } satisfies MapOpPayload, + ], + }, + ], + lastHash: map.coValue.sessions[node.ownSessionID]!.lastHash!, + lastSignature: + map.coValue.sessions[node.ownSessionID]!.lastSignature!, }, - newContent: { - [node.ownSessionID]: { - after: 0, - newTransactions: [ - { - privacy: "trusting", - madeAt: map.coValue.sessions[node.ownSessionID]! - .transactions[0]!.madeAt, - changes: [ - { - op: "insert", - key: "hello", - value: "world", - } satisfies MapOpPayload, - ], - }, - ], - lastHash: - map.coValue.sessions[node.ownSessionID]!.lastHash!, - lastSignature: - map.coValue.sessions[node.ownSessionID]!.lastSignature!, - }, - }, - } satisfies SyncMessage); - }, -); + }, + } satisfies SyncMessage); +}); test("Node replies with only new tx to subscribe with some known state", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -143,7 +131,7 @@ test("Node replies with only new tx to subscribe with some known state", async ( const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject(admStateEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamStateEx(team)); const mapTellKnownStateMsg = await reader.read(); @@ -152,7 +140,7 @@ test("Node replies with only new tx to subscribe with some known state", async ( ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapNewContentMsg = await reader.read(); @@ -166,7 +154,7 @@ test("Node replies with only new tx to subscribe with some known state", async ( after: 1, newTransactions: [ { - privacy: "trusting", + privacy: "trusting" as const, madeAt: map.coValue.sessions[node.ownSessionID]! .transactions[1]!.madeAt, changes: [ @@ -187,14 +175,12 @@ test("Node replies with only new tx to subscribe with some known state", async ( }); test.todo( - "TODO: node only replies with new tx to subscribe with some known state, even in the depended on coValues", + "TODO: node only replies with new tx to subscribe with some known state, even in the depended on coValues" ); test("After subscribing, node sends own known state and new txs to peer", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -223,7 +209,7 @@ test("After subscribing, node sends own known state and new txs to peer", async const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject(admStateEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamStateEx(team)); const mapTellKnownStateMsg = await reader.read(); @@ -232,7 +218,7 @@ test("After subscribing, node sends own known state and new txs to peer", async ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapNewContentHeaderOnlyMsg = await reader.read(); @@ -258,7 +244,7 @@ test("After subscribing, node sends own known state and new txs to peer", async after: 0, newTransactions: [ { - privacy: "trusting", + privacy: "trusting" as const, madeAt: map.coValue.sessions[node.ownSessionID]! .transactions[0]!.madeAt, changes: [ @@ -291,7 +277,7 @@ test("After subscribing, node sends own known state and new txs to peer", async after: 1, newTransactions: [ { - privacy: "trusting", + privacy: "trusting" as const, madeAt: map.coValue.sessions[node.ownSessionID]! .transactions[1]!.madeAt, changes: [ @@ -312,10 +298,8 @@ test("After subscribing, node sends own known state and new txs to peer", async }); test("Client replies with known new content to tellKnownState from server", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -350,7 +334,7 @@ test("Client replies with known new content to tellKnownState from server", asyn }, }); - expect((await reader.read()).value).toMatchObject(admStateEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamStateEx(team)); const mapTellKnownStateMsg = await reader.read(); @@ -359,7 +343,7 @@ test("Client replies with known new content to tellKnownState from server", asyn ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapNewContentMsg = await reader.read(); @@ -373,7 +357,7 @@ test("Client replies with known new content to tellKnownState from server", asyn after: 0, newTransactions: [ { - privacy: "trusting", + privacy: "trusting" as const, madeAt: map.coValue.sessions[node.ownSessionID]! .transactions[0]!.madeAt, changes: [ @@ -394,10 +378,8 @@ test("Client replies with known new content to tellKnownState from server", asyn }); test("No matter the optimistic known state, node respects invalid known state messages and resyncs", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -426,7 +408,7 @@ test("No matter the optimistic known state, node respects invalid known state me const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject(admStateEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamStateEx(team)); const mapTellKnownStateMsg = await reader.read(); @@ -435,7 +417,7 @@ test("No matter the optimistic known state, node respects invalid known state me ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapNewContentHeaderOnlyMsg = await reader.read(); @@ -478,7 +460,7 @@ test("No matter the optimistic known state, node respects invalid known state me after: 1, newTransactions: [ { - privacy: "trusting", + privacy: "trusting" as const, madeAt: map.coValue.sessions[node.ownSessionID]! .transactions[1]!.madeAt, changes: [ @@ -499,10 +481,8 @@ test("No matter the optimistic known state, node respects invalid known state me }); test("If we add a peer, but it never subscribes to a coValue, it won't get any messages", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -524,14 +504,14 @@ test("If we add a peer, but it never subscribes to a coValue, it won't get any m const reader = outRx.getReader(); - await expect(shouldNotResolve(reader.read(), {timeout: 100})).resolves.toBeUndefined(); + await expect( + shouldNotResolve(reader.read(), { timeout: 100 }) + ).resolves.toBeUndefined(); }); test("If we add a server peer, all updates to all coValues are sent to it, even if it doesn't subscribe", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -548,10 +528,10 @@ test("If we add a server peer, all updates to all coValues are sent to it, even }); const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject({ - action: "subscribe", - coValueID: adminID, - }); + // expect((await reader.read()).value).toMatchObject({ + // action: "subscribe", + // coValueID: adminID, + // }); expect((await reader.read()).value).toMatchObject({ action: "subscribe", coValueID: team.teamMap.coValue.id, @@ -570,7 +550,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even editable.set("hello", "world", "trusting"); }); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapNewContentMsg = await reader.read(); @@ -584,7 +564,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even after: 0, newTransactions: [ { - privacy: "trusting", + privacy: "trusting" as const, madeAt: map.coValue.sessions[node.ownSessionID]! .transactions[0]!.madeAt, changes: [ @@ -605,10 +585,8 @@ test("If we add a server peer, all updates to all coValues are sent to it, even }); test("If we add a server peer, newly created coValues are auto-subscribed to", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -623,10 +601,10 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a }); const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject({ - action: "subscribe", - coValueID: adminID, - }); + // expect((await reader.read()).value).toMatchObject({ + // action: "subscribe", + // coValueID: admin.id, + // }); expect((await reader.read()).value).toMatchObject({ action: "subscribe", coValueID: team.teamMap.coValue.id, @@ -641,7 +619,7 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(adminID)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapContentMsg = await reader.read(); @@ -655,14 +633,12 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a }); test.todo( - "TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it", + "TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it" ); test("When we connect a new server peer, we try to sync all existing coValues to it", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -680,7 +656,7 @@ test("When we connect a new server peer, we try to sync all existing coValues to const reader = outRx.getReader(); - const _adminSubscribeMessage = await reader.read(); + // const _adminSubscribeMessage = await reader.read(); const teamSubscribeMessage = await reader.read(); expect(teamSubscribeMessage.value).toEqual({ @@ -697,10 +673,8 @@ test("When we connect a new server peer, we try to sync all existing coValues to }); test("When receiving a subscribe with a known state that is ahead of our own, peers should respond with a corresponding subscribe response message", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -729,7 +703,7 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject(admStateEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admStateEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamStateEx(team)); const mapTellKnownState = await reader.read(); @@ -741,10 +715,9 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe test.skip("When replaying creation and transactions of a coValue as new content, the receiving peer integrates this information", async () => { // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); + const [admin, session] = randomAnonymousAccountAndSessionID(); - const node1 = new LocalNode(admin, newRandomSessionID(adminID)); + const node1 = new LocalNode(admin, session); const team = node1.createTeam(); @@ -761,7 +734,7 @@ test.skip("When replaying creation and transactions of a coValue as new content, const to1 = inTx1.getWriter(); const from1 = outRx1.getReader(); - const node2 = new LocalNode(admin, newRandomSessionID(adminID)); + const node2 = new LocalNode(admin, newRandomSessionID(admin.id)); const [inRx2, inTx2] = newStreamPair(); const [outRx2, outTx2] = newStreamPair(); @@ -779,7 +752,7 @@ test.skip("When replaying creation and transactions of a coValue as new content, const adminSubscribeMessage = await from1.read(); expect(adminSubscribeMessage.value).toMatchObject({ action: "subscribe", - coValueID: adminID, + coValueID: admin.id, }); const teamSubscribeMsg = await from1.read(); expect(teamSubscribeMsg.value).toMatchObject({ @@ -790,8 +763,8 @@ test.skip("When replaying creation and transactions of a coValue as new content, await to2.write(adminSubscribeMessage.value!); await to2.write(teamSubscribeMsg.value!); - const adminTellKnownStateMsg = await from2.read(); - expect(adminTellKnownStateMsg.value).toMatchObject(admStateEx(adminID)); + // const adminTellKnownStateMsg = await from2.read(); + // expect(adminTellKnownStateMsg.value).toMatchObject(admStateEx(admin.id)); const teamTellKnownStateMsg = await from2.read(); expect(teamTellKnownStateMsg.value).toMatchObject(teamStateEx(team)); @@ -802,16 +775,16 @@ test.skip("When replaying creation and transactions of a coValue as new content, ] ).toBeDefined(); - await to1.write(adminTellKnownStateMsg.value!); + // await to1.write(adminTellKnownStateMsg.value!); await to1.write(teamTellKnownStateMsg.value!); - const adminContentMsg = await from1.read(); - expect(adminContentMsg.value).toMatchObject(admContEx(adminID)); + // const adminContentMsg = await from1.read(); + // expect(adminContentMsg.value).toMatchObject(admContEx(admin.id)); const teamContentMsg = await from1.read(); expect(teamContentMsg.value).toMatchObject(teamContentEx(team)); - await to2.write(adminContentMsg.value!); + // await to2.write(adminContentMsg.value!); await to2.write(teamContentMsg.value!); const map = team.createMap(); @@ -863,10 +836,9 @@ test.skip("When replaying creation and transactions of a coValue as new content, test.skip("When loading a coValue on one node, the server node it is requested from replies with all the necessary depended on coValues to make it work", async () => { // TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); + const [admin, session] = randomAnonymousAccountAndSessionID(); - const node1 = new LocalNode(admin, newRandomSessionID(adminID)); + const node1 = new LocalNode(admin, session); const team = node1.createTeam(); @@ -875,7 +847,7 @@ test.skip("When loading a coValue on one node, the server node it is requested f editable.set("hello", "world", "trusting"); }); - const node2 = new LocalNode(admin, newRandomSessionID(adminID)); + const node2 = new LocalNode(admin, newRandomSessionID(admin.id)); const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2"); @@ -892,10 +864,9 @@ test.skip("When loading a coValue on one node, the server node it is requested f }); test("Can sync a coValue through a server to another client", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); + const [admin, session] = randomAnonymousAccountAndSessionID(); - const client1 = new LocalNode(admin, newRandomSessionID(adminID)); + const client1 = new LocalNode(admin, session); const team = client1.createTeam(); @@ -904,10 +875,9 @@ test("Can sync a coValue through a server to another client", async () => { editable.set("hello", "world", "trusting"); }); - const serverUser = newRandomAgentCredential("serverUser"); - const serverUserID = getAgentID(getAgent(serverUser)); + const [serverUser, serverSession] = randomAnonymousAccountAndSessionID(); - const server = new LocalNode(serverUser, newRandomSessionID(serverUserID)); + const server = new LocalNode(serverUser, serverSession); const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", { peer1role: "server", @@ -917,7 +887,7 @@ test("Can sync a coValue through a server to another client", async () => { client1.sync.addPeer(serverAsPeer); server.sync.addPeer(client1AsPeer); - const client2 = new LocalNode(admin, newRandomSessionID(adminID)); + const client2 = new LocalNode(admin, newRandomSessionID(admin.id)); const [serverAsOtherPeer, client2AsPeer] = connectedPeers( "server", @@ -936,10 +906,9 @@ test("Can sync a coValue through a server to another client", async () => { }); test("Can sync a coValue with private transactions through a server to another client", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); + const [admin, session] = randomAnonymousAccountAndSessionID(); - const client1 = new LocalNode(admin, newRandomSessionID(adminID)); + const client1 = new LocalNode(admin, session); const team = client1.createTeam(); @@ -948,10 +917,9 @@ test("Can sync a coValue with private transactions through a server to another c editable.set("hello", "world", "private"); }); - const serverUser = newRandomAgentCredential("serverUser"); - const serverUserID = getAgentID(getAgent(serverUser)); + const [serverUser, serverSession] = randomAnonymousAccountAndSessionID(); - const server = new LocalNode(serverUser, newRandomSessionID(serverUserID)); + const server = new LocalNode(serverUser, serverSession); const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", { trace: true, @@ -962,7 +930,7 @@ test("Can sync a coValue with private transactions through a server to another c client1.sync.addPeer(serverAsPeer); server.sync.addPeer(client1AsPeer); - const client2 = new LocalNode(admin, newRandomSessionID(adminID)); + const client2 = new LocalNode(admin, newRandomSessionID(admin.id)); const [serverAsOtherPeer, client2AsPeer] = connectedPeers( "server", @@ -981,10 +949,8 @@ test("Can sync a coValue with private transactions through a server to another c }); test("When a peer's incoming/readable stream closes, we remove the peer", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -999,10 +965,10 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async }); const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject({ - action: "subscribe", - coValueID: adminID, - }); + // expect((await reader.read()).value).toMatchObject({ + // action: "subscribe", + // coValueID: admin.id, + // }); expect((await reader.read()).value).toMatchObject({ action: "subscribe", coValueID: team.teamMap.coValue.id, @@ -1017,7 +983,7 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapContentMsg = await reader.read(); @@ -1037,10 +1003,8 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async }); test("When a peer's outgoing/writable stream closes, we remove the peer", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); - - const node = new LocalNode(admin, newRandomSessionID(adminID)); + const [admin, session] = randomAnonymousAccountAndSessionID(); + const node = new LocalNode(admin, session); const team = node.createTeam(); @@ -1055,10 +1019,10 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async }); const reader = outRx.getReader(); - expect((await reader.read()).value).toMatchObject({ - action: "subscribe", - coValueID: adminID, - }); + // expect((await reader.read()).value).toMatchObject({ + // action: "subscribe", + // coValueID: admin.id, + // }); expect((await reader.read()).value).toMatchObject({ action: "subscribe", coValueID: team.teamMap.coValue.id, @@ -1073,7 +1037,7 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async ...map.coValue.knownState(), } satisfies SyncMessage); - expect((await reader.read()).value).toMatchObject(admContEx(adminID)); + // expect((await reader.read()).value).toMatchObject(admContEx(admin.id)); expect((await reader.read()).value).toMatchObject(teamContentEx(team)); const mapContentMsg = await reader.read(); @@ -1095,13 +1059,12 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async await new Promise((resolve) => setTimeout(resolve, 100)); expect(node.sync.peers["test"]).toBeUndefined(); -}) +}); test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => { - const admin = newRandomAgentCredential("admin"); - const adminID = getAgentID(getAgent(admin)); + const [admin, session] = randomAnonymousAccountAndSessionID(); - const node1 = new LocalNode(admin, newRandomSessionID(adminID)); + const node1 = new LocalNode(admin, session); const team = node1.createTeam(); @@ -1110,9 +1073,13 @@ test("If we start loading a coValue before connecting to a peer that has it, it editable.set("hello", "world", "trusting"); }); - const node2 = new LocalNode(admin, newRandomSessionID(adminID)); + const node2 = new LocalNode(admin, newRandomSessionID(admin.id)); - const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {peer1role: 'server', peer2role: 'client', trace: true}); + const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", { + peer1role: "server", + peer2role: "client", + trace: true, + }); node1.sync.addPeer(node2asPeer); @@ -1127,7 +1094,7 @@ test("If we start loading a coValue before connecting to a peer that has it, it expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual( "world" ); -}) +}); function teamContentEx(team: Team) { return { @@ -1136,7 +1103,7 @@ function teamContentEx(team: Team) { }; } -function admContEx(adminID: AgentID) { +function admContEx(adminID: AccountID) { return { action: "newContent", coValueID: adminID, @@ -1150,7 +1117,7 @@ function teamStateEx(team: Team) { }; } -function admStateEx(adminID: AgentID) { +function admStateEx(adminID: AccountID) { return { action: "tellKnownState", coValueID: adminID, @@ -1188,11 +1155,13 @@ function newStreamPair(): [ReadableStream, WritableStream] { await nextItemReady; } } - throw new Error("Should only use one retry to get next item in queue.") + throw new Error( + "Should only use one retry to get next item in queue." + ); }, cancel(reason) { - console.log("Manually closing reader") + console.log("Manually closing reader"); readerClosed = true; }, }); @@ -1210,7 +1179,7 @@ function newStreamPair(): [ReadableStream, WritableStream] { } }, abort(reason) { - console.log("Manually closing writer") + console.log("Manually closing writer"); writerClosed = true; resolveNextItemReady(); return Promise.resolve(); @@ -1220,7 +1189,10 @@ function newStreamPair(): [ReadableStream, WritableStream] { return [readable, writable]; } -function shouldNotResolve(promise: Promise, ops: { timeout: number }): Promise { +function shouldNotResolve( + promise: Promise, + ops: { timeout: number } +): Promise { return new Promise((resolve, reject) => { promise .then((v) => diff --git a/src/testUtils.ts b/src/testUtils.ts new file mode 100644 index 000000000..ce6a8a860 --- /dev/null +++ b/src/testUtils.ts @@ -0,0 +1,79 @@ +import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js"; +import { SessionID, newRandomSessionID } from "./coValue.js"; +import { LocalNode } from "./node.js"; +import { expectTeamContent } from "./permissions.js"; +import { AnonymousControlledAccount } from "./account.js"; + +export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccount, SessionID] { + const agentSecret = newRandomAgentSecret(); + + const sessionID = newRandomSessionID(getAgentID(agentSecret)); + + return [new AnonymousControlledAccount(agentSecret), sessionID]; +} + +export function newTeam() { + const [admin, sessionID] = randomAnonymousAccountAndSessionID(); + + const node = new LocalNode(admin, sessionID); + + const team = node.createCoValue({ + type: "comap", + ruleset: { type: "team", initialAdmin: admin.id }, + meta: null, + ...createdNowUnique(), + publicNickname: "team", + }); + + const teamContent = expectTeamContent(team.getCurrentContent()); + + teamContent.edit((editable) => { + editable.set(admin.id, "admin", "trusting"); + expect(editable.get(admin.id)).toEqual("admin"); + }); + + return { node, team, admin }; +} + +export function teamWithTwoAdmins() { + const { team, admin, node } = newTeam(); + + const otherAdmin = node.createAccount("otherAdmin"); + + let content = expectTeamContent(team.getCurrentContent()); + + content.edit((editable) => { + editable.set(otherAdmin.id, "admin", "trusting"); + expect(editable.get(otherAdmin.id)).toEqual("admin"); + }); + + content = expectTeamContent(team.getCurrentContent()); + + if (content.type !== "comap") { + throw new Error("Expected map"); + } + + expect(content.get(otherAdmin.id)).toEqual("admin"); + return { team, admin, otherAdmin, node }; +} + +export function newTeamHighLevel() { + const [admin, sessionID] = randomAnonymousAccountAndSessionID(); + + + const node = new LocalNode(admin, sessionID); + + const team = node.createTeam(); + + return { admin, node, team }; +} + +export function teamWithTwoAdminsHighLevel() { + const { admin, node, team } = newTeamHighLevel(); + + const otherAdmin = node.createAccount("otherAdmin"); + + team.addMember(otherAdmin.id, "admin"); + + return { admin, node, team, otherAdmin }; +} \ No newline at end of file From 60add421e6234ee414feaffe83878268498e007a Mon Sep 17 00:00:00 2001 From: Anselm Date: Mon, 14 Aug 2023 15:13:39 +0100 Subject: [PATCH 2/2] Rename RawAgentID to AgentID --- src/account.ts | 22 +++++++++++----------- src/crypto.ts | 8 ++++---- src/ids.ts | 4 ++-- src/node.ts | 6 +++--- src/permissions.ts | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/account.ts b/src/account.ts index 551568ef2..a12e50468 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,15 +1,15 @@ import { CoValueHeader } from './coValue.js'; import { CoValueID } from './contentType.js'; import { AgentSecret, RecipientID, RecipientSecret, SignatoryID, SignatorySecret, getAgentID, getAgentRecipientID, getAgentRecipientSecret, getAgentSignatoryID, getAgentSignatorySecret } from './crypto.js'; -import { RawAgentID } from './ids.js'; +import { AgentID } from './ids.js'; import { CoMap, LocalNode } from './index.js'; import { Team, TeamContent } from './permissions.js'; export function accountHeaderForInitialAgentSecret(agentSecret: AgentSecret): CoValueHeader { - const rawAgentID = getAgentID(agentSecret); + const agent = getAgentID(agentSecret); return { type: "comap", - ruleset: {type: "team", initialAdmin: rawAgentID}, + ruleset: {type: "team", initialAdmin: agent}, meta: { type: "account" }, @@ -23,8 +23,8 @@ export class Account extends Team { return this.teamMap.id; } - getCurrentAgentID(): RawAgentID { - const agents = this.teamMap.keys().filter((k): k is RawAgentID => k.startsWith("recipient_")); + getCurrentAgentID(): AgentID { + const agents = this.teamMap.keys().filter((k): k is AgentID => k.startsWith("recipient_")); if (agents.length !== 1) { throw new Error("Expected exactly one agent in account, got " + agents.length); @@ -38,7 +38,7 @@ export interface GeneralizedControlledAccount { id: AccountIDOrAgentID; agentSecret: AgentSecret; - currentAgentID: () => RawAgentID; + currentAgentID: () => AgentID; currentSignatoryID: () => SignatoryID; currentSignatorySecret: () => SignatorySecret; currentRecipientID: () => RecipientID; @@ -54,7 +54,7 @@ export class ControlledAccount extends Account implements GeneralizedControlledA this.agentSecret = agentSecret; } - currentAgentID(): RawAgentID { + currentAgentID(): AgentID { return getAgentID(this.agentSecret); } @@ -82,11 +82,11 @@ export class AnonymousControlledAccount implements GeneralizedControlledAccount this.agentSecret = agentSecret; } - get id(): RawAgentID { + get id(): AgentID { return getAgentID(this.agentSecret); } - currentAgentID(): RawAgentID { + currentAgentID(): AgentID { return getAgentID(this.agentSecret); } @@ -110,6 +110,6 @@ export class AnonymousControlledAccount implements GeneralizedControlledAccount export type AccountMeta = {type: "account"}; export type AccountID = CoValueID>; -export type AccountIDOrAgentID = RawAgentID | AccountID; -export type AccountOrAgentID = RawAgentID | Account; +export type AccountIDOrAgentID = AgentID | AccountID; +export type AccountOrAgentID = AgentID | Account; export type AccountOrAgentSecret = AgentSecret | Account; diff --git a/src/crypto.ts b/src/crypto.ts index 9553207d8..97624a0ac 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -5,7 +5,7 @@ import { base58, base64url } from "@scure/base"; import stableStringify from "fast-json-stable-stringify"; import { blake3 } from "@noble/hashes/blake3"; import { randomBytes } from "@noble/ciphers/webcrypto/utils"; -import { RawAgentID, RawCoValueID, TransactionID } from './ids.js'; +import { AgentID, RawCoValueID, TransactionID } from './ids.js'; export type SignatorySecret = `signatorySecret_z${string}`; export type SignatoryID = `signatory_z${string}`; @@ -104,14 +104,14 @@ export function agentSecretFromBytes(bytes: Uint8Array): AgentSecret { return `${recipientSecret}/${signatorySecret}`; } -export function getAgentID(secret: AgentSecret): RawAgentID { +export function getAgentID(secret: AgentSecret): AgentID { const [recipientSecret, signatorySecret] = secret.split("/"); return `${getRecipientID( recipientSecret as RecipientSecret )}/${getSignatoryID(signatorySecret as SignatorySecret)}`; } -export function getAgentSignatoryID(agentId: RawAgentID): SignatoryID { +export function getAgentSignatoryID(agentId: AgentID): SignatoryID { return agentId.split("/")[1] as SignatoryID; } @@ -119,7 +119,7 @@ export function getAgentSignatorySecret(agentSecret: AgentSecret): SignatorySecr return agentSecret.split("/")[1] as SignatorySecret; } -export function getAgentRecipientID(agentId: RawAgentID): RecipientID { +export function getAgentRecipientID(agentId: AgentID): RecipientID { return agentId.split("/")[0] as RecipientID; } diff --git a/src/ids.ts b/src/ids.ts index 92c7d39ca..39f11d26b 100644 --- a/src/ids.ts +++ b/src/ids.ts @@ -4,9 +4,9 @@ export type RawCoValueID = `co_z${string}` | `co_${string}_z${string}`; export type TransactionID = { sessionID: SessionID; txIndex: number }; -export type RawAgentID = `recipient_z${string}/signatory_z${string}`; +export type AgentID = `recipient_z${string}/signatory_z${string}`; -export function isRawAgentID(id: string): id is RawAgentID { +export function isAgentID(id: string): id is AgentID { return typeof id === "string" && id.startsWith("recipient_") && id.includes("/signatory_"); } diff --git a/src/node.ts b/src/node.ts index b85992e75..2d4161574 100644 --- a/src/node.ts +++ b/src/node.ts @@ -10,7 +10,7 @@ import { import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js"; import { Team, TeamContent, expectTeamContent } from "./permissions.js"; import { SyncManager } from "./sync.js"; -import { RawAgentID, RawCoValueID, SessionID, isRawAgentID } from "./ids.js"; +import { AgentID, RawCoValueID, SessionID, isAgentID } from "./ids.js"; import { CoValueID, ContentType } from "./contentType.js"; import { Account, @@ -117,8 +117,8 @@ export class LocalNode { ); } - resolveAccount(id: AccountIDOrAgentID, expectation?: string): RawAgentID { - if (isRawAgentID(id)) { + resolveAccount(id: AccountIDOrAgentID, expectation?: string): AgentID { + if (isAgentID(id)) { return id; } diff --git a/src/permissions.ts b/src/permissions.ts index a399d7c54..b1d71f4b3 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -19,7 +19,7 @@ import { accountOrAgentIDfromSessionID, } from './coValue.js'; import { LocalNode } from "./node.js"; -import { RawCoValueID, SessionID, TransactionID, isRawAgentID } from './ids.js'; +import { RawCoValueID, SessionID, TransactionID, isAgentID } from './ids.js'; import { AccountIDOrAgentID, GeneralizedControlledAccount } from './account.js'; export type PermissionsDef = @@ -264,7 +264,7 @@ export class Team { rotateReadKey() { const currentlyPermittedReaders = this.teamMap.keys().filter((key) => { - if (key.startsWith("co_") || isRawAgentID(key)) { + if (key.startsWith("co_") || isAgentID(key)) { const role = this.teamMap.get(key); return ( role === "admin" || role === "writer" || role === "reader"