import { EventEmitter } from '@herbcaudill/eventemitter42'; import type { Hash, KeyMetadata, KeyScope, Keyring, Keyset, KeysetWithSecrets, Payload, UnixTimestamp, UserWithSecrets } from '@localfirst/crdx'; import { type Base58 } from '@localfirst/crypto'; import { type Challenge } from 'connection/types.js'; import * as devices from 'device/index.js'; import { type Device } from 'device/index.js'; import * as invitations from 'invitation/index.js'; import { type ProofOfInvitation } from 'invitation/index.js'; import { type Role } from 'role/index.js'; import { type Host, type Server } from 'server/types.js'; import type { EncryptedEnvelope, InviteResult, Member, SignedEnvelope, TeamAction, TeamGraph, TeamOptions, TeamState } from './types.js'; /** * The `Team` class wraps a `TeamGraph` and exposes methods for adding and removing * members, assigning roles, creating and using invitations, and encrypting messages for * individuals, for the team, or for members of specific roles. */ export declare class Team extends EventEmitter { state: TeamState; private readonly store; private readonly context; private readonly log; private readonly seed; /** * We can make a team instance either by creating a brand-new team, or restoring one from a stored graph. */ constructor(options: TeamOptions); /** ************** PUBLIC API */ get graph(): TeamGraph; /** We use the hash of the graph's root as a unique ID for the team. */ get id(): Base58; /** Returns this team's user-facing name. */ get teamName(): string; setTeamName(teamName: string): void; /** ************** CONTEXT */ get userName(): string; get userId(): string; private get isServer(); /** ************** TEAM STATE * * All the logic for *reading* team state is in selectors (see `/team/selectors`). * * Most of the logic for *modifying* team state is in transforms (see `/team/transforms`), which * are executed by the reducer. To mutate team state, we dispatch changes to the graph, and then * run the graph through the reducer to recalculate team state. * * Any crypto operations involving the current user's secrets (for example, opening or creating * lockboxes, or signing links) are done here, not in the selectors or in the reducer. Only the * public-facing outputs (for example, the resulting lockboxesInScope, or the signed links) are * posted on the graph. */ save: () => Uint8Array; /** * Merges another graph (e.g. from a peer) with ours. * @returns This `Team` instance. */ merge: (theirGraph: TeamGraph) => this; /** Add a link to the graph, then recompute team state from the new graph */ dispatch(action: TeamAction, teamKeys?: KeysetWithSecrets): void; /** ************** MEMBERS */ /** Returns true if the team has a member with the given userId */ has: (userId: string) => boolean; /** Returns a list of all members on the team */ members(): Member[]; /** Returns the member with the given user name */ members(userId: string, options?: LookupOptions): Member; /** * Adds a member to the team, along with an (optional) device. Since this method assumes that you * know the member's secret keys, it only makes sense for unit tests. In real-world scenarios, * you'll need to use the `team.invite` workflow to add members without relying on some kind of * public key infrastructure. * * This can be used to add a device for an existing member - just pass the existing user as the * first argument. */ addForTesting: (user: UserWithSecrets, roles?: string[], device?: Device) => void; /** Remove a member from the team */ remove: (userId: string) => void; /** Returns true if the member was once on the team but was removed */ memberWasRemoved: (userId: string) => boolean; /** ************** ROLES */ /** Returns all roles in the team */ roles(): Role[]; /** Returns the role with the given name */ roles(roleName: string): Role; /** Returns true if the member with the given userId has the given role */ memberHasRole: (userId: string, roleName: string) => boolean; /** Returns true if the member with the given userId is a member of the 3 role */ memberIsAdmin: (userId: string) => boolean; /** Returns true if the team has a role with the given name */ hasRole: (roleName: string) => boolean; /** Returns a list of members who have the given role */ membersInRole: (roleName: string) => Member[]; /** Returns a list of members who are in the admin role */ admins: () => Member[]; /** Add a role to the team */ addRole: (role: Role | string) => void; /** Remove a role from the team */ removeRole: (roleName: string) => void; /** Give a member a role */ addMemberRole: (userId: string, roleName: string) => void; /** Remove a role from a member */ removeMemberRole: (userId: string, roleName: string) => void; /** ************** DEVICES */ /** Returns true if the given member has a device by the given name */ hasDevice: (deviceId: string, options?: LookupOptions) => boolean; /** Find a member's device by name */ device(deviceId: string, options?: LookupOptions): Device; /** Remove a member's device */ removeDevice: (deviceId: string) => void; /** Returns true if the device was once on the team but was removed */ deviceWasRemoved: (deviceId: string) => boolean; /** Looks for a member that has this device. If none is found, return */ memberByDeviceId: (deviceId: string, options?: LookupOptions) => Member; verifyIdentityProof: (challenge: Challenge, proof: Base58) => boolean; /** ************** INVITATIONS */ /** * To invite a new member: * * Alice generates an invitation using a secret seed. The seed an be randomly generated, or * selected by Alice. Alice sends the invitation to Bob using a trusted channel. * * Meanwhile, Alice adds Bob to the graph as a new member, with appropriate roles (if * any) and any corresponding lockboxes. * * Bob can't authenticate directly as that member, since it has random temporary keys created by * Alice. Instead, Bob generates a proof of invitation, and when they try to connect to Alice or * Charlie they present that proof instead of authenticating. * * Once Alice or Charlie verifies Bob's proof, they send him the team graph. Bob uses that to * instantiate the team, then he updates the team with his real public keys and adds his current * device information. */ inviteMember({ seed, expiration, maxUses, }?: { /** A secret to be passed to the invitee via a side channel. If not provided, one will be randomly generated. */ seed?: string; /** Time when the invitation expires. If not provided, the invitation does not expire. */ expiration?: UnixTimestamp; /** Number of times the invitation can be used. If not provided, the invitation can be used any number of times. */ maxUses?: number; }): InviteResult; /** * To invite an existing member's device: * * On his laptop, Bob generates an invitation using a secret seed. He gets that seed to his phone * using a QR code or by typing it in. * * On his phone, Bob connects to his laptop (or to Alice or Charlie). Bob's phone presents its * proof of invitation. * * Once an existing device (Bob's laptop or Alice or Charlie) verifies Bob's phone's proof, they * send it the team graph. Using the graph, the phone instantiates the team, then adds itself as * a device. */ inviteDevice({ seed, expiration, }?: { /** A secret to be passed to the device via a side channel. If not provided, one will be randomly generated. */ seed?: string; /** Time when the invitation expires. Defaults to 30 minutes from now. */ expiration?: UnixTimestamp; }): InviteResult; /** Revoke an invitation. */ revokeInvitation: (id: string) => void; /** Returns true if the invitation has ever existed in this team (even if it's been used or revoked) */ hasInvitation(id: Base58): boolean; /** Gets the invitation corresponding to the given id. If it does not exist, throws an error. */ getInvitation: (id: Base58) => invitations.InvitationState; /** Check whether (1) the invitation is still valid, and (2) the proof of invitation checks out. */ validateInvitation: (proof: ProofOfInvitation) => import("util/index.js").ValidationResult; /** Check if userId and userName are not used by any other member within the team. */ validateUser: (userId: string, userName: string) => import("util/index.js").ValidationResult; /** An existing team member calls this to admit a new member & their device to the team based on proof of invitation */ admitMember: (proof: ProofOfInvitation, memberKeys: Keyset | KeysetWithSecrets, userName: string) => void; /** An existing team member calls this to admit a new device based on proof of invitation */ admitDevice: (proof: ProofOfInvitation, firstUseDevice: devices.FirstUseDevice) => void; /** Once the new member has received the graph and can instantiate the team, they call this to add their device. */ join: (teamKeyring: Keyring, userKeyring?: Keyring) => void; /** ************** SERVERS */ /** * A server is an always-on, always-connected device that is available to the team but does not * belong to any one member. For example, `automerge-repo` calls this a "sync server". * * A server has a host name that uniquely identifies it (e.g. `example.com`, `localhost:8080`, or * `188.26.221.135`). * * The expected usage is for the application to add a server or servers immediately after the team * is created. However, the application can add or remove servers at any time. * * Just before adding a server, the application should send it the latest graph and the team keys * (so it can decrypt the team graph). No invitation or authentication is necessary in this phase, * as a TLS connection to a trusted address is sufficient to ensure the security of that * connection. In response, the server should send back its public keys. This library is not * involved in that process. * * The application should then add the server to the team using `addServer`, passing in the * server's public keys. At that point the server will be able to authenticate with other devices * using the same protocol as for members. * * The only actions that a server can dispatch to the graph are `ADMIT_MEMBER` and `ADMIT_DEVICE`. * The server needs to be able to admit invited members and devices in order to support * star-shaped networks where every device connects to a server, rather than directly to each * other.) */ addServer: (server: Server) => void; /** Removes a server from the team. */ removeServer: (host: string) => void; /** Returns a list of all servers on the team. */ servers(): Server[]; /** Returns the server with the given host */ servers(host: Host, options?: { includeRemoved: boolean; }): Server; /** Returns true if the server was once on the team but was removed */ serverWasRemoved: (host: Host) => boolean; hasServer: (host: Host) => boolean; /** ************** MESSAGES */ addMessage: (message: unknown) => void; messages: () => T[]; /** ************** CRYPTO */ /** * Symmetrically encrypt a payload for the given scope using keys available to the current user. * * > *Note*: Since this convenience function uses symmetric encryption, we can only use it to * encrypt for scopes the current user has keys for (e.g. the whole team, or roles they belong * to). If we need to encrypt asymmetrically, we use the functions in the crypto module directly. */ encrypt: (payload: Payload, roleName?: string) => EncryptedEnvelope; /** Decrypt a payload using keys available to the current user. */ decrypt: (message: EncryptedEnvelope) => Payload; /** Sign a message using the current user's keys. */ sign: (contents: Payload) => SignedEnvelope; /** Verify a signed message against the author's public key */ verify: (message: SignedEnvelope) => boolean; /** ************** KEYS * * These methods all return keysets *with secrets* that are available to the local user. To get * other members' public keys, look up the member - the `keys` property contains their public keys. */ /** * Returns the secret keyset (if available to the current device) for the given type and name. To * get other members' public keys, look up the member - the `keys` property contains their public * keys. */ keys: (scope: KeyMetadata | KeyScope) => KeysetWithSecrets; userKeyring: (userId?: string) => Keyring; /** Returns the keys for the given role. */ roleKeys: (roleName: string, generation?: number) => KeysetWithSecrets; /** Returns the current team keys or a specific generation of team keys */ teamKeys: (generation?: number) => KeysetWithSecrets; teamKeyring: () => Keyring; /** Returns the admin keyset. */ adminKeys: (generation?: number) => KeysetWithSecrets; /** * Replaces the current user or device's secret keyset with the one provided. * (This can also be used by an admin to change another user's secret keyset.) */ changeKeys: (newKeys: KeysetWithSecrets) => void; private updateUserKeys; private checkForPendingKeyRotations; private readonly createMemberLockboxes; /** * Given a compromised scope (e.g. a member or a role), find all scopes that are visible from that * scope, and generates new keys and lockboxes for each of those. Returns all of the new lockboxes * in a single array to be posted to the graph. * * You can pass it a scope, or a keyset (which includes the scope information). If you pass a * keyset, it will replace the existing keys with these. * * @param compromised If `compromised` is a keyset, that will become the new keyset for the * compromised scope. If it is just a scope, new keys will be randomly generated for that scope. */ private readonly rotateKeys; } type LookupOptions = { includeRemoved: boolean; }; type TeamEvents = { updated: (payload: { head: Hash[]; }) => void; }; export {}; //# sourceMappingURL=Team.d.ts.map