diff --git a/src/authstore/README.md b/src/authstore/README.md deleted file mode 100644 index c1bdb6613..000000000 --- a/src/authstore/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# AuthStore - -> Device access and capability authorization. - -## Purpose - -AuthStore is an internal Mapeo Core library used to authorize & verify device access and capabilities through a tree of statements describing each device's role and its relationships to other devices. - -The `AuthStore` class is responsible for authorizing access to Mapeo projects by defining the capabilities of each device through relationships with other devices. The `AuthStore` API is primarily meant to be internal to the higher-level `Mapeo` class API rather than used directly. - -When a project is created, `AuthStore` writes records about the project, project creator, and the project creator's device. -`AuthStore` is a collection of linked statements about the relationships between devices and the roles & capabilities assigned to each device. The tree of statements starts with the device assigned as project creator, which can then add devices and assign them roles. - -By traversing the links between statements about devices and verifying the signatures of statements we can validate each device's capabilities. - -### Available roles & capabilities - -In its current implementation AuthStore has three types of statements that it uses to describe devices: `Device`, `Role`, `CoreOwnership`. - -The device statements are used to add, remove, and restore device access. The role statements are used to assign the role and its associated capabilities to a device. The core ownership statements are used to prove that a device owns a specific hypercore. - -Each device in a project has a role. Roles define the capabilities a device has in the project. Roles are assigned by other devices. In assigning roles there is a tree of relationships between devices that can be used to validate a device's role & capabilities. - -There is a limited set of capabilities that can be assigned to each role: - -- `read` - read data & media in a project -- `write` - create new data & media in a project -- `edit` - edit data & media in a project -- `manage:devices` - assign roles and add, remove, and restore devices in a project - -There are two required roles: - -- `project-creator` - - capabilities: `read`, `write`, `edit`, `manage:devices` -- `non-member` - - capabilities: none - -There are two other default roles: - -- `coordinator` - - capabilities: `read`, `write`, `edit`, `manage:devices` -- `member` - - capabilities: `read`, `write` - -Default roles can be overriden through optional project config. - -Roles of devices can be changed by devices who have the `project-creator` and `coordinator` roles, as well as custom roles with the `manage:devices` capability. Devices can be removed from a project by a `project-creator` or `coordinator`. Access to a project can later be restored if needed. - -The approach used so far makes it possible to define new types of statements about devices and their relationships. We could in the future, for example, add device statements that links two devices as being owned by the same user, or add a capability statement that makes it possible to add specific capabilities in addition to those specified by a role. - -### Project creator - -As the root of the tree of relationships between devices, the public key of the `AuthStore` keyPair of the project creator is also the public key of the project. A project creator can't be removed from a project, however a group of coordinators could decide to fork a project with a new project creator and retain the data from the existing project. - -### Capability data models - -Each type of capability is a `DataType`, each with its own schema, validation, and indexing. The data types are stored in a `DataStore` instance internal to the `AuthStore` class. - -See the available data schemas in [authtypes.js](authtypes.js). - -### Syncing data - -When syncing a project the `AuthStore` hypercores are synced first. Sync then continues with other data after the device has been authorized by verifying its role & capabilities. - -## Usage - -TODO! - -## API docs - -TODO! - -## Tests - -Tests for this module are in [tests/authstore.js](../../tests/authstore.js) diff --git a/src/authstore/authtypes.js b/src/authstore/authtypes.js deleted file mode 100644 index 0c2a35951..000000000 --- a/src/authstore/authtypes.js +++ /dev/null @@ -1,145 +0,0 @@ -// @ts-nocheck -export const availableCapabilities = ['read', 'write', 'edit', 'manage:devices'] - -/** @type {AvailableRoles} */ -export const defaultRoles = [ - { - name: 'project-creator', - capabilities: ['read', 'write', 'manage:devices'], - }, - { - name: 'coordinator', - capabilities: ['read', 'write', 'manage:devices'], - }, - { - name: 'member', - capabilities: ['read', 'write'], - }, - { - name: 'non-member', - capabilities: [], - }, -] - -export const coreOwnership = { - name: 'coreOwnership', - blockPrefix: '0', - schema: { - type: 'object', - properties: { - id: { type: 'string' }, - type: { type: 'string', pattern: '^coreOwnership$' }, - timestamp: { type: 'integer' }, - links: { - type: 'array', - uniqueItems: true, - items: { type: 'string' }, - }, - version: { type: 'string' }, - action: { - type: 'string', - enum: ['core:owner'], - }, - coreId: { type: 'string' }, - projectId: { type: 'string' }, - storeType: { type: 'string' }, - signature: { type: 'string' }, - authorIndex: { type: 'integer' }, - deviceIndex: { type: 'integer' }, - }, - }, - extraColumns: ` - created INTEGER, - timestamp INTEGER, - authorId TEXT, - type TEXT, - action TEXT, - coreId TEXT, - projectId TEXT, - storeType TEXT, - authorIndex INTEGER, - deviceIndex INTEGER, - signature TEXT - `, -} - -export const devices = { - name: 'devices', - blockPrefix: '1', - schema: { - properties: { - id: { type: 'string' }, - type: { type: 'string', pattern: '^devices$' }, - timestamp: { type: 'integer' }, - links: { - type: 'array', - uniqueItems: true, - items: { type: 'string' }, - }, - version: { type: 'string' }, - action: { - type: 'string', - enum: ['device:add', 'device:remove', 'device:restore'], - }, - authorId: { type: 'string' }, - projectId: { type: 'string' }, - signature: { type: 'string' }, - authorIndex: { type: 'integer' }, - deviceIndex: { type: 'integer' }, - }, - }, - extraColumns: ` - created INTEGER, - timestamp INTEGER, - authorId TEXT, - type TEXT, - action TEXT, - identityId TEXT, - authorIndex INTEGER, - deviceIndex INTEGER, - signature TEXT - `, -} - -export const roles = { - name: 'roles', - blockPrefix: '2', - schema: { - properties: { - id: { type: 'string' }, - type: { type: 'string', pattern: '^roles$' }, - timestamp: { type: 'integer' }, - links: { - type: 'array', - uniqueItems: true, - items: { type: 'string' }, - }, - version: { type: 'string' }, - role: { - type: 'string', - enum: defaultRoles.map((r) => r.name), - }, - projectId: { type: 'string' }, - action: { - type: 'string', - enum: ['role:set'], - }, - signature: { type: 'string' }, - authorIndex: { type: 'integer' }, - deviceIndex: { type: 'integer' }, - }, - }, - extraColumns: ` - authorId TEXT, - type TEXT, - role TEXT, - action TEXT, - identityId TEXT, - projectId TEXT, - authorIndex INTEGER, - deviceIndex INTEGER, - created INTEGER, - timestamp INTEGER, - signature TEXT - `, -} diff --git a/src/authstore/index.js b/src/authstore/index.js deleted file mode 100644 index 3e8a42c3d..000000000 --- a/src/authstore/index.js +++ /dev/null @@ -1,1396 +0,0 @@ -// @ts-nocheck - -import sodium from 'sodium-universal' -import b4a from 'b4a' - -import { DataStore } from '../datastore/index.js' -import { idToKey, keyToId, parseVersion } from '../utils.js' - -import { coreOwnership, devices, roles, defaultRoles } from './authtypes.js' - -export class AuthStore { - #keyPair - #identityKeyPair - #corestore - #availableRoles - #datastore - #sqlite - - /** - * A class for managing capability statements that provide authorization in a project. - * @param {Object} options - * @param {PublicKey} [options.projectPublicKey] the public key of the project. Only needed if user is not the project owner. - * @param {KeyPair} options.keyPair The key pair used for the local writer hypercore. - * @param {IdentityKeyPair} options.identityKeyPair The key pair of the identity. - * @param {import('corestore')} options.corestore - * @param {import('../sqlite.js').Sqlite} options.sqlite An instance of the internal Sqlite class. - * @param {AvailableRoles} [options.roles] An array of role names and capabilities. - */ - constructor(options) { - this.#keyPair = options.keyPair - this.#identityKeyPair = options.identityKeyPair - this.#corestore = options.corestore - this.#availableRoles = options.roles || defaultRoles - this.#sqlite = options.sqlite - - if (!options.projectPublicKey) { - this.projectPublicKey = this.#keyPair.publicKey - } else { - this.projectPublicKey = options.projectPublicKey - } - - this.#datastore = new DataStore({ - corestore: this.#corestore, - sqlite: options.sqlite, - keyPair: this.#keyPair, - identityPublicKey: this.#identityKeyPair.publicKey, - dataTypes: [coreOwnership, devices, roles], - }) - } - - /** - * @returns {string} The id of the local writer hypercore. - */ - get id() { - return keyToId(this.key) - } - - /** - * @returns {PublicKey} The public key of the local writer hypercore. - */ - get key() { - return this.#keyPair.publicKey - } - - /** - * @returns {string} The id of the identity of this device. - */ - get authorId() { - return keyToId(this.#identityKeyPair.publicKey) - } - - /** - * @returns {string[]} The ids of the local writer hypercore and remote peer hypercores. - */ - get keys() { - return this.cores.map((core) => { - return core.key.toString('hex') - }) - } - - /** - * @returns {import('hypercore')[]} The local writer hypercore and remote peer hypercores. - */ - get cores() { - return [...this.#corestore.cores.values()] - } - - /** - * Wait for the internals of authstore to be ready. - * @returns {Promise} - */ - async ready() { - await this.#datastore.ready() - - this.coreOwnership = this.#datastore.getDataType('coreOwnership') - this.devices = this.#datastore.getDataType('devices') - this.roles = this.#datastore.getDataType('roles') - - await this.#init() - } - - /** - * Initialize the local writer hypercore. - * @returns {Promise} - */ - async #init() { - try { - await this.setCoreOwner(this.key) - } catch (err) { - if (err.message === 'Core already has an owner') { - await this.getCoreOwner({ coreId: this.id }) - } else { - throw err - } - } - } - - /** - * Create the statements for the project creator role. Only to be called once on initital project creation. - * @returns {Promise} - */ - async initProjectCreator() { - await this.setProjectCreator() - await this.addDevice({ identityId: this.authorId }) - } - - /** - * Create the core ownership statement for a given core key - * @param {PublicKey} coreKey - */ - async setCoreOwner(coreKey) { - if (!coreKey) { - coreKey = this.key - } - - const coreId = keyToId(coreKey) - const existing = await this.getCoreOwner({ coreId }) - - if (existing) { - throw new Error('Core already has an owner') - } - - const authorIndex = await this.getAuthorIndex(this.authorId) - const deviceIndex = await this.getDeviceIndex(this.authorId) - - if (authorIndex !== 0 || deviceIndex !== 0) { - throw new Error('Only the project creator can set the core owner') - } - - const timestamp = new Date().getTime() - const signature = this.#signCoreOwnerMessage({ - identityId: this.authorId, - authorId: this.authorId, - coreId, - projectId: this.id, - storeType: 'auth', - authorIndex, - deviceIndex, - timestamp, - }) - - return this.coreOwnership?.create({ - id: this.authorId, - authorId: this.authorId, - type: 'coreOwnership', - coreId, - storeType: 'auth', - action: 'core:owner', - created: timestamp, - signature, - authorIndex, - deviceIndex, - }) - } - - /** - * Get the owner of a core by its id. - * @param {Object} options - * @param {String} options.coreId - * @returns {Promise} - */ - async getCoreOwner({ coreId }) { - const statement = this.getCoreOwnershipStatementByCoreId(coreId) - if (!statement) { - return null - } - - await this.verifyCoreOwner(statement) - return statement - } - - /** - * Verify a core ownership statement. - * @param {CoreOwnershipStatement} statement - * @returns {Promise} - * @throws {Error} - */ - async verifyCoreOwner(statement) { - const { - id, - authorId, - coreId, - signature, - authorIndex, - deviceIndex, - timestamp, - } = statement - - const verified = this.#verifyCoreOwnerMessage({ - signature, - identityId: id, - message: { - identityId: id, - authorId, - coreId, - projectId: this.id, - storeType: 'auth', - authorIndex, - deviceIndex, - timestamp, - }, - }) - - if (!verified) { - throw new Error('Core ownership not verified') - } - } - - /** - * Get the statement for the creator of the project. - * @returns {Promise} - * @throws {Error} - */ - async getProjectCreator() { - const statement = this.getRoleStatementByRole('project-creator') - - if (!statement) { - return null - } - - await this.verifyProjectCreator(statement) - return statement - } - - /** - * Create the statement for the project creator role. - * @returns {Promise} - * @throws {Error} - */ - async setProjectCreator() { - const existing = await this.getProjectCreator() - if (existing) { - throw new Error('Project already has a creator') - } - - const authorIndex = await this.getAuthorIndex(this.authorId) - const deviceIndex = await this.getDeviceIndex(this.authorId) - - if (authorIndex !== 1 && deviceIndex !== 1) { - throw new Error( - 'Project creator record must be created by the first device' - ) - } - - const ownershipStatement = /** @type {CoreOwnershipStatement} */ ( - await this.getBlockByAuthorIndex({ - authorIndex: authorIndex - 1, - authorId: this.authorId, - }) - ) - - if (!ownershipStatement) { - throw new Error('No ownership statement found') - } - - this.verifyCoreOwner(ownershipStatement) - - const timestamp = new Date().getTime() - const signature = this.#signRoleMessage({ - identityId: this.authorId, - authorId: this.authorId, - role: 'project-creator', - projectId: this.id, - authorIndex, - deviceIndex, - timestamp, - links: [ownershipStatement.version], - }) - - return /** @type {Promise} */ ( - this.roles?.create({ - id: this.authorId, - type: 'roles', - role: 'project-creator', - projectId: this.id, - created: timestamp, - signature, - action: 'role:set', - authorIndex, - deviceIndex, - links: [ownershipStatement.version], - }) - ) - } - - /** - * Verify a project creator role statement. - * @param {RoleStatement} roleStatement - * @returns {Promise} - * @throws {Error} - */ - async verifyProjectCreator(roleStatement) { - const { - id, - authorId, - signature, - authorIndex, - deviceIndex, - timestamp, - links, - } = roleStatement - - if ( - !roleStatement || - !roleStatement.id || - roleStatement.type !== 'roles' || - roleStatement.role !== 'project-creator' || - !roleStatement.signature - ) { - throw new Error( - 'Project creator not verified: full role statement required' - ) - } - - if (!links.length) { - throw new Error( - 'Project creator not verified: link to core ownership statement not found' - ) - } - - const verified = this.#verifyRoleMessage({ - signature, - identityId: id, - message: { - identityId: id, - authorId, - projectId: this.id, - role: 'project-creator', - authorIndex, - deviceIndex, - timestamp, - links, - }, - }) - - if (!verified) { - throw new Error('Project creator not verified: signature not verified') - } - - const ownershipStatement = await this.getCoreOwnershipStatementByVersion( - links[0] - ) - if (!ownershipStatement) { - throw new Error( - 'Project creator not verified: core ownership statement not found' - ) - } - - return this.verifyCoreOwner(ownershipStatement) - } - - /** - * Get the statement for a device by its identityId. - * @param {Object} options - * @param {String} options.identityId - * @returns {Promise} - */ - async getDevice(options) { - const { identityId } = options - const statement = this.getDeviceStatementById(identityId) - - if (!statement) { - return null - } - - await this.verifyDevice(statement) - return statement - } - - /** - * Verify a device statement. - * @param {DeviceStatement} statement - */ - async verifyDevice(statement) { - const { - id, - signature, - authorId, - action, - authorIndex, - deviceIndex, - timestamp, - links, - } = statement - - const verified = this.#verifyDeviceMessage({ - signature, - identityId: authorId, - message: { - identityId: id, - authorId, - projectId: this.id, - action, - authorIndex, - deviceIndex, - timestamp, - links, - }, - }) - - if (!verified) { - throw new Error('Device not verified') - } - - await this.verifyLinks(links) - } - - /** - * Add a device to the project. - * @param {Object} options - * @param {String} options.identityId - * @returns {Promise} - */ - async addDevice(options) { - const { identityId } = options - - const existing = await this.getDevice({ identityId }) - if (existing) { - throw new Error('Device already exists') - } - - const authorIndex = await this.getAuthorIndex(this.authorId) - const deviceIndex = await this.getDeviceIndex(identityId) - const authorRole = await this.getRole({ identityId: this.authorId }) - - if (!authorRole) { - throw new Error('Author does not have a role') - } - - await this.verifyRole(authorRole) - - const role = this.#availableRoles.find( - (role) => role.name === authorRole.role - ) - - if (!role) { - throw new Error('Author role not found') - } - - if (!role.capabilities.includes('manage:devices')) { - throw new Error('Author does not have permission to add device') - } - - const link = await this.getBlockByDeviceIndex({ - deviceIndex: deviceIndex - 1, - identityId, - }) - - if (!link) { - throw new Error('Device does not have a previous link') - } - - let links = [link.version] - - await this.verifyLinks(links) - - const timestamp = new Date().getTime() - const signature = this.#signDeviceMessage({ - identityId, - authorId: authorRole.id, - projectId: this.id, - action: 'device:add', - authorIndex, - deviceIndex, - timestamp, - links, - }) - - return /** @type {Promise} */ ( - this.devices?.create({ - id: identityId, - authorId: this.authorId, - projectId: this.id, - type: 'devices', - action: 'device:add', - created: timestamp, - signature, - authorIndex, - deviceIndex, - links, - }) - ) - } - - /** - * Remove a device from the project. - * @param {Object} options - * @param {String} options.identityId - * @returns {Promise} - */ - async removeDevice(options) { - const { identityId } = options - - const existing = await this.getDevice({ identityId }) - - if (!existing) { - throw new Error('Device cannot be removed because it has not been added') - } - - if (existing.action === 'device:remove') { - throw new Error( - 'Device cannot be removed because it has already been removed' - ) - } - - const authorIndex = await this.getAuthorIndex(this.authorId) - const deviceIndex = await this.getDeviceIndex(identityId) - - const authorRole = await this.getRole({ identityId: this.authorId }) - - if (!authorRole) { - throw new Error('Author does not have a role') - } - - await this.verifyRole(authorRole) - - const role = this.#availableRoles.find( - (role) => role.name === authorRole.role - ) - - if (!role) { - throw new Error('Author role not found') - } - - if (!role.capabilities.includes('manage:devices')) { - throw new Error('Author does not have permission to add device') - } - - const projectCreator = await this.getProjectCreator() - - if (!projectCreator) { - throw new Error('Project creator not found') - } - - if (projectCreator.id === identityId) { - // TODO: we may want to allow this in the future - throw new Error('Project creator cannot be removed') - } - - const link = await this.getBlockByDeviceIndex({ - deviceIndex: deviceIndex - 1, - identityId, - }) - - if (!link) { - throw new Error('A previous statement is required to remove a device') - } - - let links = [link.version] - - await this.verifyLinks(links) - - const timestamp = new Date().getTime() - const signature = this.#signDeviceMessage({ - identityId, - authorId: authorRole.id, - projectId: this.id, - action: 'device:remove', - authorIndex, - deviceIndex, - timestamp, - links, - }) - - return /** @type {Promise} */ ( - this.devices?.create({ - id: identityId, - authorId: this.authorId, - projectId: this.id, - type: 'devices', - action: 'device:remove', - created: timestamp, - signature, - authorIndex, - deviceIndex, - links, - }) - ) - } - - /** - * Restore a device that has been removed from the project. - * @param {Object} options - * @param {String} options.identityId - * @returns {Promise} - */ - async restoreDevice(options) { - const { identityId } = options - - // order blocks by deviceIndex so we can check that they were created in the correct order - const existing = ( - await this.getBlocksByType({ dataType: 'devices', identityId }) - ).sort((a, b) => { - return b.deviceIndex - a.deviceIndex - }) - - const [mostRecent] = existing - - if (mostRecent.action !== 'device:remove') { - throw new Error( - 'Device cannot be restored because it has not been removed' - ) - } - - if (!existing.length) { - throw new Error( - 'Device cannot be restored because it has not been added or removed' - ) - } - - const authorIndex = await this.getAuthorIndex(this.authorId) - const deviceIndex = await this.getDeviceIndex(identityId) - - const authorRole = await this.getRole({ identityId: this.authorId }) - - if (!authorRole) { - throw new Error('Author does not have a role') - } - - await this.verifyRole(authorRole) - - const role = this.#availableRoles.find( - (role) => role.name === authorRole.role - ) - - if (!role) { - throw new Error('Author role not found') - } - - if (!role.capabilities.includes('manage:devices')) { - throw new Error('Author does not have permission to add device') - } - - const link = await this.getBlockByDeviceIndex({ - deviceIndex: deviceIndex - 1, - identityId, - }) - - if (!link) { - throw new Error('Previous statement for this device not found') - } - - const links = [link.version] - - await this.verifyLinks(links) - - const timestamp = new Date().getTime() - const signature = this.#signDeviceMessage({ - identityId, - authorId: authorRole.id, - projectId: this.id, - action: 'device:restore', - authorIndex, - deviceIndex, - timestamp, - links, - }) - - return /** @type {Promise} */ ( - this.devices?.create({ - id: identityId, - authorId: this.authorId, - projectId: this.id, - type: 'devices', - action: 'device:restore', - created: timestamp, - signature, - authorIndex, - deviceIndex, - links, - }) - ) - } - - /** - * Set the role of an identity in the project. - * @param {Object} options - * @param {String} options.role - * @param {String} options.identityId - * @returns {Promise} - * @throws {Error} - */ - async setRole(options) { - const { role, identityId } = options - - const existing = await this.getRole({ identityId }) - const authorRole = await this.getRole({ identityId: this.authorId }) - - if (existing && existing.role === role) { - throw new Error(`Role ${role} already set`) - } - - if (existing && existing.role === 'project-creator') { - // TODO: we may want to allow this in the future - throw new Error('Project creator role cannot be changed') - } - - if (!authorRole) { - throw new Error('Author does not have a role') - } - - const roleDetails = this.#availableRoles.find( - (item) => item.name === authorRole.role - ) - - if (!roleDetails) { - throw new Error('Author role not found') - } - - if (!roleDetails.capabilities.includes('manage:devices')) { - throw new Error('Author does not have permission to change roles') - } - - const authorIndex = await this.getAuthorIndex(this.authorId) - const deviceIndex = await this.getDeviceIndex(this.authorId) - - const ownershipStatement = /** @type {CoreOwnershipStatement} */ ( - await this.getBlockByAuthorIndex({ - authorIndex: authorIndex - 1, - authorId: this.authorId, - }) - ) - - const links = [] - if (ownershipStatement) { - links.push(ownershipStatement.version) - } - - this.verifyRole(authorRole) - - const timestamp = new Date().getTime() - const signature = this.#signRoleMessage({ - identityId, - authorId: authorRole.id, - role, - projectId: this.id, - authorIndex, - deviceIndex, - timestamp, - links, - }) - - return /** @type {Promise} */ ( - this.roles?.create({ - id: identityId, - type: 'roles', - role, - projectId: this.id, - created: timestamp, - signature, - action: 'role:set', - authorIndex, - deviceIndex, - links, - }) - ) - } - - /** - * Get the role of an identity in the project. - * @param {Object} options - * @param {String} options.identityId - * @returns {Promise} options - */ - async getRole(options) { - const { identityId } = options - const statement = this.getRoleStatementById(identityId) - - if (!statement) { - return null - } - - if (statement.role === 'project-creator') { - await this.verifyProjectCreator(statement) - } else { - await this.verifyRole(statement) - } - return statement - } - - /** - * @param {RoleStatement} options - * @returns {Promise} - * @throws {Error} - */ - async verifyRole(options) { - const { - id, - authorId, - role, - signature, - links, - authorIndex, - deviceIndex, - timestamp, - } = options - - if (role === 'project-creator') { - return this.verifyProjectCreator(options) - } - - const verified = this.#verifyRoleMessage({ - identityId: authorId, - signature, - message: { - identityId: id, - authorId, - projectId: this.id, - role, - authorIndex, - deviceIndex, - timestamp, - links, - }, - }) - - if (!verified) { - throw new Error('Role not verified: signature not verified') - } - - await this.verifyLinks(links) - } - - /** - * Verify the links of a statement. - * @param {String[]} links - * @returns {Promise} - * @throws {Error} - */ - async verifyLinks(links) { - for (const link of links) { - const block = await this.getBlockByVersion(link) - if (!block) { - throw new Error('Not verified: link not found') - } - if (block.type === 'devices') { - /** @ts-ignore: block.type check is sufficient */ - await this.verifyDevice(block) - } else if (block.type === 'roles') { - /** @ts-ignore: block.type check is sufficient */ - await this.verifyRole(block) - } else if (block.type === 'coreOwnership') { - /** @ts-ignore: block.type check is sufficient */ - await this.verifyCoreOwner(block) - } else { - throw new Error('Not verified: link to unknown data type') - } - } - } - - /** - * @param {Object} options - * @param {String} options.identityId - * @param {String} options.authorId - * @param {String} options.coreId - * @param {String} options.storeType - * @param {String} options.projectId - * @param {Number} options.authorIndex - * @param {Number} options.timestamp - * @param {Number} options.deviceIndex - * @returns {String} - */ - #signCoreOwnerMessage(options) { - const signature = this.#createCoreOwnerSignatureString(options) - return keyToId(this.signMessage(idToKey(signature))) - } - - /** - * @param {Object} options - * @param {String} options.identityId - * @param {String} options.authorId - * @param {String} options.role - * @param {String} options.projectId - * @param {Number} options.authorIndex - * @param {Number} options.deviceIndex - * @param {Number} options.timestamp - * @param {String[]} options.links - * @returns {String} - */ - #signRoleMessage(options) { - const signature = this.#createRoleSignatureString(options) - return keyToId(this.signMessage(idToKey(signature))) - } - - /** - * @param {Object} options - * @param {String} options.identityId - * @param {String} options.authorId - * @param {String} options.projectId - * @param {String} options.action - * @param {Number} options.authorIndex - * @param {Number} options.deviceIndex - * @param {Number} options.timestamp - * @param {String[]} options.links - * @returns {String} - */ - #signDeviceMessage(options) { - const signature = this.#createDeviceSignatureString(options) - return keyToId(this.signMessage(idToKey(signature))) - } - - /** - * @param {Object} options - * @param {Object} options.message - * @param {String} options.message.identityId - * @param {String} options.message.authorId - * @param {String} options.message.coreId - * @param {String} options.message.projectId - * @param {String} options.message.storeType - * @param {Number} options.message.authorIndex - * @param {Number} options.message.deviceIndex - * @param {Number} options.message.timestamp - * @param {String} options.signature - * @param {String} options.identityId - * @returns {Boolean} - */ - #verifyCoreOwnerMessage(options) { - const messageString = this.#createCoreOwnerSignatureString(options.message) - return this.verifyMessage( - idToKey(messageString), - idToKey(options.signature), - idToKey(options.identityId) - ) - } - - /** - * @param {Object} options - * @param {Object} options.message - * @param {String} options.message.identityId - * @param {String} options.message.authorId - * @param {String} options.message.projectId - * @param {String} options.message.role - * @param {Number} options.message.authorIndex - * @param {Number} options.message.deviceIndex - * @param {Number} options.message.timestamp - * @param {String[]} options.message.links - * @param {String} options.signature - * @param {String} options.identityId - * @returns {Boolean} - */ - #verifyRoleMessage(options) { - const messageString = this.#createRoleSignatureString(options.message) - return this.verifyMessage( - idToKey(messageString), - idToKey(options.signature), - idToKey(options.identityId) - ) - } - - /** - * @param {Object} options - * @param {Object} options.message - * @param {String} options.message.identityId - * @param {String} options.message.authorId - * @param {String} options.message.projectId - * @param {String} options.message.action - * @param {Number} options.message.authorIndex - * @param {Number} options.message.deviceIndex - * @param {Number} options.message.timestamp - * @param {String[]} options.message.links - * @param {String} options.signature - * @param {String} options.identityId - * @returns {Boolean} - */ - #verifyDeviceMessage(options) { - const messageString = this.#createDeviceSignatureString(options.message) - return this.verifyMessage( - idToKey(messageString), - idToKey(options.signature), - idToKey(options.identityId) - ) - } - - /** - * @param {Object} options - * @param {String} options.identityId - * @param {String} options.authorId - * @param {String} options.coreId - * @param {String} options.projectId - * @param {String} options.storeType - * @param {Number} options.authorIndex - * @param {Number} options.deviceIndex - * @param {Number} options.timestamp - * @returns {String} - */ - #createCoreOwnerSignatureString(options) { - return this.#createSignatureString([ - 'core_owners', - options.identityId, - options.authorId, - options.coreId, - options.authorIndex, - options.deviceIndex, - options.timestamp, - ]) - } - - /** - * @param {Object} options - * @param {String} options.identityId - * @param {String} options.authorId - * @param {String} options.projectId - * @param {String} options.role - * @param {Number} options.authorIndex - * @param {Number} options.deviceIndex - * @param {Number} options.timestamp - * @param {String[]} options.links - * @returns {String} - */ - #createRoleSignatureString(options) { - return this.#createSignatureString([ - 'roles', - options.identityId, - options.authorId, - options.projectId, - options.role, - options.authorIndex, - options.deviceIndex, - options.timestamp, - options.links.join(','), - ]) - } - - /** - * @param {Object} options - * @param {String} options.identityId - * @param {String} options.authorId - * @param {String} options.action - * @param {Number} options.authorIndex - * @param {Number} options.deviceIndex - * @param {Number} options.timestamp - * @param {String[]} options.links - * @returns {String} - */ - #createDeviceSignatureString(options) { - return this.#createSignatureString([ - 'devices', - options.identityId, - options.authorId, - options.action, - options.authorIndex, - options.deviceIndex, - options.timestamp, - options.links.join(','), - ]) - } - - /** - * - * @param {Array} fragments - * @returns {String} - */ - #createSignatureString(fragments) { - return fragments.join(':') - } - - /** - * Sign a message with the identity key pair. - * @param {Buffer} message - * @returns {Buffer} signature - */ - signMessage(message) { - const signature = b4a.alloc(sodium.crypto_sign_BYTES) - sodium.crypto_sign_detached( - signature, - message, - this.#identityKeyPair.secretKey - ) - return signature - } - - /** - * Verify that a message was signed by the given identity. - * @param {Buffer} message - * @param {Buffer} signature - * @param {Buffer} identityPublicKey - * @returns {Boolean} - */ - verifyMessage(message, signature, identityPublicKey) { - return sodium.crypto_sign_verify_detached( - signature, - message, - identityPublicKey - ) - } - - /** - * Get a core ownership statement by version. - * @param {String} version - * @returns {Promise} - */ - getCoreOwnershipStatementByVersion(version) { - return /** @type {Promise} */ ( - this.getBlockByVersion(version) - ) - } - - /** - * Get a role statement by version. - * @param {String} version - * @returns {Promise}} - */ - getRoleStatementByVersion(version) { - return /** @type {Promise} */ ( - this.getBlockByVersion(version) - ) - } - - /** - * Get a device statement by version. - * @param {String} version - * @returns {Promise} - */ - getDeviceStatementByVersion(version) { - return /** @type {Promise} */ ( - this.getBlockByVersion(version) - ) - } - - /** - * Get a core ownership statement by coreId. - * @param {String} coreId - * @returns {CoreOwnershipStatement|null} - */ - getCoreOwnershipStatementByCoreId(coreId) { - return /** @type {CoreOwnershipStatement} */ ( - this.#sqlite.get(`SELECT * FROM coreOwnership WHERE coreId = '${coreId}'`) - ) - } - - /** - * Get a core ownership statement by id. - * @param {String} id - * @returns {CoreOwnershipStatement|null} - */ - getCoreOwnershipStatementById(id) { - return /** @type {CoreOwnershipStatement} */ ( - this.#sqlite.get(`SELECT * FROM coreOwnership WHERE id = '${id}'`) - ) - } - - /** - * Get a role statement by id. - * @param {String} id - * @returns {RoleStatement|null} - */ - getRoleStatementById(id) { - return /** @type {RoleStatement} */ ( - this.#sqlite.get(`SELECT * FROM roles WHERE id = '${id}'`) - ) - } - - /** - * Get a device statement by id. - * @param {String} id - * @returns {DeviceStatement|null} - */ - getDeviceStatementById(id) { - return /** @type {DeviceStatement} */ ( - this.#sqlite.get(`SELECT * FROM devices WHERE id = '${id}'`) - ) - } - - /** - * Get a role statement by role. - * @param {String} role - * @returns {RoleStatement|null} - */ - getRoleStatementByRole(role) { - return /** @type {RoleStatement} */ ( - this.#sqlite.get(`SELECT * FROM roles WHERE role = '${role}'`) - ) - } - - /** - * Get the current length of statements for a device. - * @param {String} identityId - * @returns {Promise} - */ - async getDeviceIndex(identityId) { - const blocks = await this.getBlocksByIdentityId(identityId) - return blocks.length - } - - /** - * Get a block by its version. - * @param {String} version - * @returns {Promise} - */ - async getBlockByVersion(version) { - const { coreId, blockIndex } = parseVersion(version) - const core = await this.getCore(coreId) - const block = await core.get(blockIndex) - if (!block) { - return null - } - const dataType = this.#datastore.getDataTypeForBlock(block) - return /** @type {CoreOwnershipStatement|RoleStatement|DeviceStatement} */ ( - dataType.decode(block) - ) - } - - /** - * Get a block by the device index. - * @param {Object} options - * @param {String} options.identityId - * @param {Number} options.deviceIndex - * @returns {Promise} - */ - async getBlockByDeviceIndex({ identityId, deviceIndex }) { - const blocks = await this.getBlocksByIdentityId(identityId) - return blocks.find((block) => { - return block.deviceIndex === deviceIndex - }) - } - - /** - * Get a block by the author index. - * @param {Object} options - * @param {String} options.authorId - * @param {Number} options.authorIndex - * @returns {Promise} - */ - async getBlockByAuthorIndex({ authorId, authorIndex }) { - const blocks = await this.getBlocksByAuthorId(authorId) - return blocks.find((block) => { - return block.authorIndex === authorIndex - }) - } - - /** - * Get the current length of the author's chain of links - * @param {String} identityId - * @returns {Promise} - */ - async getAuthorIndex(identityId) { - const blocks = await this.getBlocksByAuthorId(identityId) - return blocks.length - } - - /** - * Get all blocks for a given author - * @param {String} authorId - * @returns {Promise>} - */ - async getBlocksByAuthorId(authorId) { - let blocks = [] - - for await (const block of this.getBlocks()) { - if (block.authorId === authorId) { - blocks.push(block) - } - } - - return blocks - } - - /** - * Get all blocks for a given identity. - * @param {String} identityId - * @returns {Promise>} - */ - async getBlocksByIdentityId(identityId) { - let blocks = [] - - for await (const block of this.getBlocks()) { - if (block.id === identityId) { - blocks.push(block) - } - } - - return blocks - } - - /** - * Get all blocks for a given data type. - * @param {Object} options - * @param {string} options.dataType - * @param {string} options.identityId - * @returns {Promise>} - */ - async getBlocksByType({ dataType, identityId }) { - let blocks = [] - - for await (const block of this.getBlocks()) { - if (block.type === dataType) { - if (identityId) { - if (block.id === identityId) { - blocks.push(block) - } - } else { - blocks.push(block) - } - } - } - - return blocks - } - - /** - * Get all blocks. - * @returns {AsyncGenerator} - * @yields {CoreOwnershipStatement|RoleStatement|DeviceStatement} - */ - async *getBlocks() { - for (const core of this.cores) { - await core.ready() - if (core.length) { - for await (const buf of core.createReadStream()) { - const dataType = this.#datastore.getDataTypeForBlock(buf) - if (dataType) { - const data = dataType.decode(buf) - yield /** @type {CoreOwnershipStatement|RoleStatement|DeviceStatement} */ ( - data - ) - } - } - } - } - } - - /** - * Wait for indexing to complete. - * @returns {Promise} - */ - async indexing() { - return this.#datastore.indexing() - } - - /** - * Get a hypercore. - * @param {PublicKey|String} coreKey - * @param {Object} [options] - * @returns {Promise} - */ - async getCore(coreKey, options) { - return this.#datastore.getCore(coreKey, options) - } - - /** - * Query the sqlite index of documents. - * @param {string} sql - * @param {any[]} [params] - * @returns {Object[]} - */ - query(sql, params) { - return this.#sqlite.query(sql, params) - } - - /** - * Get a single row from the sqlite index of documents. - * @param {string} sql - * @param {any[]} [params] - * @returns {Object} - */ - get(sql, params) { - return this.#sqlite.get(sql, params) - } - - /** - * Replicate all hypercores. - * @param {Boolean} isInitiator - a boolean indicating whether this device is initiating or responding to a connection - * @param {Object} options - Options object passed to `corestore.replicate` - */ - replicate(isInitiator, options) { - return this.#datastore.replicate(isInitiator, options) - } - - /** - * Close the internals of authstore. - * @returns {Promise} - */ - async close() { - await this.#datastore.close() - } -} diff --git a/src/sqlite.js b/src/sqlite.js deleted file mode 100644 index d4fe1024d..000000000 --- a/src/sqlite.js +++ /dev/null @@ -1,87 +0,0 @@ -// @ts-nocheck -import BetterSqlite from 'better-sqlite3' - -export class Sqlite { - /** @type {string} */ - #filepath - - /** - * Create a Sqlite client. This class is a wrapper around [better-sqlite3](https://npmjs.com/better-sqlite3) - * @param {string} filepath - * @param {import('better-sqlite3').Options} options passed to [better-sqlite3 client](https://npmjs.com/better-sqlite3) - */ - constructor(filepath, options = {}) { - this.#filepath = filepath - - /** @type {import('better-sqlite3').Database} */ - this.db = BetterSqlite(this.#filepath, options) - this.db.pragma('journal_mode = WAL') - } - - /** - * Query the database - * @param {string} sql - * @param {any[]} [params] - * @returns {Doc[]} - */ - query(sql, params) { - const statement = this.db.prepare(sql) - const rows = params ? statement.all(...params) : statement.all() - - return rows.map((row) => { - for (const [key, value] of Object.entries(row)) { - if (['links', 'forks'].includes(key)) { - row[key] = JSON.parse(value) - } - } - - return row - }) - } - - /** - * Get a single record from the database - * @param {string} sql - * @param {any[]} [params] - * @returns {Doc} - */ - get(sql, params) { - const statement = this.db.prepare(sql) - const row = params ? statement.get(...params) : statement.get() - - if (!row) { - return row - } - - if (row['links']) { - row['links'] = JSON.parse(row['links']) - } - - if (row['forks']) { - row['forks'] = JSON.parse(row['forks']) - } - - return row - } - - /** - * Run a statement against the database - * @param {string} sql - * @param {any[]} [params] - * @returns {import('better-sqlite3').RunResult} - */ - run(sql, params) { - const statement = this.db.prepare(sql) - return params ? statement.run(...params) : statement.run() - } - - /** - * Close the database connection - * @returns {void} - */ - close() { - if (this.db.open) { - this.db.close() - } - } -} diff --git a/tests/authstore.js b/tests/authstore.js deleted file mode 100644 index 2c53c16c9..000000000 --- a/tests/authstore.js +++ /dev/null @@ -1,132 +0,0 @@ -import test from 'brittle' -import { createAuthStores } from './helpers/authstore.js' -import { waitForIndexing } from './helpers/index.js' - -// Skipping tests until migrated to new DataStore & DataType API - -test.skip('authstore - core ownership, project creator', async (t) => { - t.plan(7) - - const [peer1, peer2] = await createAuthStores(2) - await waitForIndexing([peer1.authstore, peer2.authstore]) - - const peer1Owner = await peer1.authstore.getCoreOwner({ - coreId: peer1.authstore.id, - }) - const peer2Owner = await peer2.authstore.getCoreOwner({ - coreId: peer2.authstore.id, - }) - - t.is(peer1Owner.id, peer1.identityId, 'peer1 owns their core') - t.is(peer2Owner.id, peer2.identityId, 'peer2 owns their core') - - const peer1OwnerRemote = await peer2.authstore.getCoreOwner({ - coreId: peer1.authstore.id, - }) - const peer2OwnerRemote = await peer1.authstore.getCoreOwner({ - coreId: peer2.authstore.id, - }) - - t.is(peer1OwnerRemote.id, peer1.identityId, 'peer1 owns their core') - t.is(peer2OwnerRemote.id, peer2.identityId, 'peer2 owns their core') - - const peer2NotOwner = peer2.authstore.verifyCoreOwner({ - id: peer2.identityId, - coreId: peer1.authstore.id, - signature: peer1Owner.signature, - }) - - await t.exception(peer2NotOwner, 'peer2 cannot verify as owner of peer1 core') - - const projectCreator = await peer1.authstore.getProjectCreator() - t.is(projectCreator.id, peer1.identityId, 'peer1 is project creator') - - const onlyOneProjectCreator = peer2.authstore.setProjectCreator({ - projectId: peer2.authstore.projectId, - }) - - await t.exception( - onlyOneProjectCreator, - 'peer2 cannot set themselves as project creator' - ) -}) - -test.skip('authstore - device add, remove, restore, set role', async (t) => { - t.plan(10) - - const [peer1, peer2] = await createAuthStores(2) - await waitForIndexing([peer1.authstore, peer2.authstore]) - - const peer2Device = await peer1.authstore.addDevice({ - identityId: peer2.identityId, - }) - - t.ok(peer2Device) - await waitForIndexing([peer1.authstore, peer2.authstore]) - - const peer2DeviceRemote = await peer2.authstore.getDevice({ - identityId: peer2.identityId, - }) - - t.ok(peer2DeviceRemote, 'peer2 device added') - - const peer1NotRemoved = peer2.authstore.removeDevice({ - identityId: peer1.identityId, - }) - - await t.exception(peer1NotRemoved, 'project creator cannot be removed') - - const peer2Removed = await peer1.authstore.removeDevice({ - identityId: peer2.identityId, - }) - - t.is(peer2Removed.action, 'device:remove', 'peer2 device removed') - - const peer2NotRemovedTwice = peer1.authstore.removeDevice({ - identityId: peer2.identityId, - }) - - await t.exception( - peer2NotRemovedTwice, - 'peer2 device cannot be removed twice' - ) - - const peer2Restored = await peer1.authstore.restoreDevice({ - identityId: peer2.identityId, - }) - - t.is(peer2Restored.action, 'device:restore', 'peer2 device restored') - - const peer2NotRestoredTwice = peer1.authstore.restoreDevice({ - identityId: peer2.identityId, - }) - - await t.exception( - peer2NotRestoredTwice, - 'peer2 device cannot be restored twice' - ) - - const peer2Contributor = await peer1.authstore.setRole({ - role: 'contributor', - identityId: peer2.identityId, - }) - - t.is(peer2Contributor.role, 'contributor', 'peer2 role set to member') - - const noChangingProjectCreator = peer2.authstore.setRole({ - role: 'member', - identityId: peer1.identityId, - }) - - await t.exception( - noChangingProjectCreator, - 'project creator cannot be changed' - ) - - const peer2NonMember = await peer1.authstore.setRole({ - role: 'non-member', - identityId: peer2.identityId, - }) - - t.is(peer2NonMember.role, 'non-member', 'peer2 role set to nonmember') -}) diff --git a/tests/helpers/authstore.js b/tests/helpers/authstore.js deleted file mode 100644 index 2794d2675..000000000 --- a/tests/helpers/authstore.js +++ /dev/null @@ -1,132 +0,0 @@ -import { randomBytes } from 'crypto' -import Corestore from 'corestore' -import ram from 'random-access-memory' - -import { Sqlite } from '../../src/sqlite.js' -import { AuthStore } from '../../src/authstore/index.js' -import { addCores, replicate, createIdentityKeys } from './index.js' -import { keyToId } from '../../src/utils.js' - -export async function createAuthStore({ - corestore, - keyPair, - name, - projectPublicKey, -} = {}) { - const { rootKey, identityKeyPair, keyManager } = createIdentityKeys() - const identityId = keyToId(identityKeyPair.publicKey) - - if (!keyPair) { - keyPair = keyManager.getHypercoreKeypair(identityId, randomBytes(32)) - } - - if (!corestore) { - corestore = new Corestore(ram, { - primaryKey: identityKeyPair.publicKey, - }) - } - - if (!projectPublicKey) { - projectPublicKey = keyManager.getHypercoreKeypair( - 'project', - randomBytes(32) - ).publicKey - } - - const sqlite = new Sqlite(':memory:') - const authstore = new AuthStore({ - name, - corestore, - sqlite, - identityKeyPair, - keyPair, - keyManager, - projectPublicKey, - }) - - await authstore.ready() - - return { - authstore, - corestore, - identityKeyPair, - identityId, - keyPair, - keyManager, - rootKey, - sqlite, - } -} - -export async function createAuthStores(count, options) { - const projectPublicKey = randomBytes(32) - - const peers = [] - for (let i = 0; i < count; i++) { - const peer = await createAuthStore({ ...options, projectPublicKey }) - peers.push(peer) - - if (i === 0) { - await peer.authstore.initProjectCreator() - } - } - - await addCores(peers) - replicate( - peers.map((peer) => { - return { - id: peer.identityId, - core: peer.authstore, - } - }) - ) - return peers -} - -export async function runAuthStoreScenario(scenario, options = {}) { - const { t } = options - - const peers = {} - for (const peerName of scenario.peers) { - peers[peerName] = await createAuthStore(options) - if (peerName === 'project-creator') { - await peers[peerName].authstore.createCapability({ - identityPublicKey: - peers[peerName].identityKeyPair.publicKey.toString('hex'), - capability: 'project-creator', - }) - } - } - - const results = [] - for (const step of scenario.steps) { - const peer = peers[step.peer] - const action = actions[step.action] - const data = getScenarioData(peers, step.data) - const previousResult = results[results.length - 1] - const result = await action(peer, data, previousResult) - await step.check(t, peer, data, result, previousResult) - results.push(result) - } - - return Object.values(peers) -} - -function getScenarioData(peers, data) { - const peer = peers[data.identityPublicKey] - return { - ...data, - identityPublicKey: peer.authstore.key.toString('hex'), - } -} - -const actions = { - createCapability: async (peer, data) => { - return peer.authstore.createCapability(data) - }, - updateCapability: async (peer, data, previousResult) => { - return peer.authstore.updateCapability( - Object.assign({}, previousResult, data) - ) - }, -} diff --git a/tests/helpers/index.js b/tests/helpers/index.js index 6629c19a9..088b166f9 100644 --- a/tests/helpers/index.js +++ b/tests/helpers/index.js @@ -55,17 +55,6 @@ export function replicate(peers) { } } -export async function addCores(peers) { - for (const peer1 of peers) { - for (const peer2 of peers) { - if (peer1 === peer2) continue - for (const key of peer2.authstore.keys) { - await peer1.authstore.getCore(key) - } - } - } -} - export async function waitForIndexing(stores) { await Promise.all( stores.map((store) => { diff --git a/tests/scenarios/authstore.js b/tests/scenarios/authstore.js deleted file mode 100644 index b326b687e..000000000 --- a/tests/scenarios/authstore.js +++ /dev/null @@ -1,158 +0,0 @@ -export const scenarios = [ - { - name: 'make peer2 a member, then update to coordinator', - peers: ['peer1', 'peer2'], - steps: [ - { - action: 'createCapability', - peer: 'peer1', - data: { - type: 'capabilities', - capability: 'project-creator', - identityPublicKey: 'peer1', - }, - check: async (t, peer, data) => { - const capabilities = await peer.authstore.getCapabilities( - data.identityPublicKey - ) - t.is( - capabilities.length, - 1, - 'peer1 should have 1 capabilities statements' - ) - t.is( - capabilities[0].capability, - 'project-creator', - 'peer1 should have creator capability' - ) - }, - }, - { - action: 'createCapability', - peer: 'peer1', - data: { - type: 'capabilities', - capability: 'member', - identityPublicKey: 'peer2', - }, - check: async (t, peer, data) => { - const capabilities = await peer.authstore.getCapabilities( - data.identityPublicKey - ) - t.is( - capabilities.length, - 1, - 'peer2 should have 1 capabilities statements' - ) - t.is( - capabilities[0].capability, - 'member', - 'peer2 should have member capability' - ) - }, - }, - { - action: 'updateCapability', - peer: 'peer1', - data: { - type: 'capabilities', - capability: 'coordinator', - identityPublicKey: 'peer2', - }, - check: async (t, peer, data) => { - const capabilities = await peer.authstore.getCapabilities( - data.identityPublicKey - ) - t.is( - capabilities.length, - 1, - 'peer2 should have 1 capabilities statements' - ) - t.is( - capabilities[0].capability, - 'coordinator', - 'peer2 should have coordinator capability' - ) - }, - }, - ], - }, - { - name: 'creator makes peer2 a coordinator, peer2 makes peer3 a coordinator, creator makes peer3 a member', - peers: ['project-creator', 'peer2', 'peer3'], - steps: [ - { - action: 'createCapability', - peer: 'project-creator', - data: { - type: 'capabilities', - capability: 'coordinator', - identityPublicKey: 'peer2', - }, - check: async (t, peer, data) => { - const capabilities = await peer.authstore.getCapabilities( - data.identityPublicKey - ) - t.is( - capabilities.length, - 1, - 'peer2 should have 1 capabilities statements' - ) - t.is( - capabilities[0].capability, - 'coordinator', - 'peer2 should have coordinator capability' - ) - }, - }, - { - action: 'createCapability', - peer: 'peer2', - data: { - type: 'capabilities', - capability: 'coordinator', - identityPublicKey: 'peer3', - }, - check: async (t, peer, data) => { - const capabilities = await peer.authstore.getCapabilities( - data.identityPublicKey - ) - t.is( - capabilities.length, - 1, - 'peer3 should have 1 capabilities statements' - ) - t.is( - capabilities[0].capability, - 'coordinator', - 'peer3 should have coordinator capability' - ) - }, - }, - { - action: 'createCapability', - peer: 'project-creator', - data: { - type: 'capabilities', - capability: 'member', - identityPublicKey: 'peer3', - }, - check: async (t, peer, data) => { - const capabilities = await peer.authstore.getCapabilities( - data.identityPublicKey - ) - t.is( - capabilities.length, - 1, - 'peer3 should have 1 capabilities statements' - ) - t.is( - capabilities[0].capability, - 'member', - 'peer3 should have member capability' - ) - }, - }, - ], - }, -]