Merge pull request #6 from gardencmp/anselm/gar-81-accountsagents-as-teams
Implement accounts as teams
This commit is contained in:
115
src/account.ts
Normal file
115
src/account.ts
Normal file
@@ -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 { AgentID } from './ids.js';
|
||||
import { CoMap, LocalNode } from './index.js';
|
||||
import { Team, TeamContent } from './permissions.js';
|
||||
|
||||
export function accountHeaderForInitialAgentSecret(agentSecret: AgentSecret): CoValueHeader {
|
||||
const agent = getAgentID(agentSecret);
|
||||
return {
|
||||
type: "comap",
|
||||
ruleset: {type: "team", initialAdmin: agent},
|
||||
meta: {
|
||||
type: "account"
|
||||
},
|
||||
createdAt: null,
|
||||
uniqueness: null,
|
||||
}
|
||||
}
|
||||
|
||||
export class Account extends Team {
|
||||
get id(): AccountID {
|
||||
return this.teamMap.id;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return agents[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GeneralizedControlledAccount {
|
||||
id: AccountIDOrAgentID;
|
||||
agentSecret: AgentSecret;
|
||||
|
||||
currentAgentID: () => AgentID;
|
||||
currentSignatoryID: () => SignatoryID;
|
||||
currentSignatorySecret: () => SignatorySecret;
|
||||
currentRecipientID: () => RecipientID;
|
||||
currentRecipientSecret: () => RecipientSecret;
|
||||
}
|
||||
|
||||
export class ControlledAccount extends Account implements GeneralizedControlledAccount {
|
||||
agentSecret: AgentSecret;
|
||||
|
||||
constructor(agentSecret: AgentSecret, teamMap: CoMap<TeamContent, {}>, node: LocalNode) {
|
||||
super(teamMap, node);
|
||||
|
||||
this.agentSecret = agentSecret;
|
||||
}
|
||||
|
||||
currentAgentID(): AgentID {
|
||||
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(): AgentID {
|
||||
return getAgentID(this.agentSecret);
|
||||
}
|
||||
|
||||
currentAgentID(): AgentID {
|
||||
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<CoMap<TeamContent, AccountMeta>>;
|
||||
|
||||
export type AccountIDOrAgentID = AgentID | AccountID;
|
||||
export type AccountOrAgentID = AgentID | Account;
|
||||
export type AccountOrAgentSecret = AgentSecret | Account;
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
176
src/coValue.ts
176
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<AgentID, Role, {[agent: AgentID]: Role}>;
|
||||
export { SessionID };
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 { AgentID, 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<T> = `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): AgentID {
|
||||
const [recipientSecret, signatorySecret] = secret.split("/");
|
||||
return `${getRecipientID(
|
||||
recipientSecret as RecipientSecret
|
||||
)}/${getSignatoryID(signatorySecret as SignatorySecret)}`;
|
||||
}
|
||||
|
||||
export function getAgentSignatoryID(agentId: AgentID): SignatoryID {
|
||||
return agentId.split("/")[1] as SignatoryID;
|
||||
}
|
||||
|
||||
export function getAgentSignatorySecret(agentSecret: AgentSecret): SignatorySecret {
|
||||
return agentSecret.split("/")[1] as SignatorySecret;
|
||||
}
|
||||
|
||||
export function getAgentRecipientID(agentId: AgentID): RecipientID {
|
||||
return agentId.split("/")[0] as RecipientID;
|
||||
}
|
||||
|
||||
export function getAgentRecipientSecret(agentSecret: AgentSecret): RecipientSecret {
|
||||
return agentSecret.split("/")[0] as RecipientSecret;
|
||||
}
|
||||
|
||||
export type SealedSet<T> = {
|
||||
[recipient: RecipientID]: Sealed<T>;
|
||||
};
|
||||
|
||||
10
src/ids.ts
10
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 AgentID = `recipient_z${string}/signatory_z${string}`;
|
||||
|
||||
export type SessionID = `${AgentID}_session_z${string}`;
|
||||
export function isAgentID(id: string): id is AgentID {
|
||||
return typeof id === "string" && id.startsWith("recipient_") && id.includes("/signatory_");
|
||||
}
|
||||
|
||||
export type SessionID = `${AccountIDOrAgentID}_session_z${string}`;
|
||||
|
||||
37
src/index.ts
37
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,
|
||||
};
|
||||
|
||||
133
src/node.ts
133
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 { AgentID, RawCoValueID, SessionID, isAgentID } 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<TeamContent, AccountMeta>,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
expectAgentLoaded(id: AgentID, expectation?: string): Agent {
|
||||
const coValue = this.expectCoValueLoaded(
|
||||
id,
|
||||
expectation
|
||||
);
|
||||
resolveAccount(id: AccountIDOrAgentID, expectation?: string): AgentID {
|
||||
if (isAgentID(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<TeamContent, AccountMeta>,
|
||||
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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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, isAgentID } 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<AgentID, Role>
|
||||
| MapOpPayload<AccountIDOrAgentID, Role>
|
||||
| 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<KeySecret>;
|
||||
@@ -230,20 +221,20 @@ export class Team {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
get id(): RawCoValueID {
|
||||
get id(): CoValueID<CoMap<TeamContent, {}>> {
|
||||
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_") || isAgentID(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<M, Meta>;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
394
src/sync.test.ts
394
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<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
|
||||
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
||||
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
||||
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<string, string>,
|
||||
],
|
||||
},
|
||||
],
|
||||
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<string, string>,
|
||||
],
|
||||
},
|
||||
],
|
||||
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<SyncMessage>();
|
||||
const [outRx2, outTx2] = newStreamPair<SyncMessage>();
|
||||
@@ -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<T>(): [ReadableStream<T>, WritableStream<T>] {
|
||||
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<T>(): [ReadableStream<T>, WritableStream<T>] {
|
||||
}
|
||||
},
|
||||
abort(reason) {
|
||||
console.log("Manually closing writer")
|
||||
console.log("Manually closing writer");
|
||||
writerClosed = true;
|
||||
resolveNextItemReady();
|
||||
return Promise.resolve();
|
||||
@@ -1220,7 +1189,10 @@ function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
||||
return [readable, writable];
|
||||
}
|
||||
|
||||
function shouldNotResolve<T>(promise: Promise<T>, ops: { timeout: number }): Promise<void> {
|
||||
function shouldNotResolve<T>(
|
||||
promise: Promise<T>,
|
||||
ops: { timeout: number }
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
promise
|
||||
.then((v) =>
|
||||
|
||||
79
src/testUtils.ts
Normal file
79
src/testUtils.ts
Normal file
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user