diff --git a/src/acl/ACL.ts b/src/acl/ACL.ts index beb4fc4fa..cf4d4d543 100644 --- a/src/acl/ACL.ts +++ b/src/acl/ACL.ts @@ -144,11 +144,7 @@ class ACL { if (permId in permIds) { nodePerm = permIds[permId]; // Get the first existing perm object - let perm: Permission; - for (const nodeId_ in nodePerm) { - perm = nodePerm[nodeId_]; - break; - } + const perm = Object.values(nodePerm)[0]; // All perm objects are shared nodePerm[nodeId] = perm!; } else { @@ -614,8 +610,8 @@ class ACL { [...this.aclNodesDbPath, nodeId.toBuffer()], true, ); - // Skip if the nodeId doesn't exist - // this means that it previously been removed + // Skip if the nodeId doesn't exist. This means that it has previously + // been removed. if (permId == null) { continue; } diff --git a/src/client/handlers/AgentStatus.ts b/src/client/handlers/AgentStatus.ts index 249af6e58..4ee6017a2 100644 --- a/src/client/handlers/AgentStatus.ts +++ b/src/client/handlers/AgentStatus.ts @@ -5,8 +5,8 @@ import type { } from '../types'; import type PolykeyAgent from '../../PolykeyAgent'; import { UnaryHandler } from '@matrixai/rpc'; -import * as nodesUtils from '../../nodes/utils'; import config from '../../config'; +import * as nodesUtils from '../../nodes/utils'; class AgentStatus extends UnaryHandler< { diff --git a/src/client/handlers/AuditEventsGet.ts b/src/client/handlers/AuditEventsGet.ts index 8abeefcf5..308bb05c7 100644 --- a/src/client/handlers/AuditEventsGet.ts +++ b/src/client/handlers/AuditEventsGet.ts @@ -1,4 +1,5 @@ import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult } from '../types'; import type { AuditEvent, @@ -42,8 +43,8 @@ class AuditEventsGet extends ServerHandler< }> & { paths: Array; }, - _cancel, - _meta, + _cancel: (reason?: any) => void, + _meta: Record, ctx: ContextTimed, ): AsyncGenerator> { const { audit }: { audit: Audit } = this.container; diff --git a/src/client/handlers/GestaltsActionsGetByIdentity.ts b/src/client/handlers/GestaltsActionsGetByIdentity.ts index 5b5544e4d..33d118e04 100644 --- a/src/client/handlers/GestaltsActionsGetByIdentity.ts +++ b/src/client/handlers/GestaltsActionsGetByIdentity.ts @@ -8,9 +8,9 @@ import type GestaltGraph from '../../gestalts/GestaltGraph'; import type { GestaltAction } from '../../gestalts/types'; import type { IdentityId, ProviderId } from '../../ids'; import { UnaryHandler } from '@matrixai/rpc'; -import * as ids from '../../ids'; import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as ids from '../../ids'; class GestaltsActionsGetByIdentity extends UnaryHandler< { diff --git a/src/client/handlers/IdentitiesAuthenticate.ts b/src/client/handlers/IdentitiesAuthenticate.ts index 6f0c3cd61..f5ec068d8 100644 --- a/src/client/handlers/IdentitiesAuthenticate.ts +++ b/src/client/handlers/IdentitiesAuthenticate.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { AuthProcessMessage, ClientRPCRequestParams, @@ -21,11 +23,11 @@ class IdentitiesAuthenticate extends ServerHandler< public timeout = 120000; // 2 Minutes public handle = async function* ( input: ClientRPCRequestParams<{ providerId: string }>, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { identitiesManager }: { identitiesManager: IdentitiesManager } = this.container; const { @@ -52,7 +54,7 @@ class IdentitiesAuthenticate extends ServerHandler< if (authFlowResult.done) { never('authFlow signalled done too soon'); } - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { request: { url: authFlowResult.value.url, @@ -63,7 +65,7 @@ class IdentitiesAuthenticate extends ServerHandler< if (!authFlowResult.done) { never('authFlow did not signal done when expected'); } - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { response: { identityId: authFlowResult.value, diff --git a/src/client/handlers/IdentitiesAuthenticatedGet.ts b/src/client/handlers/IdentitiesAuthenticatedGet.ts index dd6de73e8..11f43ffe7 100644 --- a/src/client/handlers/IdentitiesAuthenticatedGet.ts +++ b/src/client/handlers/IdentitiesAuthenticatedGet.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -19,11 +21,11 @@ class IdentitiesAuthenticatedGet extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams<{ providerId?: string }>, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { identitiesManager }: { identitiesManager: IdentitiesManager } = this.container; let providerId: ProviderId | undefined; @@ -46,12 +48,10 @@ class IdentitiesAuthenticatedGet extends ServerHandler< : [providerId]; for (const providerId of providerIds) { const provider = identitiesManager.getProvider(providerId); - if (provider == null) { - continue; - } + if (provider == null) continue; const identities = await provider.getAuthIdentityIds(); for (const identityId of identities) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { providerId: provider.id, identityId: identityId, diff --git a/src/client/handlers/IdentitiesInfoConnectedGet.ts b/src/client/handlers/IdentitiesInfoConnectedGet.ts index 09178cded..5dbe759c6 100644 --- a/src/client/handlers/IdentitiesInfoConnectedGet.ts +++ b/src/client/handlers/IdentitiesInfoConnectedGet.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -22,11 +24,11 @@ class IdentitiesInfoConnectedGet extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { identitiesManager }: { identitiesManager: IdentitiesManager } = this.container; const { @@ -71,6 +73,7 @@ class IdentitiesInfoConnectedGet extends ServerHandler< } const identities: Array> = []; for (const providerId of providerIds) { + ctx.signal.throwIfAborted(); // Get provider from id const provider = identitiesManager.getProvider(providerId); if (provider === undefined) { @@ -94,7 +97,7 @@ class IdentitiesInfoConnectedGet extends ServerHandler< let count = 0; for (const gen of identities) { for await (const identity of gen) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); if (input.limit !== undefined && count >= input.limit) break; yield { providerId: identity.providerId, diff --git a/src/client/handlers/IdentitiesInfoGet.ts b/src/client/handlers/IdentitiesInfoGet.ts index a85ff6d21..80ded3ee8 100644 --- a/src/client/handlers/IdentitiesInfoGet.ts +++ b/src/client/handlers/IdentitiesInfoGet.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -8,11 +10,11 @@ import type { IdentityId, ProviderId } from '../../ids'; import type IdentitiesManager from '../../identities/IdentitiesManager'; import type { IdentityData } from '../../identities/types'; import { ServerHandler } from '@matrixai/rpc'; +import { validateSync } from '../../validation'; +import { matchSync } from '../../utils'; import * as ids from '../../ids'; import * as identitiesErrors from '../../identities/errors'; import * as identitiesUtils from '../../identities/utils'; -import { validateSync } from '../../validation'; -import { matchSync } from '../../utils'; class IdentitiesInfoGet extends ServerHandler< { @@ -23,9 +25,9 @@ class IdentitiesInfoGet extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { if (ctx.signal.aborted) throw ctx.signal.reason; const { identitiesManager }: { identitiesManager: IdentitiesManager } = @@ -86,7 +88,7 @@ class IdentitiesInfoGet extends ServerHandler< input.limit = identities.length; } for (let i = 0; i < input.limit; i++) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const identity = identities[i]; if (identity !== undefined) { if (identitiesUtils.matchIdentityData(identity, searchTerms)) { diff --git a/src/client/handlers/KeysCertsChainGet.ts b/src/client/handlers/KeysCertsChainGet.ts index c3466c550..ef3ca5ff5 100644 --- a/src/client/handlers/KeysCertsChainGet.ts +++ b/src/client/handlers/KeysCertsChainGet.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { CertMessage, ClientRPCRequestParams, @@ -14,17 +16,15 @@ class KeysCertsChainGet extends ServerHandler< ClientRPCResponseResult > { public handle = async function* ( - _input, - _cancel, - _meta, - ctx, + _input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { const { certManager }: { certManager: CertManager } = this.container; for (const certPEM of await certManager.getCertPEMsChain()) { - if (ctx.signal.aborted) throw ctx.signal.reason; - yield { - cert: certPEM, - }; + ctx.signal.throwIfAborted(); + yield { cert: certPEM }; } }; } diff --git a/src/client/handlers/KeysCertsGet.ts b/src/client/handlers/KeysCertsGet.ts index 7684af221..f699f4527 100644 --- a/src/client/handlers/KeysCertsGet.ts +++ b/src/client/handlers/KeysCertsGet.ts @@ -16,9 +16,7 @@ class KeysCertsGet extends UnaryHandler< public handle = async (): Promise> => { const { certManager }: { certManager: CertManager } = this.container; const cert = await certManager.getCurrentCertPEM(); - return { - cert, - }; + return { cert }; }; } diff --git a/src/client/handlers/KeysKeyPairRenew.ts b/src/client/handlers/KeysKeyPairRenew.ts index 29e3c298b..3e22f9602 100644 --- a/src/client/handlers/KeysKeyPairRenew.ts +++ b/src/client/handlers/KeysKeyPairRenew.ts @@ -17,11 +17,9 @@ class KeysKeyPairRenew extends UnaryHandler< input: ClientRPCRequestParams, ): Promise => { const { certManager }: { certManager: CertManager } = this.container; - // Other domains will be updated accordingly via the `EventBus` so we // only need to modify the KeyManager await certManager.renewCertWithNewKeyPair(input.password); - return {}; }; } diff --git a/src/client/handlers/KeysVerify.ts b/src/client/handlers/KeysVerify.ts index 2f6c0b171..0609f897f 100644 --- a/src/client/handlers/KeysVerify.ts +++ b/src/client/handlers/KeysVerify.ts @@ -35,7 +35,7 @@ class KeysVerify extends UnaryHandler< Buffer.from(input.data, 'binary'), Buffer.from(input.signature, 'binary') as Signature, ); - return { type: 'success', success: success }; + return { success: success }; }; } diff --git a/src/client/handlers/NodesAdd.ts b/src/client/handlers/NodesAdd.ts index 68d4985da..84991763b 100644 --- a/src/client/handlers/NodesAdd.ts +++ b/src/client/handlers/NodesAdd.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -8,11 +10,11 @@ import type { NodeId } from '../../ids'; import type { Host, Port } from '../../network/types'; import type NodeManager from '../../nodes/NodeManager'; import { UnaryHandler } from '@matrixai/rpc'; +import { matchSync } from '../../utils'; +import { validateSync } from '../../validation'; import * as ids from '../../ids'; import * as networkUtils from '../../network/utils'; import * as nodeErrors from '../../nodes/errors'; -import { matchSync } from '../../utils'; -import { validateSync } from '../../validation'; class NodesAdd extends UnaryHandler< { @@ -24,6 +26,9 @@ class NodesAdd extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): Promise => { const { db, nodeManager }: { db: DB; nodeManager: NodeManager } = this.container; @@ -72,8 +77,8 @@ class NodesAdd extends UnaryHandler< true, input.force ?? false, 1500, - undefined, tran, + ctx, ), ); return {}; diff --git a/src/client/handlers/NodesClaim.ts b/src/client/handlers/NodesClaim.ts index 9056f07bd..deed7352c 100644 --- a/src/client/handlers/NodesClaim.ts +++ b/src/client/handlers/NodesClaim.ts @@ -41,11 +41,11 @@ class NodesClaim extends UnaryHandler< }, ); await db.withTransactionF(async (tran) => { - // Attempt to claim the node, - // if there is no permission then we get an error + // Attempt to claim the node. If there is no permission then we get an + // error. await nodeManager.claimNode(nodeId, tran); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/NodesFind.ts b/src/client/handlers/NodesFind.ts index d8de9d7ba..904ed7137 100644 --- a/src/client/handlers/NodesFind.ts +++ b/src/client/handlers/NodesFind.ts @@ -1,3 +1,5 @@ +import type { JSONValue } from '@matrixai/rpc'; +import type { ContextTimed } from '@matrixai/contexts'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -6,12 +8,11 @@ import type { } from '../types'; import type { NodeId } from '../../ids'; import type NodeManager from '../../nodes/NodeManager'; -import type { ContextTimed } from '@matrixai/contexts'; import { UnaryHandler } from '@matrixai/rpc'; -import * as ids from '../../ids'; -import * as nodesErrors from '../../nodes/errors'; import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as ids from '../../ids'; +import * as nodesErrors from '../../nodes/errors'; class NodesFind extends UnaryHandler< { @@ -22,8 +23,8 @@ class NodesFind extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, - _cancel, - _meta, + _cancel: (reason?: any) => void, + _meta: Record, ctx: ContextTimed, ): Promise> => { const { nodeManager }: { nodeManager: NodeManager } = this.container; @@ -42,12 +43,7 @@ class NodesFind extends UnaryHandler< nodeId: input.nodeIdEncoded, }, ); - const result = await nodeManager.findNode( - { - nodeId: nodeId, - }, - ctx, - ); + const result = await nodeManager.findNode({ nodeId: nodeId }, ctx); if (result == null) { throw new nodesErrors.ErrorNodeGraphNodeIdNotFound(); } diff --git a/src/client/handlers/NodesGetAll.ts b/src/client/handlers/NodesGetAll.ts index 1477edc2d..f031411d8 100644 --- a/src/client/handlers/NodesGetAll.ts +++ b/src/client/handlers/NodesGetAll.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -15,20 +17,18 @@ class NodesGetAll extends ServerHandler< ClientRPCResponseResult > { public handle = async function* ( - _input, - _cancel, - _meta, - ctx, + _input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { nodeGraph } = this.container; for await (const [index, bucket] of nodeGraph.getBuckets()) { for (const [id, nodeContact] of bucket) { const encodedId = nodesUtils.encodeNodeId(id); // For every node in every bucket, add it to our message - if (ctx.signal.aborted) { - throw ctx.signal.reason; - } + ctx.signal.throwIfAborted(); yield { bucketIndex: index, nodeIdEncoded: encodedId, diff --git a/src/client/handlers/NodesListConnections.ts b/src/client/handlers/NodesListConnections.ts index 0e7710189..2fcfb8c87 100644 --- a/src/client/handlers/NodesListConnections.ts +++ b/src/client/handlers/NodesListConnections.ts @@ -4,6 +4,8 @@ import type { NodeConnectionMessage, } from '../types'; import type NodeConnectionManager from '../../nodes/NodeConnectionManager'; +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import { ServerHandler } from '@matrixai/rpc'; import * as nodesUtils from '../../nodes/utils'; @@ -15,17 +17,19 @@ class NodesListConnections extends ServerHandler< ClientRPCResponseResult > { public handle = async function* ( - _input, - _cancel, - _meta, - ctx, + _input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { const { nodeConnectionManager, - }: { nodeConnectionManager: NodeConnectionManager } = this.container; + }: { + nodeConnectionManager: NodeConnectionManager; + } = this.container; const connections = nodeConnectionManager.listConnections(); for (const connection of connections) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { host: connection.address.host, hostname: connection.address.hostname ?? '', diff --git a/src/client/handlers/NodesPing.ts b/src/client/handlers/NodesPing.ts index 7d9e16f03..e89b91056 100644 --- a/src/client/handlers/NodesPing.ts +++ b/src/client/handlers/NodesPing.ts @@ -38,7 +38,7 @@ class NodesPing extends UnaryHandler< }, ); const result = await nodeManager.pingNode(nodeId); - return { type: 'success', success: result != null }; + return { success: result != null }; }; } diff --git a/src/client/handlers/NotificationsInboxClear.ts b/src/client/handlers/NotificationsInboxClear.ts index 1789a5bc0..38f0d4c26 100644 --- a/src/client/handlers/NotificationsInboxClear.ts +++ b/src/client/handlers/NotificationsInboxClear.ts @@ -15,7 +15,10 @@ class NotificationsInboxClear extends UnaryHandler< const { db, notificationsManager, - }: { db: DB; notificationsManager: NotificationsManager } = this.container; + }: { + db: DB; + notificationsManager: NotificationsManager; + } = this.container; await db.withTransactionF((tran) => notificationsManager.clearInboxNotifications(tran), ); diff --git a/src/client/handlers/NotificationsInboxRead.ts b/src/client/handlers/NotificationsInboxRead.ts index 232204d0d..9d45458a7 100644 --- a/src/client/handlers/NotificationsInboxRead.ts +++ b/src/client/handlers/NotificationsInboxRead.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -20,15 +22,18 @@ class NotificationsInboxRead extends ServerHandler< > { public handle( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { db, notificationsManager, - }: { db: DB; notificationsManager: NotificationsManager } = this.container; + }: { + db: DB; + notificationsManager: NotificationsManager; + } = this.container; const { seek, seekEnd, unread, order, limit } = input; let seek_: NotificationId | number | undefined; @@ -55,7 +60,7 @@ class NotificationsInboxRead extends ServerHandler< tran, }); for await (const notification of notifications) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { notification: notification, }; diff --git a/src/client/handlers/NotificationsOutboxRead.ts b/src/client/handlers/NotificationsOutboxRead.ts index 8160f1d1b..da9d7473c 100644 --- a/src/client/handlers/NotificationsOutboxRead.ts +++ b/src/client/handlers/NotificationsOutboxRead.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -20,15 +22,18 @@ class NotificationsOutboxRead extends ServerHandler< > { public handle( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { db, notificationsManager, - }: { db: DB; notificationsManager: NotificationsManager } = this.container; + }: { + db: DB; + notificationsManager: NotificationsManager; + } = this.container; const { seek, seekEnd, order, limit } = input; let seek_: NotificationId | number | undefined; @@ -54,7 +59,7 @@ class NotificationsOutboxRead extends ServerHandler< tran, }); for await (const notification of notifications) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const taskInfo = await notificationsManager.getOutboxNotificationTaskInfoById( notificationsUtils.decodeNotificationId( diff --git a/src/client/handlers/NotificationsSend.ts b/src/client/handlers/NotificationsSend.ts index 86de7d3e9..6628c6a22 100644 --- a/src/client/handlers/NotificationsSend.ts +++ b/src/client/handlers/NotificationsSend.ts @@ -23,7 +23,9 @@ class NotificationsSend extends UnaryHandler< ): Promise => { const { notificationsManager, - }: { notificationsManager: NotificationsManager } = this.container; + }: { + notificationsManager: NotificationsManager; + } = this.container; const { nodeId, }: { diff --git a/src/client/handlers/VaultsClone.ts b/src/client/handlers/VaultsClone.ts index ff2b99376..069b2ddf4 100644 --- a/src/client/handlers/VaultsClone.ts +++ b/src/client/handlers/VaultsClone.ts @@ -1,16 +1,18 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, CloneMessage, SuccessMessage, } from '../types'; -import type VaultManager from '../../vaults/VaultManager'; import type { NodeId } from '../../ids'; +import type VaultManager from '../../vaults/VaultManager'; import { UnaryHandler } from '@matrixai/rpc'; -import * as ids from '../../ids'; import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as ids from '../../ids'; class VaultsClone extends UnaryHandler< { @@ -22,6 +24,9 @@ class VaultsClone extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -40,11 +45,10 @@ class VaultsClone extends UnaryHandler< nodeId: input.nodeIdEncoded, }, ); - // Vault id await db.withTransactionF(async (tran) => { - await vaultManager.cloneVault(nodeId, input.nameOrId, tran); + await vaultManager.cloneVault(nodeId, input.nameOrId, tran, ctx); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsCreate.ts b/src/client/handlers/VaultsCreate.ts index cd7f503d0..fb4518fc7 100644 --- a/src/client/handlers/VaultsCreate.ts +++ b/src/client/handlers/VaultsCreate.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -19,17 +21,16 @@ class VaultsCreate extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; - const vaultId = await db.withTransactionF((tran) => - vaultManager.createVault(input.vaultName, tran), + vaultManager.createVault(input.vaultName, tran, ctx), ); - - return { - vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId), - }; + return { vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId) }; }; } diff --git a/src/client/handlers/VaultsDelete.ts b/src/client/handlers/VaultsDelete.ts index d341d4f66..714410c6d 100644 --- a/src/client/handlers/VaultsDelete.ts +++ b/src/client/handlers/VaultsDelete.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -6,10 +8,9 @@ import type { VaultIdentifierMessage, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; -import type { VaultName } from '../../vaults/types'; import { UnaryHandler } from '@matrixai/rpc'; -import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; +import * as vaultsUtils from '../../vaults/utils'; class VaultsDelete extends UnaryHandler< { @@ -21,22 +22,27 @@ class VaultsDelete extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; await db.withTransactionF(async (tran) => { const vaultIdFromName = await vaultManager.getVaultId( - input.nameOrId as VaultName, + input.nameOrId, tran, ); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } - await vaultManager.destroyVault(vaultId, tran); + await vaultManager.destroyVault(vaultId, tran, ctx); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsList.ts b/src/client/handlers/VaultsList.ts index fb0c0aa3d..9820a9ffb 100644 --- a/src/client/handlers/VaultsList.ts +++ b/src/client/handlers/VaultsList.ts @@ -1,3 +1,4 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; import type { ClientRPCRequestParams, @@ -5,6 +6,7 @@ import type { VaultListMessage, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; +import type { JSONValue } from '@matrixai/rpc'; import { ServerHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils'; @@ -17,21 +19,21 @@ class VaultsList extends ServerHandler< ClientRPCResponseResult > { public handle = async function* ( - _input, - _cancel, - _meta, - ctx, + _input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; + ctx.signal.throwIfAborted(); const vaults = await db.withTransactionF((tran) => - vaultManager.listVaults(tran), + vaultManager.listVaults(ctx, tran), ); for await (const [vaultName, vaultId] of vaults) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { - vaultName, + vaultName: vaultName, vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId), }; } diff --git a/src/client/handlers/VaultsLog.ts b/src/client/handlers/VaultsLog.ts index 591b6ca9c..16fe0e573 100644 --- a/src/client/handlers/VaultsLog.ts +++ b/src/client/handlers/VaultsLog.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -6,7 +8,6 @@ import type { VaultsLogMessage, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; -import type { VaultName } from '../../vaults/types'; import { ServerHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; @@ -21,34 +22,37 @@ class VaultsLog extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; const log = await db.withTransactionF(async (tran) => { const vaultIdFromName = await vaultManager.getVaultId( - input.nameOrId as VaultName, + input.nameOrId, tran, ); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } // Getting the log return await vaultManager.withVaults( [vaultId], async (vault) => { - return await vault.log(input.commitId, input.depth); + return await vault.log(input.commitId ?? 'HEAD', input.depth, ctx); }, tran, + ctx, ); }); for (const entry of log) { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { commitId: entry.commitId, committer: entry.committer.name, diff --git a/src/client/handlers/VaultsPermissionGet.ts b/src/client/handlers/VaultsPermissionGet.ts index 9e08d6bff..2d25c6d6b 100644 --- a/src/client/handlers/VaultsPermissionGet.ts +++ b/src/client/handlers/VaultsPermissionGet.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -26,11 +28,11 @@ class VaultsPermissionGet extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { db, vaultManager, @@ -45,7 +47,9 @@ class VaultsPermissionGet extends ServerHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } // Getting permissions return [await acl.getVaultPerm(vaultId, tran), vaultId]; @@ -62,7 +66,7 @@ class VaultsPermissionGet extends ServerHandler< const actions = Object.keys( permissionList[nodeIdString], ) as Array; - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); yield { vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId), nodeIdEncoded: nodesUtils.encodeNodeId(nodeId), diff --git a/src/client/handlers/VaultsPermissionSet.ts b/src/client/handlers/VaultsPermissionSet.ts index 358fd8340..9614ccb3f 100644 --- a/src/client/handlers/VaultsPermissionSet.ts +++ b/src/client/handlers/VaultsPermissionSet.ts @@ -7,16 +7,16 @@ import type { } from '../types'; import type ACL from '../../acl/ACL'; import type { VaultAction, VaultActions } from '../../vaults/types'; -import type { NodeId } from '../../ids'; import type VaultManager from '../../vaults/VaultManager'; import type NotificationsManager from '../../notifications/NotificationsManager'; import type GestaltGraph from '../../gestalts/GestaltGraph'; +import type { NodeId } from '../../ids'; import { UnaryHandler } from '@matrixai/rpc'; +import { validateSync } from '../../validation'; +import { matchSync } from '../../utils'; import * as ids from '../../ids'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; -import { validateSync } from '../../validation'; -import { matchSync } from '../../utils'; class VaultsPermissionSet extends UnaryHandler< { @@ -53,7 +53,9 @@ class VaultsPermissionSet extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } const { nodeId, @@ -76,7 +78,11 @@ class VaultsPermissionSet extends UnaryHandler< ); // Checking if vault exists const vaultMeta = await vaultManager.getVaultMeta(vaultId, tran); - if (!vaultMeta) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + if (!vaultMeta) { + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); + } // Setting permissions const actionsSet: VaultActions = {}; await gestaltGraph.setGestaltAction(['node', nodeId], 'scan', tran); @@ -86,7 +92,7 @@ class VaultsPermissionSet extends UnaryHandler< } // Sending notification await notificationsManager.sendNotification({ - nodeId, + nodeId: nodeId, data: { type: 'VaultShare', vaultId: vaultsUtils.encodeVaultId(vaultId), @@ -95,7 +101,7 @@ class VaultsPermissionSet extends UnaryHandler< }, }); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsPermissionUnset.ts b/src/client/handlers/VaultsPermissionUnset.ts index 4e19a91d7..6f1668181 100644 --- a/src/client/handlers/VaultsPermissionUnset.ts +++ b/src/client/handlers/VaultsPermissionUnset.ts @@ -49,7 +49,9 @@ class VaultsPermissionUnset extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } const { nodeId, @@ -93,7 +95,7 @@ class VaultsPermissionUnset extends UnaryHandler< } }); // Formatting response - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsPull.ts b/src/client/handlers/VaultsPull.ts index 328ea8a78..917a79bab 100644 --- a/src/client/handlers/VaultsPull.ts +++ b/src/client/handlers/VaultsPull.ts @@ -1,19 +1,20 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, SuccessMessage, VaultsPullMessage, } from '../types'; -import type { VaultName } from '../../vaults/types'; import type { NodeId } from '../../ids'; import type VaultManager from '../../vaults/VaultManager'; import { UnaryHandler } from '@matrixai/rpc'; +import { validateSync } from '../../validation'; +import { matchSync } from '../../utils'; import * as ids from '../../ids'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; -import { validateSync } from '../../validation'; -import { matchSync } from '../../utils'; class VaultsPull extends UnaryHandler< { @@ -25,30 +26,33 @@ class VaultsPull extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; - let pullVaultId; - pullVaultId = vaultsUtils.decodeVaultId(input.pullVault); - pullVaultId = pullVaultId ?? input.pullVault; + const pullVaultId = + vaultsUtils.decodeVaultId(input.pullVault) ?? input.pullVault; await db.withTransactionF(async (tran) => { const vaultIdFromName = await vaultManager.getVaultId( - input.nameOrId as VaultName, + input.nameOrId!, tran, ); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } - const { - nodeId, - }: { - nodeId: NodeId | undefined; - } = validateSync( + const { nodeId }: { nodeId: NodeId } = validateSync( (keyPath, value) => { return matchSync(keyPath)( - [['nodeId'], () => (value ? ids.parseNodeId(value) : undefined)], + [ + ['nodeId'], + () => (value != null ? ids.parseNodeId(value) : undefined), + ], () => value, ); }, @@ -56,14 +60,17 @@ class VaultsPull extends UnaryHandler< nodeId: input.nodeIdEncoded, }, ); - await vaultManager.pullVault({ - vaultId, - pullNodeId: nodeId, - pullVaultNameOrId: pullVaultId, + await vaultManager.pullVault( + { + vaultId: vaultId, + pullNodeId: nodeId, + pullVaultNameOrId: pullVaultId, + }, tran, - }); + ctx, + ); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsRename.ts b/src/client/handlers/VaultsRename.ts index 3720e792e..74415a907 100644 --- a/src/client/handlers/VaultsRename.ts +++ b/src/client/handlers/VaultsRename.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -20,6 +22,9 @@ class VaultsRename extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -31,12 +36,12 @@ class VaultsRename extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } - await vaultManager.renameVault(vaultId, input.newName, tran); - return { - vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId), - }; + await vaultManager.renameVault(vaultId, input.newName, tran, ctx); + return { vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId) }; }); }; } diff --git a/src/client/handlers/VaultsScan.ts b/src/client/handlers/VaultsScan.ts index 7d0880399..2f7dace6d 100644 --- a/src/client/handlers/VaultsScan.ts +++ b/src/client/handlers/VaultsScan.ts @@ -1,15 +1,17 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, NodeIdMessage, VaultsScanMessage, } from '../types'; -import type VaultManager from '../../vaults/VaultManager'; import type { NodeId } from '../../ids'; +import type VaultManager from '../../vaults/VaultManager'; import { ServerHandler } from '@matrixai/rpc'; -import * as ids from '../../ids'; import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as ids from '../../ids'; class VaultsScan extends ServerHandler< { @@ -20,17 +22,13 @@ class VaultsScan extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); const { vaultManager }: { vaultManager: VaultManager } = this.container; - const { - nodeId, - }: { - nodeId: NodeId; - } = validateSync( + const { nodeId }: { nodeId: NodeId } = validateSync( (keyPath, value) => { return matchSync(keyPath)( [['nodeId'], () => ids.parseNodeId(value)], @@ -45,11 +43,11 @@ class VaultsScan extends ServerHandler< vaultIdEncoded, vaultName, vaultPermissions, - } of vaultManager.scanVaults(nodeId)) { - if (ctx.signal.aborted) throw ctx.signal.reason; + } of vaultManager.scanVaults(nodeId, ctx)) { + ctx.signal.throwIfAborted(); yield { - vaultName, - vaultIdEncoded, + vaultName: vaultName, + vaultIdEncoded: vaultIdEncoded, permissions: vaultPermissions, }; } diff --git a/src/client/handlers/VaultsSecretsCat.ts b/src/client/handlers/VaultsSecretsCat.ts index e75be0c30..dcd8e0234 100644 --- a/src/client/handlers/VaultsSecretsCat.ts +++ b/src/client/handlers/VaultsSecretsCat.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -26,26 +28,33 @@ class VaultsSecretsCat extends DuplexHandler< input: AsyncIterableIterator< ClientRPCRequestParams >, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; yield* db.withTransactionG(async function* (tran): AsyncGenerator< ClientRPCResponseResult > { - // As we need to preserve the order of parameters, we need to loop over - // them individually, as grouping them would make them go out of order. - for await (const secretIdentiferMessage of input) { - const { nameOrId, secretName } = secretIdentiferMessage; + // To preserve the order of parameters, we need to loop over them + // individually, as grouping them would make them go out of order. + for await (const secretIdentifierMessage of input) { + const { nameOrId, secretName } = secretIdentifierMessage; const vaultIdFromName = await vaultManager.getVaultId(nameOrId, tran); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(nameOrId); - if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + if (vaultId == null) { + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${nameOrId}" does not exist`, + ); + } yield await vaultManager.withVaults( [vaultId], async (vault) => { try { const content = await vaultOps.getSecret(vault, secretName); return { - type: 'success', + type: 'SuccessMessage', success: true, secretContent: content.toString('binary'), }; @@ -55,7 +64,7 @@ class VaultsSecretsCat extends DuplexHandler< e instanceof vaultsErrors.ErrorSecretsIsDirectory ) { return { - type: 'error', + type: 'ErrorMessage', code: e.cause.code, reason: secretName, }; @@ -64,6 +73,7 @@ class VaultsSecretsCat extends DuplexHandler< } }, tran, + ctx, ); } }); diff --git a/src/client/handlers/VaultsSecretsEnv.ts b/src/client/handlers/VaultsSecretsEnv.ts index 58cb1b79d..becc15c07 100644 --- a/src/client/handlers/VaultsSecretsEnv.ts +++ b/src/client/handlers/VaultsSecretsEnv.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -22,24 +24,24 @@ class VaultsSecretsEnv extends DuplexHandler< input: AsyncIterableIterator< ClientRPCRequestParams >, - _cancel, - _meta, - ctx, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; - return yield* db.withTransactionG(async function* (tran): AsyncGenerator< ClientRPCResponseResult > { - if (ctx.signal.aborted) throw ctx.signal.reason; + ctx.signal.throwIfAborted(); for await (const secretIdentifierMessage of input) { const { nameOrId, secretName } = secretIdentifierMessage; const vaultIdFromName = await vaultManager.getVaultId(nameOrId, tran); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${nameOrId}" does not exist`, + ); } const secrets = await vaultManager.withVaults( [vaultId], @@ -54,9 +56,10 @@ class VaultsSecretsEnv extends DuplexHandler< fs, secretName, )) { + ctx.signal.throwIfAborted(); const fileContents = await fs.readFile(filePath); results.push({ - filePath, + filePath: filePath, value: fileContents.toString(), }); } @@ -73,8 +76,10 @@ class VaultsSecretsEnv extends DuplexHandler< }); }, tran, + ctx, ); for (const { filePath, value } of secrets) { + ctx.signal.throwIfAborted(); yield { nameOrId: nameOrId, secretName: filePath, diff --git a/src/client/handlers/VaultsSecretsGet.ts b/src/client/handlers/VaultsSecretsGet.ts index a0fa0f94d..21d4cf42e 100644 --- a/src/client/handlers/VaultsSecretsGet.ts +++ b/src/client/handlers/VaultsSecretsGet.ts @@ -34,7 +34,11 @@ class VaultsSecretsGet extends ServerHandler< ); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); - if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + if (vaultId == null) { + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); + } // Get the contents of the file return await vaultManager.withVaults([vaultId], async (vault) => { const content = await vaultOps.getSecret(vault, input.secretName); diff --git a/src/client/handlers/VaultsSecretsList.ts b/src/client/handlers/VaultsSecretsList.ts index 5e5735357..c2e194f25 100644 --- a/src/client/handlers/VaultsSecretsList.ts +++ b/src/client/handlers/VaultsSecretsList.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { DB } from '@matrixai/db'; import type { ClientRPCRequestParams, @@ -21,6 +23,9 @@ class VaultsSecretsList extends ServerHandler< > { public handle = async function* ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator, void, void> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -31,7 +36,11 @@ class VaultsSecretsList extends ServerHandler< ); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); - if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + if (vaultId == null) { + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); + } return vaultId; }); @@ -58,6 +67,7 @@ class VaultsSecretsList extends ServerHandler< throw e; } for await (const file of files) { + ctx.signal.throwIfAborted(); const filePath = path.join(input.secretName, file.toString()); const stat = await fs.promises.stat(filePath); const type = stat.isFile() ? 'FILE' : 'DIRECTORY'; diff --git a/src/client/handlers/VaultsSecretsMkdir.ts b/src/client/handlers/VaultsSecretsMkdir.ts index da8b2c00c..22dee5aed 100644 --- a/src/client/handlers/VaultsSecretsMkdir.ts +++ b/src/client/handlers/VaultsSecretsMkdir.ts @@ -1,9 +1,11 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, SecretDirMessage, - SuccessOrErrorMessage, + SuccessOrErrorMessageTagged, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; import type { POJO } from '../../types'; @@ -18,17 +20,21 @@ class VaultsSecretsMkdir extends DuplexHandler< vaultManager: VaultManager; }, ClientRPCRequestParams, - ClientRPCResponseResult + ClientRPCResponseResult > { public handle = async function* ( input: AsyncIterableIterator>, - ): AsyncGenerator> { + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, + ): AsyncGenerator> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; let metadata: POJO; yield* db.withTransactionG( - async function* (tran): AsyncGenerator { + async function* (tran): AsyncGenerator { for await (const secretDirMessage of input) { + ctx.signal.throwIfAborted(); // Unpack input if (metadata == null) metadata = secretDirMessage.metadata ?? {}; const nameOrId = secretDirMessage.nameOrId; @@ -38,13 +44,13 @@ class VaultsSecretsMkdir extends DuplexHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${nameOrId}" does not exist`, + ); } + // Write directories. This doesn't need to be grouped by vault names, - // as no commit is created for empty directories anyways. The - // vaultOps.mkdir() method also returns an object of type - // SuccessOrErrorMessage. As such, we can return the result without - // doing any type conversion or extra processing. + // as no commit is created for empty directories anyway. yield await vaultManager.withVaults( [vaultId], async (vault) => { @@ -52,14 +58,14 @@ class VaultsSecretsMkdir extends DuplexHandler< await vaultOps.mkdir(vault, dirName, { recursive: metadata?.options?.recursive, }); - return { type: 'success', success: true }; + return { type: 'SuccessMessage', success: true }; } catch (e) { if ( e instanceof vaultsErrors.ErrorVaultsRecursive || e instanceof vaultsErrors.ErrorSecretsSecretDefined ) { return { - type: 'error', + type: 'ErrorMessage', code: e.cause.code, reason: dirName, }; @@ -69,6 +75,7 @@ class VaultsSecretsMkdir extends DuplexHandler< } }, tran, + ctx, ); } }, diff --git a/src/client/handlers/VaultsSecretsNew.ts b/src/client/handlers/VaultsSecretsNew.ts index 33b21fd12..23ff7360e 100644 --- a/src/client/handlers/VaultsSecretsNew.ts +++ b/src/client/handlers/VaultsSecretsNew.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -21,6 +23,9 @@ class VaultsSecretsNew extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -32,7 +37,9 @@ class VaultsSecretsNew extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } const content = Buffer.from(input.secretContent, 'binary'); await vaultManager.withVaults( @@ -41,9 +48,10 @@ class VaultsSecretsNew extends UnaryHandler< await vaultOps.addSecret(vault, input.secretName, content); }, tran, + ctx, ); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsSecretsNewDir.ts b/src/client/handlers/VaultsSecretsNewDir.ts index 22d436914..1287fa077 100644 --- a/src/client/handlers/VaultsSecretsNewDir.ts +++ b/src/client/handlers/VaultsSecretsNewDir.ts @@ -1,5 +1,7 @@ import type { FileSystem } from 'types'; +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -23,6 +25,9 @@ class VaultsSecretsNewDir extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, @@ -37,17 +42,26 @@ class VaultsSecretsNewDir extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } await vaultManager.withVaults( [vaultId], async (vault) => { - await vaultOps.addSecretDirectory(vault, input.dirName, fs); + await vaultOps.addSecretDirectory( + vault, + input.dirName, + fs, + undefined, + ctx, + ); }, tran, + ctx, ); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsSecretsRemove.ts b/src/client/handlers/VaultsSecretsRemove.ts index eb8796ef5..9fd7ad787 100644 --- a/src/client/handlers/VaultsSecretsRemove.ts +++ b/src/client/handlers/VaultsSecretsRemove.ts @@ -1,11 +1,13 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; import type { ResourceAcquire } from '@matrixai/resources'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, SecretsRemoveHeaderMessage, SecretIdentifierMessageTagged, - SuccessOrErrorMessage, + SuccessOrErrorMessageTagged, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; import type { FileSystemWritable } from '../../vaults/types'; @@ -23,7 +25,7 @@ class VaultsSecretsRemove extends DuplexHandler< ClientRPCRequestParams< SecretsRemoveHeaderMessage | SecretIdentifierMessageTagged >, - ClientRPCResponseResult + ClientRPCResponseResult > { public handle = async function* ( input: AsyncIterableIterator< @@ -31,7 +33,10 @@ class VaultsSecretsRemove extends DuplexHandler< SecretsRemoveHeaderMessage | SecretIdentifierMessageTagged > >, - ): AsyncGenerator> { + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, + ): AsyncGenerator> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; // Extract the header message from the iterator @@ -50,16 +55,19 @@ class VaultsSecretsRemove extends DuplexHandler< const vaultAcquires = await db.withTransactionF(async (tran) => { const vaultAcquires: Array> = []; for (const vaultName of headerMessage.vaultNames) { + ctx.signal.throwIfAborted(); const vaultIdFromName = await vaultManager.getVaultId(vaultName, tran); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName); if (vaultId == null) { throw new vaultsErrors.ErrorVaultsVaultUndefined( - `Vault ${vaultName} does not exist`, + `Vault "${vaultName}" does not exist`, ); } + // The resource acquisition will automatically create a transaction and + // release it when cleaning up. const acquire = await vaultManager.withVaults( [vaultId], - async (vault) => vault.acquireWrite(), + async (vault) => vault.acquireWrite(undefined, ctx), ); vaultAcquires.push(acquire); } @@ -68,7 +76,7 @@ class VaultsSecretsRemove extends DuplexHandler< // Acquire all locks in parallel and perform all operations at once yield* withG( vaultAcquires, - async function* (efses): AsyncGenerator { + async function* (efses): AsyncGenerator { // Creating the vault name to efs map for easy access const vaultMap = new Map(); for (let i = 0; i < efses.length; i++) { @@ -76,6 +84,7 @@ class VaultsSecretsRemove extends DuplexHandler< } let loopRan = false; for await (const message of input) { + ctx.signal.throwIfAborted(); loopRan = true; // Header messages should not be seen anymore if (message.type === 'VaultNamesHeaderMessage') { @@ -99,7 +108,7 @@ class VaultsSecretsRemove extends DuplexHandler< await efs.unlink(message.secretName); } yield { - type: 'success', + type: 'SuccessMessage', success: true, }; } catch (e) { @@ -108,10 +117,10 @@ class VaultsSecretsRemove extends DuplexHandler< e.code === 'ENOTEMPTY' || e.code === 'EINVAL' ) { - // EINVAL can be triggered if removing the root of the - // vault is attempted. + // EINVAL can be triggered if removing the root of the vault is + // attempted. yield { - type: 'error', + type: 'ErrorMessage', code: e.code, reason: message.secretName, }; diff --git a/src/client/handlers/VaultsSecretsRename.ts b/src/client/handlers/VaultsSecretsRename.ts index 433b409ee..dea09fbae 100644 --- a/src/client/handlers/VaultsSecretsRename.ts +++ b/src/client/handlers/VaultsSecretsRename.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -21,6 +23,9 @@ class VaultsSecretsRename extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -32,7 +37,9 @@ class VaultsSecretsRename extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } await vaultManager.withVaults( [vaultId], @@ -41,12 +48,15 @@ class VaultsSecretsRename extends UnaryHandler< vault, input.secretName, input.newSecretName, + undefined, + ctx, ); }, tran, + ctx, ); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsSecretsStat.ts b/src/client/handlers/VaultsSecretsStat.ts index 456ff611e..8f66527cc 100644 --- a/src/client/handlers/VaultsSecretsStat.ts +++ b/src/client/handlers/VaultsSecretsStat.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -21,6 +23,9 @@ class VaultsSecretsStat extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -32,7 +37,9 @@ class VaultsSecretsStat extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } const secretName = input.secretName; const stat = await vaultManager.withVaults( @@ -41,6 +48,7 @@ class VaultsSecretsStat extends UnaryHandler< return await vaultOps.statSecret(vault, secretName); }, tran, + ctx, ); return { stat: { diff --git a/src/client/handlers/VaultsSecretsWriteFile.ts b/src/client/handlers/VaultsSecretsWriteFile.ts index 7a1cc7d5d..1d6a30863 100644 --- a/src/client/handlers/VaultsSecretsWriteFile.ts +++ b/src/client/handlers/VaultsSecretsWriteFile.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -21,6 +23,9 @@ class VaultsSecretsWriteFile extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -32,18 +37,27 @@ class VaultsSecretsWriteFile extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } const secretContent = Buffer.from(input.secretContent, 'binary'); await vaultManager.withVaults( [vaultId], async (vault) => { - await vaultOps.writeSecret(vault, input.secretName, secretContent); + await vaultOps.writeSecret( + vault, + input.secretName, + secretContent, + undefined, + ctx, + ); }, tran, + ctx, ); }); - return { type: 'success', success: true }; + return { success: true }; }; } diff --git a/src/client/handlers/VaultsVersion.ts b/src/client/handlers/VaultsVersion.ts index f32daaa9f..835530b3c 100644 --- a/src/client/handlers/VaultsVersion.ts +++ b/src/client/handlers/VaultsVersion.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { ClientRPCRequestParams, ClientRPCResponseResult, @@ -20,6 +22,9 @@ class VaultsVersion extends UnaryHandler< > { public handle = async ( input: ClientRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): Promise> => { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; @@ -31,24 +36,29 @@ class VaultsVersion extends UnaryHandler< const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault "${input.nameOrId}" does not exist`, + ); } const versionId = input.versionId; const [latestOid, currentVersionId] = await vaultManager.withVaults( [vaultId], async (vault) => { - const latestOid = (await vault.log())[0].commitId; + // Use default values for the ref and limit. We only care about + // passing in the relevant context. + const latestOid = (await vault.log(undefined, undefined, ctx))[0] + .commitId; await vault.version(versionId); - const currentVersionId = (await vault.log(versionId, 0))[0]?.commitId; + const currentVersionId = ( + await vault.log(versionId, undefined, ctx) + )[0]?.commitId; return [latestOid, currentVersionId]; }, tran, + ctx, ); // Checking if latest version ID - const latestVersion = latestOid === currentVersionId; - return { - latestVersion, - }; + return { latestVersion: latestOid === currentVersionId }; }); }; } diff --git a/src/client/types.ts b/src/client/types.ts index c8843e334..d5a180153 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -201,18 +201,22 @@ type SignatureMessage = { type VerifySignatureMessage = PublicKeyMessage & DataMessage & SignatureMessage; type SuccessMessage = { - type: 'success'; success: boolean; }; -type ErrorMessage = { - type: 'error'; +type SuccessMessageTagged = { + type: 'SuccessMessage'; + success: boolean; +}; + +type ErrorMessageTagged = { + type: 'ErrorMessage'; code?: string | number; reason?: string; data?: JSONObject; }; -type SuccessOrErrorMessage = SuccessMessage | ErrorMessage; +type SuccessOrErrorMessageTagged = SuccessMessageTagged | ErrorMessageTagged; // Notifications messages @@ -321,9 +325,9 @@ type ContentMessage = { secretContent: string; }; -type ContentSuccessMessage = ContentMessage & SuccessMessage; +type ContentSuccessMessage = ContentMessage & SuccessMessageTagged; -type ContentOrErrorMessage = ContentSuccessMessage | ErrorMessage; +type ContentOrErrorMessage = ContentSuccessMessage | ErrorMessageTagged; type SecretContentMessage = SecretIdentifierMessage & ContentMessage; @@ -364,12 +368,12 @@ type SecretIdentifierMessageTagged = SecretIdentifierMessage & { type: 'SecretIdentifierMessage'; }; -type VaultNamesHeaderMessage = { +type VaultNamesHeaderMessageTagged = { type: 'VaultNamesHeaderMessage'; vaultNames: Array; }; -type SecretsRemoveHeaderMessage = VaultNamesHeaderMessage & { +type SecretsRemoveHeaderMessage = VaultNamesHeaderMessageTagged & { recursive?: boolean; }; @@ -416,8 +420,9 @@ export type { NodesGetMessage, NodesAddMessage, SuccessMessage, - ErrorMessage, - SuccessOrErrorMessage, + SuccessMessageTagged, + ErrorMessageTagged, + SuccessOrErrorMessageTagged, NotificationInboxMessage, NotificationOutboxMessage, NotificationReadMessage, @@ -449,7 +454,7 @@ export type { SecretFilesMessage, SecretStatMessage, SecretIdentifierMessageTagged, - VaultNamesHeaderMessage, + VaultNamesHeaderMessageTagged, SecretsRemoveHeaderMessage, SignatureMessage, OverrideRPClientType, diff --git a/src/gestalts/GestaltGraph.ts b/src/gestalts/GestaltGraph.ts index 30a1aa068..efe129876 100644 --- a/src/gestalts/GestaltGraph.ts +++ b/src/gestalts/GestaltGraph.ts @@ -1149,7 +1149,7 @@ class GestaltGraph { return; } default: - never(`type must be either "node" or "identity" got "${type}"`); + never(`type must be either "node" or "identity", got "${type}"`); } } diff --git a/src/git/http.ts b/src/git/http.ts index bd81dac36..a4bd43f04 100644 --- a/src/git/http.ts +++ b/src/git/http.ts @@ -1,3 +1,4 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { CapabilityList, Reference, @@ -110,15 +111,18 @@ import * as utils from '../utils'; * * `referenceList` is called for generating the `ref_list` stage. */ -async function* advertiseRefGenerator({ - efs, - dir, - gitDir, -}: { - efs: EncryptedFS; - dir: string; - gitDir: string; -}): AsyncGenerator { +async function* advertiseRefGenerator( + { + efs, + dir, + gitDir, + }: { + efs: EncryptedFS; + dir: string; + gitDir: string; + }, + ctx: ContextTimed, +): AsyncGenerator { // Providing side-band-64, symref for the HEAD and agent name capabilities const capabilityList = [ gitUtils.SIDE_BAND_64_CAPABILITY, @@ -130,18 +134,21 @@ async function* advertiseRefGenerator({ }), gitUtils.AGENT_CAPABILITY, ]; - const objectGenerator = gitUtils.listReferencesGenerator({ - efs, - dir, - gitDir, - }); + const objectGenerator = gitUtils.listReferencesGenerator( + { + efs, + dir, + gitDir, + }, + ctx, + ); // PKT-LINE("# service=$servicename" LF) yield packetLineBuffer(gitUtils.REFERENCE_DISCOVERY_HEADER); // "0000" yield gitUtils.FLUSH_PACKET_BUFFER; // Ref_list - yield* referenceListGenerator(objectGenerator, capabilityList); + yield* referenceListGenerator(objectGenerator, capabilityList, ctx); // "0000" yield gitUtils.FLUSH_PACKET_BUFFER; } @@ -165,6 +172,7 @@ async function* advertiseRefGenerator({ async function* referenceListGenerator( objectGenerator: AsyncGenerator<[Reference, ObjectId], void, void>, capabilities: CapabilityList, + ctx: ContextTimed, ): AsyncGenerator { // Cap-list = capability *(SP capability) const capabilitiesListBuffer = Buffer.from( @@ -175,6 +183,7 @@ async function* referenceListGenerator( // *ref_record let first = true; for await (const [name, objectId] of objectGenerator) { + ctx.signal.throwIfAborted(); if (first) { // PKT-LINE(obj-id SP name NUL cap_list LF) yield packetLineBuffer( @@ -341,34 +350,43 @@ async function parsePackRequest( * It will respond with the `PKT-LINE(NAK_BUFFER)` and then the `packFile` data chunked into lines for the stream. * */ -async function* generatePackRequest({ - efs, - dir, - gitDir, - body, -}: { - efs: EncryptedFS; - dir: string; - gitDir: string; - body: Array; -}): AsyncGenerator { - const [wants, haves, _capabilities] = await parsePackRequest(body); - const objectIds = await gitUtils.listObjects({ - efs: efs, +async function* generatePackRequest( + { + efs, dir, - gitDir: gitDir, - wants, - haves, - }); + gitDir, + body, + }: { + efs: EncryptedFS; + dir: string; + gitDir: string; + body: Array; + }, + ctx: ContextTimed, +): AsyncGenerator { + const [wants, haves, _capabilities] = await parsePackRequest(body); + const objectIds = await gitUtils.listObjects( + { + efs: efs, + dir: dir, + gitDir: gitDir, + wants: wants, + haves: haves, + }, + ctx, + ); // Reply that we have no common history and that we need to send everything yield packetLineBuffer(gitUtils.NAK_BUFFER); // Send everything over in pack format - yield* generatePackData({ - efs: efs, - dir, - gitDir, - objectIds, - }); + yield* generatePackData( + { + efs: efs, + dir: dir, + gitDir: gitDir, + objectIds: objectIds, + }, + ctx, + ); // Send dummy progress data yield packetLineBuffer( gitUtils.DUMMY_PROGRESS_BUFFER, @@ -384,26 +402,29 @@ async function* generatePackRequest({ * The `packFile` is chunked into the `packetLineBuffer` with the size defined by `chunkSize`. * */ -async function* generatePackData({ - efs, - dir, - gitDir, - objectIds, - chunkSize = gitUtils.PACK_CHUNK_SIZE, -}: { - efs: EncryptedFS; - dir: string; - gitDir: string; - objectIds: Array; - chunkSize?: number; -}): AsyncGenerator { +async function* generatePackData( + { + efs, + dir, + gitDir, + objectIds, + chunkSize = gitUtils.PACK_CHUNK_SIZE, + }: { + efs: EncryptedFS; + dir: string; + gitDir: string; + objectIds: Array; + chunkSize?: number; + }, + ctx: ContextTimed, +): AsyncGenerator { let packFile: PackObjectsResult; // In case of errors we don't want to throw them. This will result in the error being thrown into `isometric-git` // when it consumes the response. It handles this by logging out the error which we don't want to happen. try { packFile = await git.packObjects({ fs: efs, - dir, + dir: dir, gitdir: gitDir, oids: objectIds, }); @@ -423,6 +444,7 @@ async function* generatePackData({ // Streaming the packFile as chunks of the length specified by the `chunkSize`. // Each line is formatted as a `PKT-LINE` do { + ctx.signal.throwIfAborted(); const subBuffer = packFileBuffer.subarray(0, chunkSize); packFileBuffer = packFileBuffer.subarray(chunkSize); yield packetLineBuffer(subBuffer, gitUtils.CHANNEL_DATA); diff --git a/src/git/utils.ts b/src/git/utils.ts index dd5f914d6..c4dde6ae2 100644 --- a/src/git/utils.ts +++ b/src/git/utils.ts @@ -1,3 +1,4 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { Capability, CapabilityList, @@ -67,23 +68,27 @@ const DUMMY_PROGRESS_BUFFER = Buffer.from('progress is at 50%', BUFFER_FORMAT); * This will generate references and the objects they point to as a tuple. * `HEAD` is always yielded first along with all branches. */ -async function* listReferencesGenerator({ - efs, - dir, - gitDir, -}: { - efs: EncryptedFS; - dir: string; - gitDir: string; -}): AsyncGenerator<[Reference, ObjectId], void, void> { +async function* listReferencesGenerator( + { + efs, + dir, + gitDir, + }: { + efs: EncryptedFS; + dir: string; + gitDir: string; + }, + ctx: ContextTimed, +): AsyncGenerator<[Reference, ObjectId], void, void> { const refs: Array<[string, Promise]> = await git .listBranches({ fs: efs, - dir, + dir: dir, gitdir: gitDir, }) .then((refs) => { return refs.map((ref) => { + ctx.signal.throwIfAborted(); return [ `${REFERENCES_STRING}${ref}`, git.resolveRef({ fs: efs, dir, gitdir: gitDir, ref: ref }), @@ -93,12 +98,13 @@ async function* listReferencesGenerator({ // HEAD always comes first const resolvedHead = await git.resolveRef({ fs: efs, - dir, + dir: dir, gitdir: gitDir, ref: HEAD_REFERENCE, }); yield [HEAD_REFERENCE, resolvedHead]; for (const [key, refP] of refs) { + ctx.signal.throwIfAborted(); yield [key, await refP]; } } @@ -122,7 +128,7 @@ async function referenceCapability({ try { const resolvedHead = await git.resolveRef({ fs: efs, - dir, + dir: dir, gitdir: gitDir, ref: reference, depth: 2, @@ -143,19 +149,22 @@ async function referenceCapability({ * The walk is preformed recursively and concurrently using promises. * Inspecting the git data structure objects is done using `isomorphic-git`. */ -async function listObjects({ - efs, - dir, - gitDir, - wants, - haves, -}: { - efs: EncryptedFS; - dir: string; - gitDir: string; - wants: ObjectIdList; - haves: ObjectIdList; -}): Promise { +async function listObjects( + { + efs, + dir, + gitDir, + wants, + haves, + }: { + efs: EncryptedFS; + dir: string; + gitDir: string; + wants: ObjectIdList; + haves: ObjectIdList; + }, + ctx: ContextTimed, +): Promise { const commits = new Set(); const trees = new Set(); const blobs = new Set(); @@ -163,6 +172,7 @@ async function listObjects({ const havesSet: Set = new Set(haves); async function walk(objectId: ObjectId, type: ObjectType): Promise { + ctx.signal.throwIfAborted(); // If object was listed as a have then we don't need to walk over it if (havesSet.has(objectId)) return; switch (type) { @@ -171,7 +181,7 @@ async function listObjects({ commits.add(objectId); const readCommitResult = await git.readCommit({ fs: efs, - dir, + dir: dir, gitdir: gitDir, oid: objectId, }); @@ -188,7 +198,7 @@ async function listObjects({ trees.add(objectId); const readTreeResult = await git.readTree({ fs: efs, - dir, + dir: dir, gitdir: gitDir, oid: objectId, }); @@ -209,7 +219,7 @@ async function listObjects({ tags.add(objectId); const readTagResult = await git.readTag({ fs: efs, - dir, + dir: dir, gitdir: gitDir, oid: objectId, }); @@ -239,17 +249,21 @@ const excludedDirs = ['pack', 'info']; /** * Walks the filesystem to list out all git objects in the objects directory */ -async function listObjectsAll({ - fs, - gitDir, -}: { - fs: EncryptedFS; - gitDir: string; -}) { +async function listObjectsAll( + { + fs, + gitDir, + }: { + fs: EncryptedFS; + gitDir: string; + }, + ctx: ContextTimed, +): Promise> { const objectsDirPath = path.join(gitDir, objectsDirName); const objectSet: Set = new Set(); const objectDirs = await fs.promises.readdir(objectsDirPath); for (const objectDir of objectDirs) { + ctx.signal.throwIfAborted(); if (typeof objectDir !== 'string') { utils.never('objectDir should be a string'); } @@ -258,6 +272,7 @@ async function listObjectsAll({ path.join(objectsDirPath, objectDir), ); for (const objectId of objectIds) { + ctx.signal.throwIfAborted(); objectSet.add(objectDir + objectId); } } diff --git a/src/nodes/NodeManager.ts b/src/nodes/NodeManager.ts index e76ede91d..18a526676 100644 --- a/src/nodes/NodeManager.ts +++ b/src/nodes/NodeManager.ts @@ -494,7 +494,7 @@ class NodeManager { * Perform some function on another node over the network with a connection. * Will either retrieve an existing connection, or create a new one if it * doesn't exist. - * for use with normal arrow function + * For use with normal arrow function * @param nodeId Id of target node to communicate with * @param f Function to handle communication * @param ctx @@ -1632,8 +1632,8 @@ class NodeManager { block?: boolean, force?: boolean, connectionConnectTimeoutTime?: number, - ctx?: Partial, tran?: DBTransaction, + ctx?: Partial, ): PromiseCancellable; @ready(new nodesErrors.ErrorNodeManagerNotRunning(), true, ['stopping']) @timedCancellable(true) @@ -1644,8 +1644,8 @@ class NodeManager { block: boolean = false, force: boolean = false, connectionConnectTimeoutTime: number = this.connectionConnectTimeoutTime, + tran: DBTransaction, @context ctx: ContextTimed, - tran?: DBTransaction, ): Promise { // We don't want to add our own node if (nodeId.equals(this.keyRing.getNodeId())) { @@ -1662,22 +1662,23 @@ class NodeManager { block, force, connectionConnectTimeoutTime, - ctx, tran, + ctx, ), ); } - // Need to await node connection verification, if fail, need to reject connection. + // Need to await node connection verification. If failed, need to reject + // connection. // When adding a node we need to handle 3 cases // 1. The node already exists. We need to update it's last updated field - // 2. The node doesn't exist and bucket has room. - // We need to add the node to the bucket - // 3. The node doesn't exist and the bucket is full. - // We need to ping the oldest node. If the ping succeeds we need to update - // the lastUpdated of the oldest node and drop the new one. If the ping - // fails we delete the old node and add in the new one. + // 2. The node doesn't exist and bucket has room. We need to add the node + // to the bucket + // 3. The node doesn't exist and the bucket is full. We need to ping the + // oldest node. If the ping succeeds we need to update the lastUpdated of + // the oldest node and drop the new one. If the ping fails we delete the + // old node and add in the new one. const [bucketIndex] = this.nodeGraph.bucketIndex(nodeId); // To avoid conflict we want to lock on the bucket index await this.nodeGraph.lockBucket(bucketIndex, tran, ctx); @@ -1857,8 +1858,8 @@ class NodeManager { false, false, undefined, - ctx, tran, + ctx, ); } else { // We don't remove node the ping was aborted @@ -1891,8 +1892,8 @@ class NodeManager { false, false, undefined, - ctx, tran, + ctx, ); removedNodes -= 1; } @@ -1930,7 +1931,7 @@ class NodeManager { } protected async setupGCTask(bucketIndex: number) { - // Check and start a 'garbageCollect` bucket task + // Check and start a `garbageCollect` bucket task let scheduled: boolean = false; for await (const task of this.taskManager.getTasks('asc', true, [ this.tasksPath, diff --git a/src/nodes/agent/handlers/NodesClaimNetworkSign.ts b/src/nodes/agent/handlers/NodesClaimNetworkSign.ts index aac7cfd30..470885c82 100644 --- a/src/nodes/agent/handlers/NodesClaimNetworkSign.ts +++ b/src/nodes/agent/handlers/NodesClaimNetworkSign.ts @@ -18,10 +18,10 @@ class NodesClaimNetworkSign extends UnaryHandler< > { public handle = async ( input: AgentRPCRequestParams, - _cancel, + _cancel: (reason?: any) => void, meta: Record | undefined, ): Promise> => { - const { nodeManager } = this.container; + const { nodeManager }: { nodeManager: NodeManager } = this.container; // Connections should always be validated const requestingNodeId = agentUtils.nodeIdFromMeta(meta); if (requestingNodeId == null) { diff --git a/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts b/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts index f5eaa9886..fb786f5e7 100644 --- a/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts +++ b/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts @@ -18,17 +18,15 @@ class NodesClaimNetworkVerify extends UnaryHandler< > { public handle = async ( input: AgentRPCRequestParams, - _cancel, + _cancel: (reason?: any) => void, meta: Record | undefined, ): Promise> => { + const { nodeManager }: { nodeManager: NodeManager } = this.container; const requestingNodeId = agentUtils.nodeIdFromMeta(meta); if (requestingNodeId == null) { throw new agentErrors.ErrorAgentNodeIdMissing(); } - return this.container.nodeManager.handleVerifyClaimNetwork( - requestingNodeId, - input, - ); + return nodeManager.handleVerifyClaimNetwork(requestingNodeId, input); }; } diff --git a/src/nodes/agent/handlers/NodesClaimsGet.ts b/src/nodes/agent/handlers/NodesClaimsGet.ts index 6f1a9d618..287c76901 100644 --- a/src/nodes/agent/handlers/NodesClaimsGet.ts +++ b/src/nodes/agent/handlers/NodesClaimsGet.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type Sigchain from '../../../sigchain/Sigchain'; import type { AgentRPCRequestParams, @@ -22,15 +24,19 @@ class NodesClaimsGet extends ServerHandler< > { public handle = async function* ( _input: ClaimIdMessage, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - const { sigchain, db } = this.container; + const { sigchain, db }: { sigchain: Sigchain; db: DB } = this.container; yield* db.withTransactionG(async function* (tran): AsyncGenerator< AgentRPCResponseResult > { for await (const [claimId, signedClaim] of sigchain.getSignedClaims( - { /* seek: seekClaimId,*/ order: 'asc' }, + { order: 'asc' }, tran, )) { + ctx.signal.throwIfAborted(); const encodedClaim = claimsUtils.generateSignedClaim(signedClaim); const response: AgentClaimMessage = { claimIdEncoded: claimsUtils.encodeClaimId(claimId), diff --git a/src/nodes/agent/handlers/NodesClosestActiveConnectionsGet.ts b/src/nodes/agent/handlers/NodesClosestActiveConnectionsGet.ts index cee6c9b1d..e6f699fd4 100644 --- a/src/nodes/agent/handlers/NodesClosestActiveConnectionsGet.ts +++ b/src/nodes/agent/handlers/NodesClosestActiveConnectionsGet.ts @@ -1,3 +1,5 @@ +import type { ContextTimed } from '@matrixai/contexts'; +import type { JSONValue } from '@matrixai/rpc'; import type { AgentRPCRequestParams, AgentRPCResponseResult, @@ -24,10 +26,15 @@ class NodesClosestActiveConnectionsGet extends ServerHandler< > { public handle = async function* ( input: AgentRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record, + ctx: ContextTimed, ): AsyncGenerator> { - const { nodeConnectionManager } = this.container as { + const { + nodeConnectionManager, + }: { nodeConnectionManager: NodeConnectionManager; - }; + } = this.container; const { nodeId, @@ -47,6 +54,7 @@ class NodesClosestActiveConnectionsGet extends ServerHandler< const nodes = nodeConnectionManager.getClosestConnections(nodeId); for (const nodeInfo of nodes) { + ctx.signal.throwIfAborted(); yield { nodeId: nodesUtils.encodeNodeId(nodeInfo.nodeId), connections: nodeInfo.connections, diff --git a/src/nodes/agent/handlers/NodesClosestLocalNodesGet.ts b/src/nodes/agent/handlers/NodesClosestLocalNodesGet.ts index a3c49fec6..b6a5cd088 100644 --- a/src/nodes/agent/handlers/NodesClosestLocalNodesGet.ts +++ b/src/nodes/agent/handlers/NodesClosestLocalNodesGet.ts @@ -1,4 +1,6 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { DB } from '@matrixai/db'; +import type { JSONValue } from '@matrixai/rpc'; import type { AgentRPCRequestParams, AgentRPCResponseResult, @@ -31,8 +33,11 @@ class NodesClosestLocalNodesGet extends ServerHandler< > { public handle = async function* ( input: AgentRPCRequestParams, + _cancel: (reason?: any) => void, + _meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - const { nodeGraph, db } = this.container; + const { nodeGraph, db }: { nodeGraph: NodeGraph; db: DB } = this.container; const { nodeId, @@ -49,6 +54,7 @@ class NodesClosestLocalNodesGet extends ServerHandler< nodeId: input.nodeIdEncoded, }, ); + // Get all local nodes that are closest to the target node from the request return yield* db.withTransactionG(async function* (tran): AsyncGenerator< AgentRPCResponseResult @@ -59,6 +65,7 @@ class NodesClosestLocalNodesGet extends ServerHandler< tran, ); for (const [nodeId, nodeContact] of closestNodes) { + ctx.signal.throwIfAborted(); // Filter out local scoped addresses const nodeContactOutput: NodeContact = {}; for (const key of Object.keys(nodeContact)) { diff --git a/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts b/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts index 4e8b52457..1d9f18506 100644 --- a/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts +++ b/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts @@ -1,12 +1,16 @@ import type Logger from '@matrixai/logger'; +import type { JSONValue } from '@matrixai/rpc'; import type { AgentRPCRequestParams, AgentRPCResponseResult, HolePunchRequestMessage, } from '../types'; +import type { NodeId } from '../../../ids'; import type NodeConnectionManager from '../../NodeConnectionManager'; import type { Host, Port } from '../../../network/types'; import { UnaryHandler } from '@matrixai/rpc'; +import { validateSync } from '../../../validation'; +import { matchSync } from '../../../utils'; import * as keysUtils from '../../../keys/utils'; import * as ids from '../../../ids'; import * as agentErrors from '../errors'; @@ -22,13 +26,33 @@ class NodesConnectionSignalFinal extends UnaryHandler< > { public handle = async ( input: AgentRPCRequestParams, - _cancel, - meta, + _cancel: (reason?: any) => void, + meta: Record | undefined, ): Promise => { - const { nodeConnectionManager, logger } = this.container; + const { + nodeConnectionManager, + logger, + }: { + nodeConnectionManager: NodeConnectionManager; + logger: Logger; + } = this.container; // Connections should always be validated - const sourceNodeId = ids.parseNodeId(input.sourceNodeIdEncoded); - const targetNodeId = ids.parseNodeId(input.targetNodeIdEncoded); + const { + sourceNodeId, + targetNodeId, + }: { sourceNodeId: NodeId; targetNodeId: NodeId } = validateSync( + (keyPath, value) => { + return matchSync(keyPath)( + [['sourceNodeId'], () => ids.parseNodeId(value)], + [['targetNodeId'], () => ids.parseNodeId(value)], + () => value, + ); + }, + { + sourceNodeId: input.sourceNodeIdEncoded, + targetNodeId: input.targetNodeIdEncoded, + }, + ); const relayingNodeId = agentUtils.nodeIdFromMeta(meta); if (relayingNodeId == null) { throw new agentErrors.ErrorAgentNodeIdMissing(); diff --git a/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts b/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts index 3cbd7ed30..b586dc280 100644 --- a/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts +++ b/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts @@ -4,13 +4,16 @@ import type { HolePunchSignalMessage, AddressMessage, } from '../types'; +import type { NodeId } from '../../../ids'; import type NodeConnectionManager from '../../../nodes/NodeConnectionManager'; import type { Host, Port } from '../../../network/types'; import type { JSONValue } from '../../../types'; import { UnaryHandler } from '@matrixai/rpc'; +import { validateSync } from '../../../validation'; +import { matchSync } from '../../../utils'; +import { never } from '../../../utils'; import * as agentErrors from '../errors'; import * as agentUtils from '../utils'; -import { never } from '../../../utils'; import * as keysUtils from '../../../keys/utils'; import * as ids from '../../../ids'; @@ -23,16 +26,30 @@ class NodesConnectionSignalInitial extends UnaryHandler< > { public handle = async ( input: AgentRPCRequestParams, - _cancel, + _cancel: (reason?: any) => void, meta: Record | undefined, ): Promise> => { - const { nodeConnectionManager } = this.container; + const { + nodeConnectionManager, + }: { + nodeConnectionManager: NodeConnectionManager; + } = this.container; // Connections should always be validated const requestingNodeId = agentUtils.nodeIdFromMeta(meta); if (requestingNodeId == null) { throw new agentErrors.ErrorAgentNodeIdMissing(); } - const targetNodeId = ids.parseNodeId(input.targetNodeIdEncoded); + const { targetNodeId }: { targetNodeId: NodeId } = validateSync( + (keyPath, value) => { + return matchSync(keyPath)( + [['targetNodeId'], () => ids.parseNodeId(value)], + () => value, + ); + }, + { + targetNodeId: input.targetNodeIdEncoded, + }, + ); const signature = Buffer.from(input.signature, 'base64url'); // Checking signature, data is just `` concatenated const data = Buffer.concat([requestingNodeId, targetNodeId]); diff --git a/src/nodes/agent/handlers/NodesCrossSignClaim.ts b/src/nodes/agent/handlers/NodesCrossSignClaim.ts index e617c39a8..6b669e526 100644 --- a/src/nodes/agent/handlers/NodesCrossSignClaim.ts +++ b/src/nodes/agent/handlers/NodesCrossSignClaim.ts @@ -1,3 +1,4 @@ +import type { JSONValue } from '@matrixai/rpc'; import type { AgentRPCRequestParams, AgentRPCResponseResult, @@ -23,10 +24,16 @@ class NodesCrossSignClaim extends DuplexHandler< > { public handle = async function* ( input: AsyncIterableIterator>, - _cancel, - meta, + _cancel: (reason?: any) => void, + meta: Record, ): AsyncGenerator> { - const { acl, nodeManager } = this.container; + const { + acl, + nodeManager, + }: { + acl: ACL; + nodeManager: NodeManager; + } = this.container; const requestingNodeId = agentUtils.nodeIdFromMeta(meta); if (requestingNodeId == null) { throw new agentErrors.ErrorAgentNodeIdMissing(); diff --git a/src/nodes/agent/handlers/NotificationsSend.ts b/src/nodes/agent/handlers/NotificationsSend.ts index cb13081f2..33c236302 100644 --- a/src/nodes/agent/handlers/NotificationsSend.ts +++ b/src/nodes/agent/handlers/NotificationsSend.ts @@ -6,7 +6,6 @@ import type { } from '../types'; import type KeyRing from '../../../keys/KeyRing'; import type NotificationsManager from '../../../notifications/NotificationsManager'; -import type { SignedNotification } from '../../../notifications/types'; import { UnaryHandler } from '@matrixai/rpc'; import * as notificationsUtils from '../../../notifications/utils'; @@ -25,9 +24,17 @@ class NotificationsSend extends UnaryHandler< public handle = async ( input: AgentRPCRequestParams, ): Promise => { - const { db, keyRing, notificationsManager } = this.container; + const { + db, + keyRing, + notificationsManager, + }: { + db: DB; + keyRing: KeyRing; + notificationsManager: NotificationsManager; + } = this.container; const notification = await notificationsUtils.verifyAndDecodeNotif( - input.signedNotificationEncoded as SignedNotification, + input.signedNotificationEncoded, keyRing.getNodeId(), ); await db.withTransactionF((tran) => diff --git a/src/nodes/agent/handlers/VaultsGitInfoGet.ts b/src/nodes/agent/handlers/VaultsGitInfoGet.ts index 8c0518713..b13fa48ac 100644 --- a/src/nodes/agent/handlers/VaultsGitInfoGet.ts +++ b/src/nodes/agent/handlers/VaultsGitInfoGet.ts @@ -25,9 +25,9 @@ class VaultsGitInfoGet extends RawHandler<{ }> { public handle = async ( input: [JSONRPCRequest, ReadableStream], - _cancel, - meta: Record | undefined, - _ctx: ContextTimed, // TODO: use + _cancel: (reason?: any) => void, + meta: Record, + ctx: ContextTimed, ): Promise<[JSONObject, ReadableStream]> => { const { db, vaultManager, acl } = this.container; const [headerMessage, inputStream] = input; @@ -91,7 +91,12 @@ class VaultsGitInfoGet extends RawHandler<{ let handleInfoRequestGen: AsyncGenerator; const stream = new ReadableStream({ start: async () => { - handleInfoRequestGen = vaultManager.handleInfoRequest(data.vaultId); + // Automatically handle the transaction lifetime + handleInfoRequestGen = vaultManager.handleInfoRequest( + data.vaultId, + undefined, + ctx, + ); }, pull: async (controller) => { const result = await handleInfoRequestGen.next(); diff --git a/src/nodes/agent/handlers/VaultsGitPackGet.ts b/src/nodes/agent/handlers/VaultsGitPackGet.ts index dcdc6846e..2f006b8cd 100644 --- a/src/nodes/agent/handlers/VaultsGitPackGet.ts +++ b/src/nodes/agent/handlers/VaultsGitPackGet.ts @@ -1,5 +1,6 @@ import type { DB } from '@matrixai/db'; -import type { JSONObject, JSONRPCRequest } from '@matrixai/rpc'; +import type { JSONObject, JSONRPCRequest, JSONValue } from '@matrixai/rpc'; +import type { ContextTimed } from '@matrixai/contexts'; import type { VaultName } from '../../../vaults/types'; import type ACL from '../../../acl/ACL'; import type VaultManager from '../../../vaults/VaultManager'; @@ -22,8 +23,9 @@ class VaultsGitPackGet extends RawHandler<{ }> { public handle = async ( input: [JSONRPCRequest, ReadableStream], - _cancel, - meta, + _cancel: (reason: any) => void, + meta: Record, + ctx: ContextTimed, ): Promise<[JSONObject, ReadableStream]> => { const { vaultManager, acl, db } = this.container; const [headerMessage, inputStream] = input; @@ -77,7 +79,13 @@ class VaultsGitPackGet extends RawHandler<{ for await (const message of inputStream) { body.push(Buffer.from(message)); } - packRequestGen = vaultManager.handlePackRequest(vaultId, body); + // Automatically handle the transaction lifetime + packRequestGen = vaultManager.handlePackRequest( + vaultId, + body, + undefined, + ctx, + ); }, pull: async (controller) => { const next = await packRequestGen.next(); diff --git a/src/nodes/agent/handlers/VaultsScan.ts b/src/nodes/agent/handlers/VaultsScan.ts index 9d2922cfe..72a4705bc 100644 --- a/src/nodes/agent/handlers/VaultsScan.ts +++ b/src/nodes/agent/handlers/VaultsScan.ts @@ -1,10 +1,12 @@ import type { DB } from '@matrixai/db'; +import type { ContextTimed } from '@matrixai/contexts'; import type { AgentRPCRequestParams, AgentRPCResponseResult, VaultsScanMessage, } from '../types'; import type VaultManager from '../../../vaults/VaultManager'; +import type { JSONValue } from '@matrixai/rpc'; import { ServerHandler } from '@matrixai/rpc'; import * as agentErrors from '../errors'; import * as agentUtils from '../utils'; @@ -22,11 +24,13 @@ class VaultsScan extends ServerHandler< AgentRPCResponseResult > { public handle = async function* ( - input: AgentRPCRequestParams, - _cancel, - meta, + _input: AgentRPCRequestParams, + _cancel: (reason?: any) => void, + meta: Record | undefined, + ctx: ContextTimed, ): AsyncGenerator> { - const { vaultManager, db } = this.container; + const { vaultManager, db }: { vaultManager: VaultManager; db: DB } = + this.container; const requestingNodeId = agentUtils.nodeIdFromMeta(meta); if (requestingNodeId == null) { throw new agentErrors.ErrorAgentNodeIdMissing(); @@ -37,16 +41,18 @@ class VaultsScan extends ServerHandler< const listResponse = vaultManager.handleScanVaults( requestingNodeId, tran, + ctx, ); for await (const { vaultId, vaultName, vaultPermissions, } of listResponse) { + ctx.signal.throwIfAborted(); yield { vaultIdEncoded: vaultsUtils.encodeVaultId(vaultId), - vaultName, - vaultPermissions, + vaultName: vaultName, + vaultPermissions: vaultPermissions, }; } }); diff --git a/src/validation/index.ts b/src/validation/index.ts index 87164a2fa..2d29ef407 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -72,8 +72,7 @@ function validateSync( e.value = value; e.context = context; errors.push(e); - // If lazy mode, short circuit evaluation - // And throw the error up + // If lazy mode, short circuit evaluation and throw the error up if (options.mode === 'lazy') { throw e; } diff --git a/src/vaults/VaultInternal.ts b/src/vaults/VaultInternal.ts index af778590d..e73551cd1 100644 --- a/src/vaults/VaultInternal.ts +++ b/src/vaults/VaultInternal.ts @@ -1,5 +1,6 @@ -import type { ReadCommitResult } from 'isomorphic-git'; import type { EncryptedFS } from 'encryptedfs'; +import type { ReadCommitResult } from 'isomorphic-git'; +import type { ContextTimed, ContextTimedInput } from '@matrixai/contexts'; import type { DB, DBTransaction, LevelPath } from '@matrixai/db'; import type { RPCClient } from '@matrixai/rpc'; import type { ResourceAcquire, ResourceRelease } from '@matrixai/resources'; @@ -14,11 +15,11 @@ import type { VaultName, VaultRef, } from './types'; -import type KeyRing from '../keys/KeyRing'; +import type { POJO } from '../types'; import type { NodeId, NodeIdEncoded } from '../ids/types'; +import type KeyRing from '../keys/KeyRing'; import type NodeManager from '../nodes/NodeManager'; import type agentClientManifest from '../nodes/agent/callers'; -import type { POJO } from '../types'; import path from 'path'; import git from 'isomorphic-git'; import Logger from '@matrixai/logger'; @@ -26,16 +27,22 @@ import { CreateDestroyStartStop, ready, } from '@matrixai/async-init/dist/CreateDestroyStartStop'; -import { withF, withG } from '@matrixai/resources'; import { RWLockWriter } from '@matrixai/async-locks'; -import * as vaultsUtils from './utils'; +import { timedCancellable as timedCancellableF } from '@matrixai/contexts/dist/functions'; +import { + context, + timed, + timedCancellable, +} from '@matrixai/contexts/dist/decorators'; +import { withF, withG } from '@matrixai/resources'; +import { tagLast } from './types'; import * as vaultsErrors from './errors'; import * as vaultsEvents from './events'; -import { tagLast } from './types'; +import * as vaultsUtils from './utils'; import * as ids from '../ids'; +import * as utils from '../utils'; import * as nodesUtils from '../nodes/utils'; import * as gitUtils from '../git/utils'; -import * as utils from '../utils'; type RemoteInfo = { remoteNode: NodeIdEncoded; @@ -43,6 +50,7 @@ type RemoteInfo = { }; interface VaultInternal extends CreateDestroyStartStop {} + @CreateDestroyStartStop( new vaultsErrors.ErrorVaultRunning(), new vaultsErrors.ErrorVaultDestroyed(), @@ -57,44 +65,74 @@ interface VaultInternal extends CreateDestroyStartStop {} ) class VaultInternal { /** - * Creates a VaultInternal. - * If no state already exists then state for the vault is initialized. - * If state already exists then this just creates the `VaultInternal` instance for managing that state. + * Creates a VaultInternal. + * If no state already exists then a new state for the vault is initialized. + * If state already exists then this just creates the `VaultInternal` + * instance for managing that state. */ - public static async createVaultInternal({ - vaultId, - vaultName, - db, - vaultsDbPath, - keyRing, - efs, - logger = new Logger(this.name), - fresh = false, - tran, - }: { - vaultId: VaultId; - vaultName?: VaultName; - db: DB; - vaultsDbPath: LevelPath; - keyRing: KeyRing; - efs: EncryptedFS; - logger?: Logger; - fresh?: boolean; - tran?: DBTransaction; - }): Promise { + public static async createVaultInternal( + { + vaultId, + vaultName, + db, + vaultsDbPath, + keyRing, + efs, + fresh = false, + logger = new Logger(this.name), + }: { + vaultId: VaultId; + vaultName?: VaultName; + db: DB; + vaultsDbPath: LevelPath; + keyRing: KeyRing; + efs: EncryptedFS; + fresh?: boolean; + logger?: Logger; + }, + tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @timedCancellable(true) + public static async createVaultInternal( + { + vaultId, + vaultName, + db, + vaultsDbPath, + keyRing, + efs, + fresh = false, + logger = new Logger(this.name), + }: { + vaultId: VaultId; + vaultName?: VaultName; + db: DB; + vaultsDbPath: LevelPath; + keyRing: KeyRing; + efs: EncryptedFS; + fresh?: boolean; + logger?: Logger; + }, + tran: DBTransaction, + @context ctx: ContextTimed, + ): Promise { if (tran == null) { return await db.withTransactionF((tran) => - this.createVaultInternal({ - vaultId, - vaultName, - db, - vaultsDbPath, - keyRing, - efs, - logger, - fresh, + this.createVaultInternal( + { + vaultId, + vaultName, + db, + vaultsDbPath, + keyRing, + efs, + fresh, + logger, + }, tran, - }), + ctx, + ), ); } @@ -108,7 +146,7 @@ class VaultInternal { efs, logger, }); - await vault.start({ fresh, vaultName, tran }); + await vault.start({ fresh, vaultName }, tran, ctx); logger.info(`Created ${this.name} - ${vaultIdEncoded}`); return vault; } @@ -116,45 +154,77 @@ class VaultInternal { /** * Will create a new vault by cloning the vault from a remote node. */ - public static async cloneVaultInternal({ - targetNodeId, - targetVaultNameOrId, - vaultId, - db, - vaultsDbPath, - keyRing, - nodeManager, - efs, - logger = new Logger(this.name), - tran, - }: { - targetNodeId: NodeId; - targetVaultNameOrId: VaultId | VaultName; - vaultId: VaultId; - db: DB; - vaultsDbPath: LevelPath; - efs: EncryptedFS; - keyRing: KeyRing; - nodeManager: NodeManager; - logger?: Logger; - tran?: DBTransaction; - }): Promise { + public static async cloneVaultInternal( + { + targetNodeId, + targetVaultNameOrId, + vaultId, + db, + vaultsDbPath, + efs, + keyRing, + nodeManager, + logger = new Logger(this.name), + }: { + targetNodeId: NodeId; + targetVaultNameOrId: VaultId | VaultName; + vaultId: VaultId; + db: DB; + vaultsDbPath: LevelPath; + efs: EncryptedFS; + keyRing: KeyRing; + nodeManager: NodeManager; + logger?: Logger; + }, + tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @timedCancellable(true) + public static async cloneVaultInternal( + { + targetNodeId, + targetVaultNameOrId, + vaultId, + db, + vaultsDbPath, + efs, + keyRing, + nodeManager, + logger = new Logger(this.name), + }: { + targetNodeId: NodeId; + targetVaultNameOrId: VaultId | VaultName; + vaultId: VaultId; + db: DB; + vaultsDbPath: LevelPath; + efs: EncryptedFS; + keyRing: KeyRing; + nodeManager: NodeManager; + logger?: Logger; + }, + tran: DBTransaction, + @context ctx: ContextTimed, + ): Promise { if (tran == null) { return await db.withTransactionF((tran) => - this.cloneVaultInternal({ - targetNodeId, - targetVaultNameOrId, - vaultId, - db, - vaultsDbPath, - keyRing, - nodeManager, - efs, - logger, + this.cloneVaultInternal( + { + targetNodeId, + targetVaultNameOrId, + vaultId, + db, + vaultsDbPath, + efs, + keyRing, + nodeManager, + logger, + }, tran, - }), + ctx, + ), ); } + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); logger.info(`Cloning ${this.name} - ${vaultIdEncoded}`); const vault = new this({ @@ -165,35 +235,38 @@ class VaultInternal { efs, logger, }); - // Make the directory where the .git files will be auto generated and - // where the contents will be cloned to ('contents' file) + // Make the directory where the .git files will be auto generated and where + // the contents will be cloned to ('contents' file) await efs.mkdir(vault.vaultDataDir, { recursive: true }); const [vaultName, remoteVaultId]: [VaultName, VaultId] = - await nodeManager.withConnF(targetNodeId, async (connection) => { - const client = connection.getClient(); - - const [request, vaultName, remoteVaultId] = await vault.request( - client, - targetVaultNameOrId, - 'clone', - ); - await git.clone({ - fs: efs, - http: { request }, - dir: vault.vaultDataDir, - gitdir: vault.vaultGitDir, - url: 'http://', - singleBranch: true, - ref: vaultsUtils.canonicalBranchRef, - }); - return [vaultName, remoteVaultId]; - }); + await nodeManager.withConnF( + targetNodeId, + async (connection) => { + const client = connection.getClient(); + const [request, vaultName, remoteVaultId] = await vault.request( + client, + targetVaultNameOrId, + 'clone', + ); + await git.clone({ + fs: efs, + http: { request }, + dir: vault.vaultDataDir, + gitdir: vault.vaultGitDir, + url: 'http://', + singleBranch: true, + ref: vaultsUtils.canonicalBranchRef, + }); + return [vaultName, remoteVaultId]; + }, + ctx, + ); const remote: RemoteInfo = { remoteNode: nodesUtils.encodeNodeId(targetNodeId), remoteVault: vaultsUtils.encodeVaultId(remoteVaultId), }; - await vault.start({ vaultName, tran }); + await vault.start({ vaultName }, tran, ctx); // Setting the remote in the metadata await tran.put( [...vault.vaultMetadataDbPath, VaultInternal.remoteKey], @@ -254,37 +327,57 @@ class VaultInternal { } /** - * - * @param fresh Clears all state before starting - * @param vaultName Name of the vault, Only used when creating a new vault + * @param fresh Should the state be cleared before starting? + * @param vaultName Name of the vault. Only used when creating a new vault. * @param tran + * @param ctx */ - public async start({ - fresh = false, - vaultName, - tran, - }: { - fresh?: boolean; - vaultName?: VaultName; - tran?: DBTransaction; - } = {}): Promise { + public async start( + { + vaultName, + fresh = false, + }: { + vaultName?: VaultName; + fresh?: boolean; + } = {}, + tran?: DBTransaction, + ctx?: ContextTimed, + ): Promise { if (tran == null) { return await this.db.withTransactionF((tran) => - this.start_(fresh, tran, vaultName), + this.start_({ vaultName, fresh }, tran, ctx), ); } - return await this.start_(fresh, tran, vaultName); + return await this.start_({ vaultName, fresh }, tran, ctx); } /** - * We use a protected start method to avoid the `async-init` lifecycle deadlocking when doing the recursive call to - * create a DBTransaction. + * We use a protected start method to avoid the `async-init` lifecycle + * deadlocking when doing the recursive call to create a DBTransaction. */ protected async start_( - fresh: boolean, + { + vaultName, + fresh, + }: { + vaultName?: VaultName; + fresh: boolean; + }, + tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @timedCancellable(true) + protected async start_( + { + vaultName, + fresh, + }: { + vaultName?: VaultName; + fresh: boolean; + }, tran: DBTransaction, - vaultName?: VaultName, - ) { + @context ctx: ContextTimed, + ): Promise { this.logger.info( `Starting ${this.constructor.name} - ${this.vaultIdEncoded}`, ); @@ -305,8 +398,8 @@ class VaultInternal { await vaultsUtils.mkdirExists(this.efs, this.vaultIdEncoded); await vaultsUtils.mkdirExists(this.efs, this.vaultDataDir); await vaultsUtils.mkdirExists(this.efs, this.vaultGitDir); - await this.setupMeta({ vaultName, tran }); - await this.setupGit(tran); + await this.setupMeta({ vaultName }, tran); + await this.setupGit(tran, ctx); this.efsVault = await this.efs.chroot(this.vaultDataDir); this.logger.info( `Started ${this.constructor.name} - ${this.vaultIdEncoded}`, @@ -330,10 +423,10 @@ class VaultInternal { } /** - * We use a protected destroy method to avoid the `async-init` lifecycle deadlocking when doing the recursive call to - * create a DBTransaction. + * We use a protected start method to avoid the `async-init` lifecycle + * deadlocking when doing the recursive call to create a DBTransaction. */ - protected async destroy_(tran: DBTransaction) { + protected async destroy_(tran: DBTransaction): Promise { this.logger.info( `Destroying ${this.constructor.name} - ${this.vaultIdEncoded}`, ); @@ -344,17 +437,23 @@ class VaultInternal { }); } catch (e) { if (e.code !== 'ENOENT') throw e; - // Otherwise ignore } this.logger.info( `Destroyed ${this.constructor.name} - ${this.vaultIdEncoded}`, ); } + public async log( + ref?: string | VaultRef, + limit?: number, + ctx?: Partial, + ): Promise>; @ready(new vaultsErrors.ErrorVaultNotRunning()) + @timedCancellable(true) public async log( ref: string | VaultRef = 'HEAD', - limit?: number, + limit: number, + @context ctx: ContextTimed, ): Promise> { vaultsUtils.assertRef(ref); if (ref === vaultsUtils.tagLast) { @@ -364,10 +463,11 @@ class VaultInternal { fs: this.efs, dir: this.vaultDataDir, gitdir: this.vaultGitDir, - ref, + ref: ref, depth: limit, }); return commits.map(({ oid, commit }: ReadCommitResult) => { + ctx.signal.throwIfAborted(); return { commitId: oid as CommitId, parent: commit.parent as Array, @@ -385,8 +485,8 @@ class VaultInternal { } /** - * Checks out the vault repository to specific commit ID or special tags - * This changes the working directory and updates the HEAD reference + * Checks out the vault repository to specific commit ID or special tags. + * This changes the working directory and updates the HEAD reference. */ @ready(new vaultsErrors.ErrorVaultNotRunning()) public async version(ref: string | VaultRef = tagLast): Promise { @@ -399,7 +499,7 @@ class VaultInternal { fs: this.efs, dir: this.vaultDataDir, gitdir: this.vaultGitDir, - ref, + ref: ref, force: true, }); } catch (e) { @@ -426,7 +526,8 @@ class VaultInternal { } /** - * With context handler for using a vault in a read-only context for a generator. + * With context handler for using a vault in a read-only context for a + * generator. */ @ready(new vaultsErrors.ErrorVaultNotRunning()) public readG( @@ -441,13 +542,20 @@ class VaultInternal { /** * With context handler for using a vault in a writable context. */ - @ready(new vaultsErrors.ErrorVaultNotRunning()) public async writeF( f: (fs: FileSystemWritable) => Promise, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultNotRunning()) + @timedCancellable(true) + public async writeF( + f: (fs: FileSystemWritable) => Promise, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { - return this.db.withTransactionF((tran) => this.writeF(f, tran)); + return this.db.withTransactionF((tran) => this.writeF(f, tran, ctx)); } return withF([this.lock.write()], async () => { @@ -455,10 +563,9 @@ class VaultInternal { [...this.vaultMetadataDbPath, VaultInternal.dirtyKey].join(''), ); - // This should really be an internal property - // get whether this is remote, and the remote address - // if it is, we consider this repo an "attached repo" - // this vault is a "mirrored" vault + // This should really be an internal property. Check whether this is the + // remote address. If it is, we consider this repo an "attached repo". + // This vault is a "mirrored" vault. if ( (await tran.get([ ...this.vaultMetadataDbPath, @@ -475,10 +582,10 @@ class VaultInternal { try { await f(this.efsVault); // After doing mutation we need to commit the new history - await this.createCommit(); + await this.createCommit(ctx); } catch (e) { // Error implies dirty state - await this.cleanWorkingDirectory(); + await this.cleanWorkingDirectory(ctx); throw e; } await tran.put( @@ -491,19 +598,27 @@ class VaultInternal { /** * With context handler for using a vault in a writable context for a generator. */ - @ready(new vaultsErrors.ErrorVaultNotRunning()) public writeG( g: (fs: FileSystemWritable) => AsyncGenerator, tran?: DBTransaction, + ctx?: Partial, + ): AsyncGenerator; + @ready(new vaultsErrors.ErrorVaultNotRunning()) + @timed() + public writeG( + g: (fs: FileSystemWritable) => AsyncGenerator, + tran: DBTransaction, + @context ctx: ContextTimed, ): AsyncGenerator { if (tran == null) { - return this.db.withTransactionG((tran) => this.writeG(g, tran)); + return this.db.withTransactionG((tran) => this.writeG(g, tran, ctx)); } const efsVault = this.efsVault; const vaultMetadataDbPath = this.vaultMetadataDbPath; - const createCommit = () => this.createCommit(); - const cleanWorkingDirectory = () => this.cleanWorkingDirectory(); + // In AsyncGenerators, "this" refers to the generator itself, so we alias + // "this" and use the alias to access protected methods. + const parentThis = this; return withG([this.lock.write()], async function* () { if ( (await tran.get([...vaultMetadataDbPath, VaultInternal.remoteKey])) != @@ -517,19 +632,16 @@ class VaultInternal { ); await tran.put([...vaultMetadataDbPath, VaultInternal.dirtyKey], true); + // Create the commit let result: TReturn; - // Do what you need to do here, create the commit try { result = yield* g(efsVault); - // At the end of the generator - // you need to do this - // but just before - // you need to finish it up - // After doing mutation we need to commit the new history - await createCommit(); + // After doing mutation we need to commit the new history. You need to + // do this at the end of the generator. + await parentThis.createCommit(ctx); } catch (e) { // Error implies dirty state - await cleanWorkingDirectory(); + await parentThis.cleanWorkingDirectory(ctx); throw e; } await tran.put([...vaultMetadataDbPath, VaultInternal.dirtyKey], false); @@ -538,7 +650,7 @@ class VaultInternal { } /** - * Acquire a read-only lock on this vault + * Acquire a read-only lock on this vault. */ @ready(new vaultsErrors.ErrorVaultNotRunning()) public acquireRead(): ResourceAcquire { @@ -555,92 +667,131 @@ class VaultInternal { } /** - * Acquire a read-write lock on this vault + * Acquire a read-write lock on this vault. */ @ready(new vaultsErrors.ErrorVaultNotRunning()) public acquireWrite( tran?: DBTransaction, + ctx?: ContextTimed, ): ResourceAcquire { return async () => { let releaseTran: ResourceRelease | undefined = undefined; const acquire = this.lock.write(); const [release] = await acquire(); + + const acquireF = async ( + ctx: ContextTimed, + ): Promise<[(e?: Error) => Promise, EncryptedFS]> => { + if (tran == null) { + utils.never( + 'This method should not be called without a valid transaction', + ); + } + await tran.lock( + [...this.vaultMetadataDbPath, VaultInternal.dirtyKey].join(''), + ); + if ( + (await tran.get([ + ...this.vaultMetadataDbPath, + VaultInternal.remoteKey, + ])) != null + ) { + // Mirrored vaults are immutable + throw new vaultsErrors.ErrorVaultRemoteDefined(); + } + await tran.put( + [...this.vaultMetadataDbPath, VaultInternal.dirtyKey], + true, + ); + return [ + async (e?: Error) => { + if (e == null) { + try { + // After doing mutation we need to commit the new history + await this.createCommit(ctx); + } catch (e_) { + e = e_; + // Error implies dirty state + await this.cleanWorkingDirectory(ctx); + } + } + // For some reason, the transaction type doesn't properly waterfall + // down to here. + await tran!.put( + [...this.vaultMetadataDbPath, VaultInternal.dirtyKey], + false, + ); + if (releaseTran != null) await releaseTran(e); + await release(e); + }, + this.efsVault, + ]; + }; + if (tran == null) { const acquireTran = this.db.transaction(); [releaseTran, tran] = await acquireTran(); } - // The returned transaction can be undefined, too. We won't handle those - // cases. + // The returned transaction can be undefined. We won't handle those cases. if (tran == null) utils.never('Acquired transactions cannot be null'); - await tran.lock( - [...this.vaultMetadataDbPath, VaultInternal.dirtyKey].join(''), - ); - if ( - (await tran.get([ - ...this.vaultMetadataDbPath, - VaultInternal.remoteKey, - ])) != null - ) { - // Mirrored vaults are immutable - throw new vaultsErrors.ErrorVaultRemoteDefined(); + + // TODO: check if this works + if (ctx == null) { + const fTimedCancellable = timedCancellableF(acquireF, true); + return fTimedCancellable(); + } else { + return acquireF(ctx); } - await tran.put( - [...this.vaultMetadataDbPath, VaultInternal.dirtyKey], - true, - ); - return [ - async (e?: Error) => { - if (e == null) { - try { - // After doing mutation we need to commit the new history - await this.createCommit(); - } catch (e_) { - e = e_; - // Error implies dirty state - await this.cleanWorkingDirectory(); - } - } - // For some reason, the transaction type doesn't properly waterfall - // down to here. - await tran!.put( - [...this.vaultMetadataDbPath, VaultInternal.dirtyKey], - false, - ); - if (releaseTran != null) await releaseTran(e); - await release(e); - }, - this.efsVault, - ]; }; } /** - * Pulls changes to a vault from the vault's default remote. - * If `pullNodeId` and `pullVaultNameOrId` it uses that for the remote instead. + * Pulls changes to a vault from the vault's default remote. If `pullNodeId` + * and `pullVaultNameOrId` it uses that for the remote instead. */ @ready(new vaultsErrors.ErrorVaultNotRunning()) - public async pullVault({ - nodeManager, - pullNodeId, - pullVaultNameOrId, - tran, - }: { - nodeManager: NodeManager; - pullNodeId?: NodeId; - pullVaultNameOrId?: VaultId | VaultName; - tran?: DBTransaction; - }): Promise { + public async pullVault( + { + nodeManager, + pullNodeId, + pullVaultNameOrId, + }: { + nodeManager: NodeManager; + pullNodeId?: NodeId; + pullVaultNameOrId?: VaultId | VaultName; + }, + tran?: DBTransaction, + ctx?: ContextTimed, + ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.pullVault({ - nodeManager, - pullNodeId, - pullVaultNameOrId, + this.pullVault( + { + nodeManager, + pullNodeId, + pullVaultNameOrId, + }, tran, - }), + ctx, + ), ); } + if (ctx == null) { + const f = async (ctx: ContextTimed) => { + return this.pullVault( + { + nodeManager, + pullNodeId, + pullVaultNameOrId, + }, + tran, + ctx, + ); + }; + return timedCancellableF(f, true)(); + } + // Keeps track of whether the metadata needs changing to avoid unnecessary db ops // 0 = no change, 1 = change with vault ID, 2 = change with vault name let metaChange = 0; @@ -693,17 +844,16 @@ class VaultInternal { singleBranch: true, fastForward: true, fastForwardOnly: true, - author: { - name: nodesUtils.encodeNodeId(pullNodeId!), - }, + author: { name: nodesUtils.encodeNodeId(pullNodeId!) }, }); }); return remoteVaultId; }, + // Ctx, ); } catch (e) { - // If the error flag set, and we have the generalised SmartHttpError from - // isomorphic git then we need to throw the polykey error + // If the error flag is set, and we have the generalised SmartHttpError from + // isomorphic git, then we need to throw the Polykey error. if (e instanceof git.Errors.MergeNotSupportedError) { throw new vaultsErrors.ErrorVaultsMergeConflict(e.message, { cause: e, @@ -732,13 +882,10 @@ class VaultInternal { * Creates a `dirty` boolean in the database to track dirty state of the vault. * Also adds the vault's name to the database. */ - protected async setupMeta({ - vaultName, - tran, - }: { - vaultName?: VaultName; - tran: DBTransaction; - }): Promise { + protected async setupMeta( + { vaultName }: { vaultName?: VaultName }, + tran: DBTransaction, + ): Promise { // Set up dirty key defaulting to false if ( (await tran.get([ @@ -767,17 +914,20 @@ class VaultInternal { } // Dirty: boolean - // name: string | undefined + // Name: string | undefined } /** * Does an idempotent initialization of the git repository for the vault. * If the vault is in a dirty state then we clean up the working directory - * or any history not part of the canonicalBranch. + * or any history not part of the canonical branch. */ - protected async setupGit(tran: DBTransaction): Promise { - // Initialization is idempotent - // It works even with an existing git repository + protected async setupGit( + tran: DBTransaction, + ctx: ContextTimed, + ): Promise { + // Initialization is idempotent. It works even with an existing git + // repository. await git.init({ fs: this.efs, dir: this.vaultDataDir, @@ -795,8 +945,8 @@ class VaultInternal { }); commitIdLatest = commits[0]?.oid as CommitId | undefined; } catch (e) { - // Initialized repositories do not have any commits - // It complains that `refs/heads/master` file does not exist + // Initialized repositories do not have any commits. It complains that + // `refs/heads/master` file does not exist. if (!(e instanceof git.Errors.NotFoundError)) { throw e; } @@ -821,18 +971,18 @@ class VaultInternal { force: true, }); } else { - // Checking for dirty + // Checking for dirty state if ( (await tran.get([ ...this.vaultMetadataDbPath, VaultInternal.dirtyKey, ])) === true ) { - // Force checkout out to the latest commit - // This ensures that any uncommitted state is dropped - await this.cleanWorkingDirectory(); - // Do global GC operation - await this.garbageCollectGitObjectsGlobal(); + // Force checkout out to the latest commit. This ensures that any + // uncommitted state is dropped. A global garbage collection is + // executed immediately after. + await this.cleanWorkingDirectory(ctx); + await this.garbageCollectGitObjectsGlobal(ctx); // Setting dirty back to false await tran.put( @@ -845,19 +995,20 @@ class VaultInternal { } /** - * Creates a request arrow function that implements an api that `isomorphic-git` expects to use when making a http - * request. It makes RPC calls to `vaultsGitInfoGet` for the ref advertisement phase and `vaultsGitPackGet` for the - * git pack phase. + * Creates a request arrow function that implements an API that `isomorphic-git` + * expects to use when making a HTTP request. It makes RPC calls to + * `vaultsGitInfoGet` for the ref advertisement phase and `vaultsGitPackGet` + * for the git pack phase. * - * `vaultsGitInfoGet` wraps a call to `gitHttp.advertiseRefGenerator` and `vaultsGitPackGet` to - * `gitHttp.generatePackRequest`. + * `vaultsGitInfoGet` wraps a call to `gitHttp.advertiseRefGenerator` and + * `vaultsGitPackGet` to `gitHttp.generatePackRequest`. * * ``` * ┌─────────┐ ┌───────────────────────────┐ * │ │ │ │ * ┌──────────────────────┐ │ RPC │ │ │ * │ │ │ │ │ *advertiseRefGenerator() │ - * │ ├────────┼─────────┼────► │ + * │ ├────────┼─────────┼────▶ │ * │ vault.request() │ │ │ │ │ * │ │ │ │ └────┬──────────────────────┘ * │ ├──┐ │ │ │ @@ -913,7 +1064,7 @@ class VaultInternal { const vaultsGitPackGetStream = await client.methods.vaultsGitPackGet({ nameOrId: result.vaultIdEncoded as string, - vaultAction, + vaultAction: vaultAction, }); return [ @@ -964,11 +1115,12 @@ class VaultInternal { /** * Creates a commit while moving the canonicalBranch reference to that new commit. - * If the commit creates a branch from the canonical history. Then the new commit becomes the new canonical history - * and the old history is removed from the old canonical head to the branch point. This is to maintain the strict + * If the commit creates a branch from the canonical history. Then the new commit + * becomes the new canonical history and the old history is removed from the old + * canonical head to the branch point. This is to maintain the strict * non-branching linear history. */ - protected async createCommit() { + protected async createCommit(ctx: ContextTimed): Promise { // Forced wait for 1 ms to allow difference in mTime between file changes await utils.sleep(1); // Checking if commit is appending or branching @@ -1006,27 +1158,30 @@ class VaultInternal { workingDirStatus, stageStatus, ] of statusMatrix) { - /* - Type StatusRow = [Filename, HeadStatus, WorkdirStatus, StageStatus] - The HeadStatus status is either absent (0) or present (1). - The WorkdirStatus status is either absent (0), identical to HEAD (1), or different from HEAD (2). - The StageStatus status is either absent (0), identical to HEAD (1), identical to WORKDIR (2), or different from WORKDIR (3). - - ```js - // example StatusMatrix - [ - ["a.txt", 0, 2, 0], // new, untracked - ["b.txt", 0, 2, 2], // added, staged - ["c.txt", 0, 2, 3], // added, staged, with unstaged changes - ["d.txt", 1, 1, 1], // unmodified - ["e.txt", 1, 2, 1], // modified, unstaged - ["f.txt", 1, 2, 2], // modified, staged - ["g.txt", 1, 2, 3], // modified, staged, with unstaged changes - ["h.txt", 1, 0, 1], // deleted, unstaged - ["i.txt", 1, 0, 0], // deleted, staged - ] - ``` + /** + * Type StatusRow = [Filename, HeadStatus, WorkdirStatus, StageStatus]. + * The HeadStatus status is either absent (0) or present (1). + * The WorkdirStatus status is either absent (0), identical to HEAD (1), + * or different from HEAD (2). + * The StageStatus status is either absent (0), identical to HEAD (1), + * identical to WORKDIR (2), or different from WORKDIR (3). + * + * ```js + * // Example StatusMatrix + * [ + * ["a.txt", 0, 2, 0], // new, untracked + * ["b.txt", 0, 2, 2], // added, staged + * ["c.txt", 0, 2, 3], // added, staged, unstaged changes + * ["d.txt", 1, 1, 1], // unmodified + * ["e.txt", 1, 2, 1], // modified, unstaged + * ["f.txt", 1, 2, 2], // modified, staged + * ["g.txt", 1, 2, 3], // modified, unstaged, unstaged changes + * ["h.txt", 1, 0, 1], // deleted, unstaged + * ["i.txt", 1, 0, 0], // deleted, staged + * ] + * ``` */ + ctx.signal.throwIfAborted(); const status = `${HEADStatus}${workingDirStatus}${stageStatus}`; switch (status) { case '022': // Added, staged @@ -1037,20 +1192,21 @@ class VaultInternal { case '122': // Modified, staged message.push(`${filePath} modified`); break; - case '101': // Deleted, unStaged - // need to stage the deletion with remove + case '101': // Deleted, unstaged + // Need to stage the deletion with remove await git.remove({ fs: this.efs, dir: this.vaultDataDir, gitdir: this.vaultGitDir, filepath: filePath, }); - // Fall through + // Fallthrough case '100': // Deleted, staged message.push(`${filePath} deleted`); break; default: - // We don't handle untracked and partially staged files since we add all files to staging before processing + // We don't handle untracked and partially staged files since we add + // all files to staging before processing. utils.never( `Status ${status} is unhandled because it was unexpected state`, ); @@ -1063,9 +1219,7 @@ class VaultInternal { fs: this.efs, dir: this.vaultDataDir, gitdir: this.vaultGitDir, - author: { - name: nodeIdEncoded, - }, + author: { name: nodeIdEncoded }, message: message.toString(), ref: 'HEAD', }); @@ -1080,17 +1234,18 @@ class VaultInternal { }); // We clean old history if a commit was made on previous version if (headRef !== masterRef) { - await this.garbageCollectGitObjectsLocal(masterRef, headRef); + await this.garbageCollectGitObjectsLocal(masterRef, headRef, ctx); } } } /** - * Cleans the git working directory by checking out the canonicalBranch. - * This will remove any un-committed changes since any untracked or modified files outside a commit is dirty state. - * Dirty state should only happen if the usual commit procedure was interrupted ungracefully. + * Cleans the git working directory by checking out the canonical branch. + * This will remove any un-committed changes since any untracked or modified + * files outside a commit is dirty state. Dirty state should only happen if + * the usual commit procedure was interrupted ungracefully. */ - protected async cleanWorkingDirectory() { + protected async cleanWorkingDirectory(ctx: ContextTimed): Promise { // Check the status matrix for any un-staged file changes // which are considered dirty commits const statusMatrix = await git.statusMatrix({ @@ -1099,8 +1254,9 @@ class VaultInternal { gitdir: this.vaultGitDir, }); for await (const [filePath, , workingDirStatus] of statusMatrix) { - // For all files stage all changes, this is needed - // so that we can check out all untracked files as well + ctx.signal.throwIfAborted(); + // Stage all changes across all files. This is needed so that we can + // checkout all untracked files as well. if (workingDirStatus === 0) { await git.remove({ fs: this.efs, @@ -1128,14 +1284,20 @@ class VaultInternal { } /** - * This will walk the current canonicalBranch history and delete any objects that are not a part of it. - * This is costly since it will compare the walked tree with all existing objects. + * This will walk the current canonical branch history and delete any objects + * that are not a part of it. This is costly since it will compare the walked + * tree with all existing objects. */ - protected async garbageCollectGitObjectsGlobal() { - const objectIdsAll = await gitUtils.listObjectsAll({ - fs: this.efs, - gitDir: this.vaultGitDir, - }); + protected async garbageCollectGitObjectsGlobal( + ctx: ContextTimed, + ): Promise { + const objectIdsAll = await gitUtils.listObjectsAll( + { + fs: this.efs, + gitDir: this.vaultGitDir, + }, + ctx, + ); const objects = new Set(objectIdsAll); const masterRef = await git.resolveRef({ fs: this.efs, @@ -1143,19 +1305,22 @@ class VaultInternal { gitdir: this.vaultGitDir, ref: vaultsUtils.canonicalBranch, }); - const reachableObjects = await gitUtils.listObjects({ - efs: this.efs, - dir: this.vaultDataDir, - gitDir: this.vaultGitDir, - wants: [masterRef], - haves: [], - }); + const reachableObjects = await gitUtils.listObjects( + { + efs: this.efs, + dir: this.vaultDataDir, + gitDir: this.vaultGitDir, + wants: [masterRef], + haves: [], + }, + ctx, + ); // Walk from head to all reachable objects for (const objectReachable of reachableObjects) { objects.delete(objectReachable); } - // Any objects left in `objects` was unreachable, thus they are a part of orphaned branches - // So we want to delete them. + // Any objects left in `objects` was unreachable, thus they are a part of + // orphaned branches, so we want to delete them. const deletePs: Array> = []; for (const objectId of objects) { deletePs.push( @@ -1166,20 +1331,25 @@ class VaultInternal { } /** - * This will walk from the `startId` to the `StopId` deleting objects as it goes. - * This is smarter since it only walks over the old history and not everything. + * This will walk from the `startId` to the `StopId` deleting objects as it + * goes. This is smarter since it only walks over the old history and not + * everything. */ protected async garbageCollectGitObjectsLocal( startId: string, stopId: string, - ) { - const objects = await gitUtils.listObjects({ - efs: this.efs, - dir: this.vaultDataDir, - gitDir: this.vaultGitDir, - wants: [startId], - haves: [stopId], - }); + ctx: ContextTimed, + ): Promise { + const objects = await gitUtils.listObjects( + { + efs: this.efs, + dir: this.vaultDataDir, + gitDir: this.vaultGitDir, + wants: [startId], + haves: [stopId], + }, + ctx, + ); const deletePs: Array> = []; for (const objectId of objects) { deletePs.push( diff --git a/src/vaults/VaultManager.ts b/src/vaults/VaultManager.ts index 30a7183bd..1928c8322 100644 --- a/src/vaults/VaultManager.ts +++ b/src/vaults/VaultManager.ts @@ -1,3 +1,5 @@ +import type { LockRequest } from '@matrixai/async-locks'; +import type { ContextTimed, ContextTimedInput } from '@matrixai/contexts'; import type { DBTransaction, LevelPath } from '@matrixai/db'; import type { VaultId, @@ -17,12 +19,10 @@ import type NotificationsManager from '../notifications/NotificationsManager'; import type ACL from '../acl/ACL'; import type { RemoteInfo } from './VaultInternal'; import type { VaultAction } from './types'; -import type { LockRequest } from '@matrixai/async-locks'; import type { Key } from '../keys/types'; import path from 'path'; import { DB } from '@matrixai/db'; import { EncryptedFS, errors as encryptedFsErrors } from 'encryptedfs'; -import Logger from '@matrixai/logger'; import { CreateDestroyStartStop, ready, @@ -30,19 +30,25 @@ import { import { IdInternal } from '@matrixai/id'; import { withF, withG } from '@matrixai/resources'; import { LockBox, RWLockWriter } from '@matrixai/async-locks'; +import { + context, + timedCancellable, + timed, +} from '@matrixai/contexts/dist/decorators'; +import Logger from '@matrixai/logger'; import VaultInternal from './VaultInternal'; import * as vaultsEvents from './events'; import * as vaultsUtils from './utils'; import * as vaultsErrors from './errors'; +import config from '../config'; +import { mkdirExists } from '../utils/utils'; import * as utils from '../utils'; import * as gitHttp from '../git/http'; import * as nodesUtils from '../nodes/utils'; import * as keysUtils from '../keys/utils'; -import config from '../config'; -import { mkdirExists } from '../utils/utils'; /** - * Object map pattern for each vault + * Object map pattern for each vault. */ type VaultMap = Map; @@ -54,6 +60,7 @@ type VaultMetadata = { }; interface VaultManager extends CreateDestroyStartStop {} + @CreateDestroyStartStop( new vaultsErrors.ErrorVaultManagerRunning(), new vaultsErrors.ErrorVaultManagerDestroyed(), @@ -89,7 +96,7 @@ class VaultManager { fs?: FileSystem; logger?: Logger; fresh?: boolean; - }) { + }): Promise { logger.info(`Creating ${this.name}`); logger.info(`Setting vaults path to ${vaultsPath}`); const vaultManager = new this({ @@ -183,7 +190,6 @@ class VaultManager { let efs: EncryptedFS; try { efsDb = await DB.createDB({ - fresh, crypto: { key: vaultKey, ops: { @@ -203,11 +209,12 @@ class VaultManager { }, dbPath: this.efsPath, logger: this.logger.getChild('EFS Database'), + fresh: fresh, }); efs = await EncryptedFS.createEncryptedFS({ - fresh, db: efsDb, logger: this.logger.getChild('EncryptedFileSystem'), + fresh: fresh, }); } catch (e) { if (e instanceof encryptedFsErrors.ErrorEncryptedFSKey) { @@ -230,7 +237,7 @@ class VaultManager { this.efs = efs; this.logger.info(`Started ${this.constructor.name}`); } catch (e) { - this.logger.warn(`Failed Starting ${this.constructor.name}`); + this.logger.warn(`Failed starting ${this.constructor.name}`); await this.efs?.stop(); await this.efsDb?.stop(); throw e; @@ -240,9 +247,8 @@ class VaultManager { public async stop(): Promise { this.logger.info(`Stopping ${this.constructor.name}`); - - // Iterate over vaults in memory and destroy them, ensuring that - // the working directory commit state is saved + // Iterate over vaults in memory and destroy them, ensuring that the working + // directory commit state is saved. const promises: Array> = []; for (const vaultIdString of this.vaultMap.keys()) { const vaultId = IdInternal.fromString(vaultIdString); @@ -268,7 +274,6 @@ class VaultManager { public async destroy(): Promise { this.logger.info(`Destroying ${this.constructor.name}`); await this.efsDb.start({ - fresh: false, crypto: { key: this.vaultKey, ops: { @@ -286,6 +291,7 @@ class VaultManager { }, }, }, + fresh: false, }); await this.efs.destroy(); await this.efsDb.stop(); @@ -309,17 +315,23 @@ class VaultManager { } /** - * Constructs a new vault instance with a given name and - * stores it in memory + * Constructs a new Vault instance with a given name and stores it in memory. */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async createVault( vaultName: VaultName, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timedCancellable(true) + public async createVault( + vaultName: VaultName, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.createVault(vaultName, tran), + this.createVault(vaultName, tran, ctx), ); } // Adding vault to name map @@ -343,17 +355,20 @@ class VaultManager { [vaultId.toString(), RWLockWriter, 'write'], async () => { // Creating vault - const vault = await VaultInternal.createVaultInternal({ - vaultId, - vaultName, - keyRing: this.keyRing, - efs: this.efs, - logger: this.logger.getChild(VaultInternal.name), - db: this.db, - vaultsDbPath: this.vaultsDbPath, - fresh: true, + const vault = await VaultInternal.createVaultInternal( + { + vaultId: vaultId, + vaultName: vaultName, + keyRing: this.keyRing, + efs: this.efs, + db: this.db, + vaultsDbPath: this.vaultsDbPath, + fresh: true, + logger: this.logger.getChild(VaultInternal.name), + }, tran, - }); + ctx, + ); // Adding vault to object map this.vaultMap.set(vaultIdString, vault); return vault.vaultId; @@ -362,8 +377,8 @@ class VaultManager { } /** - * Retrieves the vault metadata using the VaultId - * and parses it to return the associated vault name + * Retrieves the vault metadata using the VaultId and parses it to return the + * associated vault name. */ @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async getVaultMeta( @@ -402,17 +417,23 @@ class VaultManager { } /** - * Removes the metadata and EFS state of a vault using a - * given VaultId + * Removes the metadata and EFS state of a vault using a given VaultId. */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async destroyVault( vaultId: VaultId, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timedCancellable(true) + public async destroyVault( + vaultId: VaultId, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.destroyVault(vaultId, tran), + this.destroyVault(vaultId, tran, ctx), ); } @@ -433,7 +454,7 @@ class VaultManager { `Destroying Vault ${vaultsUtils.encodeVaultId(vaultId)}`, ); const vaultIdString = vaultId.toString() as VaultIdString; - const vault = await this.getVault(vaultId, tran); + const vault = await this.getVault(vaultId, tran, ctx); // Destroying vault state and metadata await vault.stop(); await vault.destroy(tran); @@ -447,15 +468,24 @@ class VaultManager { } /** - * Removes vault from the vault map + * Removes a vault from the vault map. */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async closeVault( vaultId: VaultId, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timedCancellable(true) + public async closeVault( + vaultId: VaultId, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { - return this.db.withTransactionF((tran) => this.closeVault(vaultId, tran)); + return this.db.withTransactionF((tran) => + this.closeVault(vaultId, tran, ctx), + ); } if ((await this.getVaultName(vaultId, tran)) == null) { @@ -466,7 +496,7 @@ class VaultManager { [vaultId.toString(), RWLockWriter, 'write'], async () => { await tran.lock([...this.vaultsDbPath, vaultId].join('')); - const vault = await this.getVault(vaultId, tran); + const vault = await this.getVault(vaultId, tran, ctx); await vault.stop(); this.vaultMap.delete(vaultIdString); }, @@ -474,13 +504,20 @@ class VaultManager { } /** - * Lists the vault name and associated VaultId of all - * the vaults stored + * Lists the vault name and associated VaultId of all the stored vaults. */ + public async listVaults( + ctx?: Partial, + tran?: DBTransaction, + ): Promise; @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) - public async listVaults(tran?: DBTransaction): Promise { + @timedCancellable(true) + public async listVaults( + @context ctx: ContextTimed, + tran?: DBTransaction, + ): Promise { if (tran == null) { - return this.db.withTransactionF((tran) => this.listVaults(tran)); + return this.db.withTransactionF((tran) => this.listVaults(ctx, tran)); } const vaults: VaultList = new Map(); @@ -488,6 +525,7 @@ class VaultManager { for await (const [vaultNameBuffer, vaultIdBuffer] of tran.iterator( this.vaultsNamesDbPath, )) { + if (ctx.signal.aborted) throw ctx.signal.reason; const vaultName = vaultNameBuffer.toString() as VaultName; const vaultId = IdInternal.fromBuffer(vaultIdBuffer); vaults.set(vaultName, vaultId); @@ -496,17 +534,25 @@ class VaultManager { } /** - * Changes the vault name metadata of a VaultId + * Changes the vault name metadata of a VaultId. */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async renameVault( vaultId: VaultId, newVaultName: VaultName, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timedCancellable(true) + public async renameVault( + vaultId: VaultId, + newVaultName: VaultName, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.renameVault(vaultId, newVaultName, tran), + this.renameVault(vaultId, newVaultName, tran, ctx), ); } @@ -556,7 +602,7 @@ class VaultManager { } /** - * Retrieves the VaultId associated with a vault name + * Retrieves the VaultId associated with a vault name. */ @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async getVaultId( @@ -579,7 +625,7 @@ class VaultManager { } /** - * Retrieves the vault name associated with a VaultId + * Retrieves the vault name associated with a VaultId. */ @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async getVaultName( @@ -619,8 +665,8 @@ class VaultManager { } /** - * Sets clone, pull and scan permissions of a vault for a - * gestalt and send a notification to this gestalt + * Sets clone, pull and scan permissions of a vault for a gestalt and send a + * notification to this gestalt. */ @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async shareVault( @@ -636,13 +682,12 @@ class VaultManager { const vaultMeta = await this.getVaultMeta(vaultId, tran); if (vaultMeta == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); - // NodeId permissions translated to other nodes in - // a gestalt by other domains + // NodeId permissions translated to other nodes in a gestalt by other domains await this.gestaltGraph.setGestaltAction(['node', nodeId], 'scan', tran); await this.acl.setVaultAction(vaultId, nodeId, 'pull', tran); await this.acl.setVaultAction(vaultId, nodeId, 'clone', tran); await this.notificationsManager.sendNotification({ - nodeId, + nodeId: nodeId, data: { type: 'VaultShare', vaultId: vaultsUtils.encodeVaultId(vaultId), @@ -656,8 +701,7 @@ class VaultManager { } /** - * Unsets clone, pull and scan permissions of a vault for a - * gestalt + * Unsets clone, pull and scan permissions of a vault for a gestalt. */ @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async unshareVault( @@ -682,15 +726,23 @@ class VaultManager { * Clones the contents of a remote vault into a new local * vault instance */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async cloneVault( nodeId: NodeId, vaultNameOrId: VaultId | VaultName, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timedCancellable(true) + public async cloneVault( + nodeId: NodeId, + vaultNameOrId: VaultId | VaultName, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.cloneVault(nodeId, vaultNameOrId, tran), + this.cloneVault(nodeId, vaultNameOrId, tran, ctx), ); } @@ -701,19 +753,23 @@ class VaultManager { ); return await this.vaultLocks.withF( [vaultId.toString(), RWLockWriter, 'write'], + ctx, async () => { - const vault = await VaultInternal.cloneVaultInternal({ - targetNodeId: nodeId, - targetVaultNameOrId: vaultNameOrId, - vaultId, - db: this.db, - nodeManager: this.nodeManager, - vaultsDbPath: this.vaultsDbPath, - keyRing: this.keyRing, - efs: this.efs, - logger: this.logger.getChild(VaultInternal.name), + const vault = await VaultInternal.cloneVaultInternal( + { + targetNodeId: nodeId, + targetVaultNameOrId: vaultNameOrId, + vaultId: vaultId, + db: this.db, + nodeManager: this.nodeManager, + vaultsDbPath: this.vaultsDbPath, + keyRing: this.keyRing, + efs: this.efs, + logger: this.logger.getChild(VaultInternal.name), + }, tran, - }); + ctx, + ); this.vaultMap.set(vaultIdString, vault); const vaultMetadata = (await this.getVaultMeta(vaultId, tran))!; const baseVaultName = vaultMetadata.vaultName; @@ -760,23 +816,38 @@ class VaultManager { } /** - * Pulls the contents of a remote vault into an existing vault - * instance + * Pulls the contents of a remote vault into an existing vault instance. */ - public async pullVault({ - vaultId, - pullNodeId, - pullVaultNameOrId, - tran, - }: { - vaultId: VaultId; - pullNodeId?: NodeId; - pullVaultNameOrId?: VaultId | VaultName; - tran?: DBTransaction; - }): Promise { + public async pullVault( + { + vaultId, + pullNodeId, + pullVaultNameOrId, + }: { + vaultId: VaultId; + pullNodeId?: NodeId; + pullVaultNameOrId?: VaultId | VaultName; + }, + tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @timedCancellable(true) + public async pullVault( + { + vaultId, + pullNodeId, + pullVaultNameOrId, + }: { + vaultId: VaultId; + pullNodeId?: NodeId; + pullVaultNameOrId?: VaultId | VaultName; + }, + tran: DBTransaction, + @context ctx: ContextTimed, + ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.pullVault({ vaultId, pullNodeId, pullVaultNameOrId, tran }), + this.pullVault({ vaultId, pullNodeId, pullVaultNameOrId }, tran, ctx), ); } @@ -785,92 +856,129 @@ class VaultManager { [vaultId.toString(), RWLockWriter, 'write'], async () => { await tran.lock([...this.vaultsDbPath, vaultId].join('')); - const vault = await this.getVault(vaultId, tran); - await vault.pullVault({ - nodeManager: this.nodeManager, - pullNodeId, - pullVaultNameOrId, + const vault = await this.getVault(vaultId, tran, ctx); + await vault.pullVault( + { + nodeManager: this.nodeManager, + pullNodeId: pullNodeId, + pullVaultNameOrId: pullVaultNameOrId, + }, tran, - }); + ctx, + ); }, ); } /** - * Handler for receiving http GET requests when being - * cloned or pulled from + * Handler for receiving http GET requests when being cloned or pulled from. */ + public handleInfoRequest( + vaultId: VaultId, + tran?: DBTransaction, + ctx?: Partial, + ): AsyncGenerator; @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timed() public async *handleInfoRequest( vaultId: VaultId, - tran?: DBTransaction, + tran: DBTransaction, + @context ctx: ContextTimed, ): AsyncGenerator { if (tran == null) { const handleInfoRequest = (tran: DBTransaction) => - this.handleInfoRequest(vaultId, tran); + this.handleInfoRequest(vaultId, tran, ctx); return yield* this.db.withTransactionG(async function* (tran) { return yield* handleInfoRequest(tran); }); } + const efs = this.efs; - const vault = await this.getVault(vaultId, tran); + const vault = await this.getVault(vaultId, tran, ctx); return yield* withG( [ - this.vaultLocks.lock([vaultId.toString(), RWLockWriter, 'read']), + this.vaultLocks.lock([vaultId.toString(), RWLockWriter, 'read'], ctx), vault.getLock().read(), ], async function* (): AsyncGenerator { + ctx.signal.throwIfAborted(); // Read the commit state of the vault - yield* gitHttp.advertiseRefGenerator({ - efs, - dir: path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), - gitDir: path.join(vaultsUtils.encodeVaultId(vaultId), '.git'), - }); + yield* gitHttp.advertiseRefGenerator( + { + efs, + dir: path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + gitDir: path.join(vaultsUtils.encodeVaultId(vaultId), '.git'), + }, + ctx, + ); }, ); } /** - * Handler for receiving http POST requests when being - * cloned or pulled from + * Handler for receiving http POST requests when being cloned or pulled from. */ + public handlePackRequest( + vaultId: VaultId, + body: Array, + tran?: DBTransaction, + ctx?: Partial, + ): AsyncGenerator; @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timed() public async *handlePackRequest( vaultId: VaultId, body: Array, - tran?: DBTransaction, + tran: DBTransaction, + @context ctx: ContextTimed, ): AsyncGenerator { if (tran == null) { // Lambda to maintain `this` context const handlePackRequest = (tran: DBTransaction) => - this.handlePackRequest(vaultId, body, tran); + this.handlePackRequest(vaultId, body, tran, ctx); return yield* this.db.withTransactionG(async function* (tran) { return yield* handlePackRequest(tran); }); } - const vault = await this.getVault(vaultId, tran); + const vault = await this.getVault(vaultId, tran, ctx); const efs = this.efs; yield* withG( [ - this.vaultLocks.lock([vaultId.toString(), RWLockWriter, 'read']), - vault.getLock().read(), + this.vaultLocks.lock([vaultId.toString(), RWLockWriter, 'read'], ctx), + vault.getLock().read(ctx), ], async function* (): AsyncGenerator { - yield* gitHttp.generatePackRequest({ - efs, - dir: path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), - gitDir: path.join(vaultsUtils.encodeVaultId(vaultId), '.git'), - body: body, - }); + ctx.signal.throwIfAborted(); + yield* gitHttp.generatePackRequest( + { + efs, + dir: path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + gitDir: path.join(vaultsUtils.encodeVaultId(vaultId), '.git'), + body: body, + }, + ctx, + ); }, ); } /** - * Retrieves all the vaults for a peers node + * Retrieves all the vaults for a peer's node. */ - public async *scanVaults(targetNodeId: NodeId): AsyncGenerator<{ + public scanVaults( + targetNodeId: NodeId, + ctx?: Partial, + ): AsyncGenerator<{ + vaultName: VaultName; + vaultIdEncoded: VaultIdEncoded; + vaultPermissions: VaultAction[]; + }>; + @timed() + public async *scanVaults( + targetNodeId: NodeId, + @context ctx: ContextTimed, + ): AsyncGenerator<{ vaultName: VaultName; vaultIdEncoded: VaultIdEncoded; vaultPermissions: VaultAction[]; @@ -884,23 +992,37 @@ class VaultManager { vaultPermissions: VaultAction[]; }> { const client = connection.getClient(); - const genReadable = await client.methods.vaultsScan({}); + const genReadable = await client.methods.vaultsScan({}, ctx); for await (const vault of genReadable) { - const vaultName = vault.vaultName; - const vaultIdEncoded = vault.vaultIdEncoded; - const vaultPermissions = vault.vaultPermissions; - yield { vaultName, vaultIdEncoded, vaultPermissions }; + ctx.signal.throwIfAborted(); + yield { + vaultName: vault.vaultName, + vaultIdEncoded: vault.vaultIdEncoded, + vaultPermissions: vault.vaultPermissions, + }; } }, + ctx, ); } /** * Returns all the shared vaults for a NodeId. */ - public async *handleScanVaults( + public handleScanVaults( nodeId: NodeId, tran?: DBTransaction, + ctx?: Partial, + ): AsyncGenerator<{ + vaultId: VaultId; + vaultName: VaultName; + vaultPermissions: VaultAction[]; + }>; + @timed() + public async *handleScanVaults( + nodeId: NodeId, + tran: DBTransaction, + @context ctx: ContextTimed, ): AsyncGenerator<{ vaultId: VaultId; vaultName: VaultName; @@ -909,7 +1031,7 @@ class VaultManager { if (tran == null) { // Lambda to maintain `this` context const handleScanVaults = (tran: DBTransaction) => - this.handleScanVaults(nodeId, tran); + this.handleScanVaults(nodeId, tran, ctx); return yield* this.db.withTransactionG(async function* (tran) { return yield* handleScanVaults(tran); }); @@ -932,6 +1054,7 @@ class VaultManager { // Getting the list of vaults const vaults = permissions.vaults; for (const vaultIdString of Object.keys(vaults)) { + ctx.signal.throwIfAborted(); // Getting vault permissions const vaultId = IdInternal.fromString(vaultIdString); const vaultPermissions = Object.keys( @@ -969,49 +1092,66 @@ class VaultManager { protected async getVault( vaultId: VaultId, tran: DBTransaction, + ctx: ContextTimed, ): Promise { if (tran == null) { - return this.db.withTransactionF((tran) => this.getVault(vaultId, tran)); + return this.db.withTransactionF((tran) => + this.getVault(vaultId, tran, ctx), + ); } + const vaultIdString = vaultId.toString() as VaultIdString; - // 1. get the vault, if it exists then return that + // 1. Try to get the vault. If it exists then return that. const vault = this.vaultMap.get(vaultIdString); if (vault != null) return vault; - // No vault or state exists then we throw error? + // If no vault or state exists, then we throw an error if ((await this.getVaultMeta(vaultId, tran)) == null) { throw new vaultsErrors.ErrorVaultsVaultUndefined( `Vault ${vaultsUtils.encodeVaultId(vaultId)} doesn't exist`, ); } - // 2. if the state exists then create, add to map and return that - const newVault = await VaultInternal.createVaultInternal({ - vaultId, - keyRing: this.keyRing, - efs: this.efs, - logger: this.logger.getChild(VaultInternal.name), - db: this.db, - vaultsDbPath: this.vaultsDbPath, + // 2. If the state doesn't exist then create it, add to map and return that. + const newVault = await VaultInternal.createVaultInternal( + { + vaultId: vaultId, + keyRing: this.keyRing, + efs: this.efs, + db: this.db, + vaultsDbPath: this.vaultsDbPath, + logger: this.logger.getChild(VaultInternal.name), + }, tran, - }); + ctx, + ); this.vaultMap.set(vaultIdString, newVault); return newVault; } /** - * Takes a function and runs it with the listed vaults. locking is handled automatically + * Takes a function and runs it with the listed vaults. Locking is handled + * automatically. * @param vaultIds List of vault ID for vaults you wish to use * @param f Function you wish to run with the provided vaults * @param tran + * @param ctx */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) public async withVaults( vaultIds: VaultId[], f: (...args: Vault[]) => Promise, tran?: DBTransaction, + ctx?: Partial, + ): Promise; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timedCancellable(true) + public async withVaults( + vaultIds: VaultId[], + f: (...args: Vault[]) => Promise, + tran: DBTransaction, + @context ctx: ContextTimed, ): Promise { if (tran == null) { return this.db.withTransactionF((tran) => - this.withVaults(vaultIds, f, tran), + this.withVaults(vaultIds, f, tran, ctx), ); } @@ -1026,7 +1166,7 @@ class VaultManager { // Getting the vaults while locked const vaults = await Promise.all( vaultIds.map(async (vaultId) => { - return await this.getVault(vaultId, tran); + return await this.getVault(vaultId, tran, ctx); }), ); return await f(...vaults); @@ -1034,17 +1174,27 @@ class VaultManager { } /** - * Takes a generator and runs it with the listed vaults. locking is handled automatically + * Takes a generator and runs it with the listed vaults. Locking is handled + * automatically. * @param vaultIds List of vault ID for vaults you wish to use * @param g Generator you wish to run with the provided vaults * @param tran + * @param ctx */ - @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) - public async *withVaultsG( + public withVaultsG( vaultIds: Array, - g: (...args: Array) => AsyncGenerator, + g: (...args: Array) => AsyncGenerator, tran?: DBTransaction, - ): AsyncGenerator { + ctx?: Partial, + ): AsyncGenerator; + @ready(new vaultsErrors.ErrorVaultManagerNotRunning()) + @timed() + public async *withVaultsG( + vaultIds: Array, + g: (...args: Array) => AsyncGenerator, + tran: DBTransaction, + @context ctx: ContextTimed, + ): AsyncGenerator { if (tran == null) { return yield* this.db.withTransactionG((tran) => this.withVaultsG(vaultIds, g, tran), @@ -1061,11 +1211,11 @@ class VaultManager { const vaultThis = this; return yield* this.vaultLocks.withG( ...vaultLocks, - async function* (): AsyncGenerator { + async function* (): AsyncGenerator { // Getting the vaults while locked const vaults = await Promise.all( vaultIds.map(async (vaultId) => { - return await vaultThis.getVault(vaultId, tran); + return await vaultThis.getVault(vaultId, tran, ctx); }), ); return yield* g(...vaults); diff --git a/src/vaults/VaultOps.ts b/src/vaults/VaultOps.ts index 6d2ea6079..3328abd32 100644 --- a/src/vaults/VaultOps.ts +++ b/src/vaults/VaultOps.ts @@ -1,9 +1,7 @@ -/** - * Adds a secret to the vault - */ import type Logger from '@matrixai/logger'; -import type { Vault } from './Vault'; +import type { ContextTimed } from '@matrixai/contexts'; import type { Stat } from 'encryptedfs'; +import type { Vault } from './Vault'; import path from 'path'; import * as vaultsErrors from './errors'; import * as vaultsUtils from './utils'; @@ -12,50 +10,30 @@ type FileOptions = { recursive?: boolean; }; -async function addSecret( - vault: Vault, - secretName: string, - content: Buffer | string, - logger?: Logger, -): Promise { - await vault.writeF(async (efs) => { - if (await efs.exists(secretName)) { - throw new vaultsErrors.ErrorSecretsSecretDefined( - `${secretName} already exists, try updating instead`, - ); - } - - // Create the directory to the secret if it doesn't exist - await vaultsUtils.mkdirExists(efs, path.dirname(secretName)); - // Write the secret into the vault - await efs.writeFile(secretName, content); - }); - - logger?.info(`Added secret ${secretName} to vault ${vault.vaultId}`); -} - /** - * Changes the contents of a secret + * Adds a secret to the vault */ -async function updateSecret( +async function addSecret( vault: Vault, secretName: string, content: Buffer | string, logger?: Logger, + ctx?: ContextTimed, ): Promise { - await vault.writeF(async (efs) => { - // Throw error if secret does not exist - if (!(await efs.exists(secretName))) { - throw new vaultsErrors.ErrorSecretsSecretUndefined( - 'Secret does not exist, try adding it instead.', - ); - } - - // Write secret into vault - await efs.writeFile(secretName, content); - }); - - logger?.info(`Updated secret ${secretName} in vault ${vault.vaultId}`); + await vault.writeF( + async (efs) => { + if (await efs.exists(secretName)) { + throw new vaultsErrors.ErrorSecretsSecretDefined( + `A secret with name '${secretName}' already exists`, + ); + } + await vaultsUtils.mkdirExists(efs, path.dirname(secretName)); + await efs.writeFile(secretName, content); + }, + undefined, + ctx, + ); + logger?.info(`Added secret ${secretName} to vault ${vault.vaultId}`); } /** @@ -66,17 +44,22 @@ async function renameSecret( secretName: string, secretNameNew: string, logger?: Logger, + ctx?: ContextTimed, ): Promise { - await vault.writeF(async (efs) => { - if (!(await efs.exists(secretName))) { - throw new vaultsErrors.ErrorSecretsSecretUndefined( - 'Secret does not exist, can not rename', - ); - } - await efs.rename(secretName, secretNameNew); - }); + await vault.writeF( + async (efs) => { + if (!(await efs.exists(secretName))) { + throw new vaultsErrors.ErrorSecretsSecretUndefined( + 'Secret does not exist, can not rename', + ); + } + await efs.rename(secretName, secretNameNew); + }, + undefined, + ctx, + ); logger?.info( - `Renamed secret at ${secretName} to ${secretNameNew} in vault ${vault.vaultId}`, + `Renamed secret ${secretName} to ${secretNameNew} in vault ${vault.vaultId}`, ); } @@ -132,60 +115,69 @@ async function deleteSecret( secretName: string, fileOptions?: FileOptions, logger?: Logger, + ctx?: ContextTimed, ): Promise { - await vault.writeF(async (efs) => { - try { - const stat = await efs.stat(secretName); - if (stat.isDirectory()) { - await efs.rmdir(secretName, fileOptions); - logger?.info(`Deleted directory at '${secretName}'`); - } else { - // Remove the specified file - await efs.unlink(secretName); - logger?.info(`Deleted secret at '${secretName}'`); - } - } catch (e) { - if (e.code === 'ENOENT') { - throw new vaultsErrors.ErrorSecretsSecretUndefined( - `Secret with name: ${secretName} does not exist`, - { cause: e }, - ); - } - if (e.code === 'ENOTEMPTY') { - throw new vaultsErrors.ErrorVaultsRecursive( - `Could not delete directory '${secretName}' without recursive option`, - { cause: e }, - ); - } - throw e; + try { + await vault.writeF( + async (efs) => { + const stat = await efs.stat(secretName); + if (stat.isDirectory()) { + await efs.rmdir(secretName, fileOptions); + logger?.info(`Deleted directory at '${secretName}'`); + } else { + await efs.unlink(secretName); + logger?.info(`Deleted secret at '${secretName}'`); + } + }, + undefined, + ctx, + ); + } catch (e) { + if (e.code === 'ENOENT') { + throw new vaultsErrors.ErrorSecretsSecretUndefined( + `Secret with name: ${secretName} does not exist`, + { cause: e }, + ); } - }); + if (e.code === 'ENOTEMPTY') { + throw new vaultsErrors.ErrorVaultsRecursive( + `Could not delete directory '${secretName}' without recursive option`, + { cause: e }, + ); + } + throw e; + } } /** - * Adds an empty directory to the root of the vault. - * i.e. mkdir("folder", { recursive = false }) creates the "/folder" directory + * Adds an empty directory to the root of the vault. Note that efs does not + * track empty directories. */ async function mkdir( vault: Vault, dirPath: string, fileOptions?: FileOptions, logger?: Logger, + ctx?: ContextTimed, ): Promise { const recursive = fileOptions?.recursive ?? false; // Technically, writing an empty directory won't make a commit, and doesn't // need a write resource as git doesn't track empty directories. It is // still being used to allow concurrency. try { - await vault.writeF(async (efs) => { - await efs.mkdir(dirPath, fileOptions); - logger?.info(`Created secret directory at '${dirPath}'`); - }); + await vault.writeF( + async (efs) => { + await efs.mkdir(dirPath, fileOptions); + logger?.info(`Created secret directory at '${dirPath}'`); + }, + undefined, + ctx, + ); } catch (e) { logger?.error(`Failed to create directory '${dirPath}'. Reason: ${e.code}`); if (e.code === 'ENOENT' && !recursive) { throw new vaultsErrors.ErrorVaultsRecursive( - `Could not create direcotry '${dirPath}' without recursive option`, + `Could not create directory '${dirPath}' without recursive option`, { cause: e }, ); } @@ -211,44 +203,51 @@ async function addSecretDirectory( secretDirectory: string, fs = require('fs'), logger?: Logger, + ctx?: ContextTimed, ): Promise { const absoluteDirPath = path.resolve(secretDirectory); + await vault.writeF( + async (efs) => { + for await (const secretPath of vaultsUtils.readDirRecursively( + fs, + absoluteDirPath, + )) { + // Determine the path to the secret + const relPath = path.relative( + path.dirname(absoluteDirPath), + secretPath, + ); + // Obtain the content of the secret + const content = await fs.promises.readFile(secretPath); - await vault.writeF(async (efs) => { - for await (const secretPath of vaultsUtils.readDirRecursively( - fs, - absoluteDirPath, - )) { - // Determine the path to the secret - const relPath = path.relative(path.dirname(absoluteDirPath), secretPath); - // Obtain the content of the secret - const content = await fs.promises.readFile(secretPath); - - if (await efs.exists(relPath)) { - try { - // Write secret into vault - await efs.writeFile(relPath, content); - logger?.info(`Added secret at directory '${relPath}'`); - } catch (e) { - // Warn of a failed addition but continue operation - logger?.warn(`Adding secret ${relPath} failed`); - throw e; - } - } else { - try { - // Create directory if it doesn't exist - await vaultsUtils.mkdirExists(efs, path.dirname(relPath)); - // Write secret into vault - await efs.writeFile(relPath, content, {}); - logger?.info(`Added secret to directory at '${relPath}'`); - } catch (e) { - // Warn of a failed addition but continue operation - logger?.warn(`Adding secret ${relPath} failed`); - throw e; + if (await efs.exists(relPath)) { + try { + // Write secret into vault + await efs.writeFile(relPath, content); + logger?.info(`Added secret at directory '${relPath}'`); + } catch (e) { + // Warn of a failed addition but continue operation + logger?.warn(`Adding secret ${relPath} failed`); + throw e; + } + } else { + try { + // Create directory if it doesn't exist + await vaultsUtils.mkdirExists(efs, path.dirname(relPath)); + // Write secret into vault + await efs.writeFile(relPath, content, {}); + logger?.info(`Added secret to directory at '${relPath}'`); + } catch (e) { + // Warn of a failed addition but continue operation + logger?.warn(`Adding secret ${relPath} failed`); + throw e; + } } } - } - }); + }, + undefined, + ctx, + ); } /** @@ -272,32 +271,36 @@ async function writeSecret( secretName: string, content: Buffer | string, logger?: Logger, + ctx?: ContextTimed, ): Promise { - await vault.writeF(async (efs) => { - try { - await efs.writeFile(secretName, content); - logger?.info(`Wrote secret ${secretName} in vault ${vault.vaultId}`); - } catch (e) { - if (e.code === 'ENOENT') { - throw new vaultsErrors.ErrorSecretsSecretUndefined( - `One or more parent directories for '${secretName}' do not exist`, - { cause: e }, - ); - } - if (e.code === 'EISDIR') { - throw new vaultsErrors.ErrorSecretsIsDirectory( - `Secret path '${secretName}' is a directory`, - { cause: e }, - ); - } - throw e; + try { + await vault.writeF( + async (efs) => { + await efs.writeFile(secretName, content); + logger?.info(`Wrote secret ${secretName} in vault ${vault.vaultId}`); + }, + undefined, + ctx, + ); + } catch (e) { + if (e.code === 'ENOENT') { + throw new vaultsErrors.ErrorSecretsSecretUndefined( + `One or more parent directories for '${secretName}' do not exist`, + { cause: e }, + ); } - }); + if (e.code === 'EISDIR') { + throw new vaultsErrors.ErrorSecretsIsDirectory( + `Secret path '${secretName}' is a directory`, + { cause: e }, + ); + } + throw e; + } } export { addSecret, - updateSecret, renameSecret, getSecret, statSecret, diff --git a/src/vaults/utils.ts b/src/vaults/utils.ts index a946127b1..7dd0de702 100644 --- a/src/vaults/utils.ts +++ b/src/vaults/utils.ts @@ -16,16 +16,16 @@ import * as nodesUtils from '../nodes/utils'; import * as validationErrors from '../validation/errors'; /** - * Vault history is designed for linear-history - * The canonical branch represents the one and only true timeline - * In the future, we can introduce non-linear history - * Where branches are automatically made when new timelines are created + * Vault history is designed for linear-history. + * The canonical branch represents the one and only true timeline. + * In the future, we can introduce non-linear history where branches are + * automatically made when new timelines are created. */ const canonicalBranch = 'master'; const canonicalBranchRef = 'refs/heads/' + canonicalBranch; /** - * Vault reference can be HEAD, any of the special tags or a commit ID + * Vault reference can be HEAD, any of the special tags, or a commit ID. */ function validateRef(ref: any): ref is VaultRef { return refs.includes(ref) || validateCommitId(ref); @@ -38,7 +38,8 @@ function assertRef(ref: any): asserts ref is VaultRef { } /** - * Commit ids are SHA1 hashes encoded as 40-character long lowercase hexadecimal strings + * Commit IDs are SHA1 hashes encoded as 40-character long lowercase + * hexadecimal strings. */ function validateCommitId(commitId: any): commitId is CommitId { return /^[a-f0-9]{40}$/.test(commitId); @@ -70,7 +71,7 @@ async function* readDirRecursively( async function* walkFs( efs: FileSystemReadable, path: string = '.', -): AsyncGenerator { +): AsyncGenerator { const shortList: Array = [path]; let path_: Path | undefined = undefined; while ((path_ = shortList.shift()) != null) { @@ -102,7 +103,11 @@ function parseVaultAction(data: any): VaultAction { return data; } -async function deleteObject(fs: EncryptedFS, gitdir: string, ref: string) { +async function deleteObject( + fs: EncryptedFS, + gitdir: string, + ref: string, +): Promise { const bucket = ref.slice(0, 2); const shortRef = ref.slice(2); const objectPath = path.join(gitdir, 'objects', bucket, shortRef); @@ -113,32 +118,33 @@ async function deleteObject(fs: EncryptedFS, gitdir: string, ref: string) { } } -async function mkdirExists(efs: FileSystemWritable, directory: string) { +async function mkdirExists( + efs: FileSystemWritable, + directory: string, +): Promise { try { await efs.mkdir(directory, { recursive: true }); } catch (e) { - if (e.code !== 'EEXIST') { - throw e; - } + if (e.code !== 'EEXIST') throw e; } } /** - * Converts a `Buffer` to a `Uint8Array` without copying the contents + * Converts a `Buffer` to a `Uint8Array` without copying the contents. */ function bufferToUint8ArrayCopyless(data: Buffer): Uint8Array { return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); } /** - * Converts a `Uint8Array` to a `Buffer` without copying the contents + * Converts a `Uint8Array` to a `Buffer` without copying the contents. */ function uint8ArrayToBufferCopyless(data: Uint8Array): Buffer { return Buffer.from(data.buffer, data.byteOffset, data.byteLength); } /** - * Concatenates `Buffers` or `Uint8Array`s into a `Uint8Array` + * Concatenates `Buffers` or `Uint8Array`s into a `Uint8Array`. */ function uint8ArrayConcat(list: Array): Uint8Array { return bufferToUint8ArrayCopyless(Buffer.concat(list)); diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index 326bea568..5cb8771c4 100644 --- a/tests/client/handlers/vaults.test.ts +++ b/tests/client/handlers/vaults.test.ts @@ -14,6 +14,7 @@ import os from 'os'; import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; import { RPCClient } from '@matrixai/rpc'; +import { ErrorRPCTimedOut } from '@matrixai/rpc/dist/errors'; import { WebSocketClient } from '@matrixai/ws'; import TaskManager from '@/tasks/TaskManager'; import ACL from '@/acl/ACL'; @@ -72,7 +73,6 @@ import * as vaultsUtils from '@/vaults/utils'; import * as vaultsErrors from '@/vaults/errors'; import * as clientErrors from '@/client/errors'; import * as networkUtils from '@/network/utils'; -import * as utils from '@/utils'; import * as testsUtils from '../../utils'; describe('vaultsClone', () => { @@ -1441,7 +1441,9 @@ describe('vaultsSecretsMkdir', () => { await writer.close(); const consumeP = async () => { try { - for await (const _ of response.readable); + for await (const _ of response.readable) { + // Consume + } } catch (e) { throw e.cause; } @@ -1465,7 +1467,7 @@ describe('vaultsSecretsMkdir', () => { await writer.close(); // Check if the operation concluded as expected for await (const data of response.readable) { - expect(data.type).toEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } await vaultManager.withVaults([vaultId], async (vault) => { await vault.readF(async (efs) => { @@ -1485,8 +1487,9 @@ describe('vaultsSecretsMkdir', () => { await writer.close(); // Check if the operation concluded as expected for await (const data of response.readable) { - expect(data.type).toEqual('error'); - if (data.type !== 'error') utils.never("Type is asserted to be 'error'"); + if (data.type !== 'ErrorMessage') { + fail('Type should be "ErrorMessage"'); + } expect(data.code).toEqual('ENOENT'); expect(data.reason).toEqual(dirPath); } @@ -1519,7 +1522,7 @@ describe('vaultsSecretsMkdir', () => { await writer.close(); // Check if the operation concluded as expected for await (const data of response.readable) { - expect(data.type).toEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } await vaultManager.withVaults( [vaultId1, vaultId2], @@ -1554,7 +1557,7 @@ describe('vaultsSecretsMkdir', () => { // Check if the operation concluded as expected let successCount = 0; for await (const data of response.readable) { - if (data.type === 'error') { + if (data.type === 'ErrorMessage') { expect(data.code).toEqual('ENOENT'); expect(data.reason).toEqual(dirPath3); } else { @@ -1592,8 +1595,10 @@ describe('vaultsSecretsMkdir', () => { await writer.close(); // Check if the operation concluded as expected for await (const data of response.readable) { - expect(data.type).toEqual('error'); - if (data.type !== 'error') utils.never("Type is asserted to be 'error'"); + expect(data.type).toEqual('ErrorMessage'); + if (data.type !== 'ErrorMessage') { + fail('Type should be "ErrorMessage"'); + } expect(data.code).toEqual('EEXIST'); expect(data.reason).toEqual(dirPath); } @@ -1707,7 +1712,9 @@ describe('vaultsSecretsCat', () => { await writer.close(); // Read response const consumeP = async () => { - for await (const _ of response.readable); + for await (const _ of response.readable) { + // Consume + } }; await testsUtils.expectRemoteError( consumeP(), @@ -1735,9 +1742,8 @@ describe('vaultsSecretsCat', () => { await writer.close(); // Read response for await (const data of response.readable) { - expect(data.type).toEqual('success'); - if (data.type !== 'success') { - utils.never("Type is asserted to be 'success'"); + if (data.type !== 'SuccessMessage') { + fail('Type should be "SuccessMessage"'); } expect(data.secretContent).toEqual(secretContent); } @@ -1756,8 +1762,9 @@ describe('vaultsSecretsCat', () => { await writer.close(); // Read response for await (const data of response.readable) { - expect(data.type).toEqual('error'); - if (data.type !== 'error') utils.never("Type is asserted to be 'error'"); + if (data.type !== 'ErrorMessage') { + fail('Type should be "ErrorMessage"'); + } expect(data.code).toEqual('ENOENT'); expect(data.reason).toEqual(secretName); } @@ -1782,8 +1789,9 @@ describe('vaultsSecretsCat', () => { await writer.close(); // Read response for await (const data of response.readable) { - expect(data.type).toEqual('error'); - if (data.type !== 'error') utils.never("Type is asserted to be 'error'"); + if (data.type !== 'ErrorMessage') { + fail('Type should be "ErrorMessage"'); + } expect(data.code).toEqual('EISDIR'); expect(data.reason).toEqual(secretName); } @@ -1812,9 +1820,8 @@ describe('vaultsSecretsCat', () => { // Read response let totalContent = ''; for await (const data of response.readable) { - expect(data.type).toEqual('success'); - if (data.type !== 'success') { - utils.never("Type is asserted to be 'success'"); + if (data.type !== 'SuccessMessage') { + fail('Type should be "SuccessMessage"'); } totalContent += data.secretContent; } @@ -1856,9 +1863,8 @@ describe('vaultsSecretsCat', () => { // Read response let totalContent = ''; for await (const data of response.readable) { - expect(data.type).toEqual('success'); - if (data.type !== 'success') { - utils.never("Type is asserted to be 'success'"); + if (data.type !== 'SuccessMessage') { + fail('Type should be "SuccessMessage"'); } totalContent += data.secretContent; } @@ -1905,7 +1911,7 @@ describe('vaultsSecretsCat', () => { // Read response let totalContent = ''; for await (const data of response.readable) { - if (data.type === 'success') { + if (data.type === 'SuccessMessage') { totalContent += data.secretContent; } else { expect(data.code).toEqual('ENOENT'); @@ -2458,6 +2464,62 @@ describe('vaultsSecretsRemove', () => { vaultsErrors.ErrorVaultsVaultUndefined, ); }); + test('should fail when cancelled', async () => { + // Inducing a cancellation by a timeout + const response = await rpcClient.methods.vaultsSecretsRemove({ + timer: 100, + }); + // Read response + const consumeP = async () => { + for await (const _ of response.readable) { + // Consume values + } + }; + await expect(consumeP()).rejects.toThrow(ErrorRPCTimedOut); + }); + test('should cancel in the midst of an operation', async () => { + // Create secrets + const secretName = 'test-secret1'; + const vaultId = await vaultManager.createVault('test-vault'); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName, secretName); + }); + }); + // Inducing a cancellation by a timeout + const response = await rpcClient.methods.vaultsSecretsRemove({ + timer: 100, + }); + // Header message + const writer = response.writable.getWriter(); + await writer.write({ + type: 'VaultNamesHeaderMessage', + vaultNames: [vaultIdEncoded], + }); + // Set a timeout so that the method will execute after RPC timeout + setTimeout(async () => { + // Content messages + await writer.write({ + type: 'SecretIdentifierMessage', + nameOrId: vaultIdEncoded, + secretName: secretName, + }); + await writer.close(); + // Read response + const consumeP = async () => { + for await (const _ of response.readable) { + // Consume values + } + }; + await expect(consumeP()).rejects.toThrow(ErrorRPCTimedOut); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(secretName)).toBeTruthy(); + }); + }); + }, 150); + }); test('fails deleting vault root', async () => { // Create secrets const secretName = 'test-secret1'; @@ -2486,9 +2548,10 @@ describe('vaultsSecretsRemove', () => { let loopRun = false; for await (const data of response.readable) { loopRun = true; - expect(data.type).toStrictEqual('error'); - if (data.type !== 'error') utils.never("Type is asserted to be 'error'"); - expect(data.code).toStrictEqual('EINVAL'); + if (data.type !== 'ErrorMessage') { + fail('Type should be "ErrorMessage"'); + } + expect(data.code).toEqual('EINVAL'); } // Check expect(loopRun).toBeTruthy(); @@ -2533,7 +2596,7 @@ describe('vaultsSecretsRemove', () => { let loopRun = false; for await (const data of response.readable) { loopRun = true; - expect(data.type).toStrictEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } expect(loopRun).toBeTruthy(); // Check each secret was deleted @@ -2544,7 +2607,7 @@ describe('vaultsSecretsRemove', () => { }); }); }); - test('continues on error', async () => { + test('should continue on error', async () => { // Create secrets const secretName1 = 'test-secret1'; const secretName2 = 'test-secret2'; @@ -2584,13 +2647,13 @@ describe('vaultsSecretsRemove', () => { await writer.close(); let errorCount = 0; for await (const data of response.readable) { - if (data.type === 'error') { + if (data.type === 'ErrorMessage') { // No other file name should raise this error - expect(data.reason).toStrictEqual(invalidName); + expect(data.reason).toEqual(invalidName); errorCount++; continue; } - expect(data.type).toStrictEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } // Only one error should have happened expect(errorCount).toEqual(1); @@ -2642,7 +2705,7 @@ describe('vaultsSecretsRemove', () => { let loopRun = false; for await (const data of response.readable) { loopRun = true; - expect(data.type).toStrictEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } expect(loopRun).toBeTruthy(); // Ensure single log message for deleting the secrets @@ -2700,7 +2763,7 @@ describe('vaultsSecretsRemove', () => { let loopRun = false; for await (const data of response.readable) { loopRun = true; - expect(data.type).toStrictEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } // Ensure single log message for deleting the secrets expect(loopRun).toBeTruthy(); @@ -2750,7 +2813,7 @@ describe('vaultsSecretsRemove', () => { }); await writer.close(); for await (const data of response.readable) { - expect(data.type).toStrictEqual('success'); + expect(data.type).toEqual('SuccessMessage'); } // Check each secret and the secret directory were deleted await vaultManager.withVaults([vaultId], async (vault) => { @@ -2793,7 +2856,7 @@ describe('vaultsSecretsRemove', () => { }); await writer.close(); for await (const data of response.readable) { - expect(data.type).toStrictEqual('error'); + expect(data.type).toEqual('ErrorMessage'); } // Check each secret and the secret directory were deleted await vaultManager.withVaults([vaultId], async (vault) => { diff --git a/tests/git/http.test.ts b/tests/git/http.test.ts index 0bb81b696..0341724fb 100644 --- a/tests/git/http.test.ts +++ b/tests/git/http.test.ts @@ -1,6 +1,8 @@ +import type { ContextTimed } from '@matrixai/contexts'; import fs from 'fs'; import path from 'path'; import os from 'os'; +import { timedCancellable as timedCancellableF } from '@matrixai/contexts/dist/functions'; import git from 'isomorphic-git'; import { test } from '@fast-check/jest'; import fc from 'fast-check'; @@ -82,61 +84,64 @@ describe('Git Http', () => { } }); test('advertiseRefGenerator', async () => { - await gitTestUtils.createGitRepo({ - ...gitDirs, - author: 'tester', - commits: [ - { - message: 'commit1', - files: [ - { - name: 'file1', - contents: 'this is a file', - }, - ], - }, - { - message: 'commit2', - files: [ - { - name: 'file2', - contents: 'this is another file', - }, - ], - }, - { - message: 'commit3', - files: [ - { - name: 'file1', - contents: 'this is a changed file', - }, - ], - }, - ], - }); - const gen = gitHttp.advertiseRefGenerator(gitDirs); - let response = ''; - for await (const result of gen) { - response += result.toString(); - } - // Header - expect(response).toInclude('001e# service=git-upload-pack\n'); - // Includes flush packets - expect(response).toInclude('0000'); - // Includes capabilities - expect(response).toIncludeMultiple([ - 'side-band-64k', - 'symref=HEAD:refs/heads/master', - 'agent=git/isomorphic-git@1.8.1', - ]); - // HEAD commit is listed twice as `HEAD` and `master` - const headCommit = (await git.log({ ...gitDirs, ref: 'HEAD' }))[0].oid; - expect(response).toIncludeRepeated(headCommit, 2); - // `HEAD` and `master` are both listed - expect(response).toIncludeMultiple(['HEAD', 'master']); - // A null byte is included to delimit first line and capabilities - expect(response).toInclude('\0'); + const f = async (ctx: ContextTimed) => { + await gitTestUtils.createGitRepo({ + ...gitDirs, + author: 'tester', + commits: [ + { + message: 'commit1', + files: [ + { + name: 'file1', + contents: 'this is a file', + }, + ], + }, + { + message: 'commit2', + files: [ + { + name: 'file2', + contents: 'this is another file', + }, + ], + }, + { + message: 'commit3', + files: [ + { + name: 'file1', + contents: 'this is a changed file', + }, + ], + }, + ], + }); + const gen = gitHttp.advertiseRefGenerator(gitDirs, ctx); + let response = ''; + for await (const result of gen) { + response += result.toString(); + } + // Header + expect(response).toInclude('001e# service=git-upload-pack\n'); + // Includes flush packets + expect(response).toInclude('0000'); + // Includes capabilities + expect(response).toIncludeMultiple([ + 'side-band-64k', + 'symref=HEAD:refs/heads/master', + 'agent=git/isomorphic-git@1.8.1', + ]); + // HEAD commit is listed twice as `HEAD` and `master` + const headCommit = (await git.log({ ...gitDirs, ref: 'HEAD' }))[0].oid; + expect(response).toIncludeRepeated(headCommit, 2); + // `HEAD` and `master` are both listed + expect(response).toIncludeMultiple(['HEAD', 'master']); + // A null byte is included to delimit first line and capabilities + expect(response).toInclude('\0'); + }; + await timedCancellableF(f, true)(); }); test('parsePackRequest', async () => { const data = Buffer.from( @@ -161,107 +166,107 @@ describe('Git Http', () => { }, ); test('generatePackData', async () => { - await gitTestUtils.createGitRepo({ - ...gitDirs, - author: 'tester', - commits: [ - { - message: 'commit1', - files: [ - { - name: 'file1', - contents: 'this is a file', - }, - ], - }, - { - message: 'commit2', - files: [ - { - name: 'file2', - contents: 'this is another file', - }, - ], - }, - { - message: 'commit3', - files: [ - { - name: 'file1', - contents: 'this is a changed file', - }, - ], - }, - ], - }); - const objectIds = await gitUtils.listObjectsAll(gitDirs); - const gen = gitHttp.generatePackData({ - ...gitDirs, - objectIds, - }); - let acc = Buffer.alloc(0); - for await (const line of gen) { - acc = Buffer.concat([acc, line.subarray(5)]); - } - const packPath = path.join(gitDirs.dir, 'pack'); - await fs.promises.writeFile(packPath, acc); - // Checking that all objectIds are included and packFile is valid using isometric git - const result = await git.indexPack({ - ...gitDirs, - filepath: 'pack', - }); - expect(result.oids).toIncludeAllMembers(objectIds); + const f = async (ctx: ContextTimed) => { + await gitTestUtils.createGitRepo({ + ...gitDirs, + author: 'tester', + commits: [ + { + message: 'commit1', + files: [ + { + name: 'file1', + contents: 'this is a file', + }, + ], + }, + { + message: 'commit2', + files: [ + { + name: 'file2', + contents: 'this is another file', + }, + ], + }, + { + message: 'commit3', + files: [ + { + name: 'file1', + contents: 'this is a changed file', + }, + ], + }, + ], + }); + const objectIds = await gitUtils.listObjectsAll(gitDirs, ctx); + const gen = gitHttp.generatePackData({ ...gitDirs, objectIds }, ctx); + let acc = Buffer.alloc(0); + for await (const line of gen) { + acc = Buffer.concat([acc, line.subarray(5)]); + } + const packPath = path.join(gitDirs.dir, 'pack'); + await fs.promises.writeFile(packPath, acc); + // Checking that all objectIds are included and packFile is valid using isometric git + const result = await git.indexPack({ + ...gitDirs, + filepath: 'pack', + }); + expect(result.oids).toIncludeAllMembers(objectIds); + }; + await timedCancellableF(f, true)(); }); test('generatePackRequest', async () => { - await gitTestUtils.createGitRepo({ - ...gitDirs, - author: 'tester', - commits: [ - { - message: 'commit1', - files: [ - { - name: 'file1', - contents: 'this is a file', - }, - ], - }, - { - message: 'commit2', - files: [ - { - name: 'file2', - contents: 'this is another file', - }, - ], - }, - { - message: 'commit3', - files: [ - { - name: 'file1', - contents: 'this is a changed file', - }, - ], - }, - ], - }); - const gen = gitHttp.generatePackRequest({ - ...gitDirs, - body: [], - }); - let response = ''; - for await (const line of gen) { - response += line.toString(); - } - // NAK response for no common objects - expect(response).toInclude('0008NAK\n'); - // Pack data included on chanel 1 - expect(response).toInclude('\x01PACK'); - // Progress data included on chanel 2 - expect(response).toInclude('0017\x02progress is at 50%'); - // Flush packet included - expect(response).toInclude('0000'); + const f = async (ctx: ContextTimed) => { + await gitTestUtils.createGitRepo({ + ...gitDirs, + author: 'tester', + commits: [ + { + message: 'commit1', + files: [ + { + name: 'file1', + contents: 'this is a file', + }, + ], + }, + { + message: 'commit2', + files: [ + { + name: 'file2', + contents: 'this is another file', + }, + ], + }, + { + message: 'commit3', + files: [ + { + name: 'file1', + contents: 'this is a changed file', + }, + ], + }, + ], + }); + const gen = gitHttp.generatePackRequest({ ...gitDirs, body: [] }, ctx); + let response = ''; + for await (const line of gen) { + response += line.toString(); + } + // NAK response for no common objects + expect(response).toInclude('0008NAK\n'); + // Pack data included on chanel 1 + expect(response).toInclude('\x01PACK'); + // Progress data included on chanel 2 + expect(response).toInclude('0017\x02progress is at 50%'); + // Flush packet included + expect(response).toInclude('0000'); + }; + await timedCancellableF(f, true)(); }); test('end to end clone', async () => { await gitTestUtils.createGitRepo({ @@ -301,14 +306,14 @@ describe('Git Http', () => { const request = gitTestUtils.request(gitDirs); const newDir = path.join(dataDir, 'newRepo'); const newDirs = { - fs, + fs: fs, dir: newDir, gitdir: path.join(newDir, '.git'), gitDir: path.join(newDir, '.git'), }; await git.clone({ - fs, + fs: fs, dir: newDir, http: { request }, url: 'http://', @@ -321,7 +326,7 @@ describe('Git Http', () => { (await fs.promises.readFile(path.join(newDirs.dir, 'file2'))).toString(), ).toBe('this is another file'); }); - test('end to end Pull', async () => { + test('end to end pull', async () => { await gitTestUtils.createGitRepo({ ...gitDirs, author: 'tester', @@ -357,14 +362,14 @@ describe('Git Http', () => { }); const newDir = path.join(dataDir, 'newRepo'); const newDirs = { - fs, + fs: fs, dir: newDir, gitdir: path.join(newDir, '.git'), gitDir: path.join(newDir, '.git'), }; const request = gitTestUtils.request(gitDirs); await git.clone({ - fs, + fs: fs, dir: newDir, http: { request }, url: 'http://', diff --git a/tests/git/utils.test.ts b/tests/git/utils.test.ts index 69b960bfd..fe7aaa90e 100644 --- a/tests/git/utils.test.ts +++ b/tests/git/utils.test.ts @@ -1,9 +1,11 @@ +import type { ContextTimed } from '@matrixai/contexts'; import fs from 'fs'; import os from 'os'; import path from 'path'; +import { timedCancellable as timedCancellableF } from '@matrixai/contexts/dist/functions'; import git from 'isomorphic-git'; -import { test } from '@fast-check/jest'; import fc from 'fast-check'; +import { test } from '@fast-check/jest'; import * as gitUtils from '@/git/utils'; import * as validationErrors from '@/validation/errors'; import * as gitTestUtils from './utils'; @@ -39,54 +41,59 @@ describe('Git utils', () => { }); test('listReferencesGenerator', async () => { - // Start with creating a git repo with commits - await gitTestUtils.createGitRepo({ - ...gitDirs, - author: 'tester', - commits: [ - { - message: 'commit1', - files: [ - { - name: 'file1', - contents: 'this is a file', - }, - ], - }, - { - message: 'commit2', - files: [ - { - name: 'file2', - contents: 'this is another file', - }, - ], - }, - { - message: 'commit3', - files: [ - { - name: 'file1', - contents: 'this is a changed file', - }, - ], - }, - ], - }); - - const headObjectId = ( - await git.log({ + const f = async (ctx: ContextTimed) => { + // Start with creating a git repo with commits + await gitTestUtils.createGitRepo({ ...gitDirs, - depth: 1, - }) - )[0].oid; - const expectedReferences = ['HEAD', 'refs/heads/master']; - for await (const [reference, objectId] of gitUtils.listReferencesGenerator({ - ...gitDirs, - })) { - expect(reference).toBeOneOf(expectedReferences); - expect(objectId).toBe(headObjectId); - } + author: 'tester', + commits: [ + { + message: 'commit1', + files: [ + { + name: 'file1', + contents: 'this is a file', + }, + ], + }, + { + message: 'commit2', + files: [ + { + name: 'file2', + contents: 'this is another file', + }, + ], + }, + { + message: 'commit3', + files: [ + { + name: 'file1', + contents: 'this is a changed file', + }, + ], + }, + ], + }); + + const headObjectId = ( + await git.log({ + ...gitDirs, + depth: 1, + }) + )[0].oid; + const expectedReferences = ['HEAD', 'refs/heads/master']; + for await (const [ + reference, + objectId, + ] of gitUtils.listReferencesGenerator({ ...gitDirs }, ctx)) { + expect(reference).toBeOneOf(expectedReferences); + expect(objectId).toBe(headObjectId); + } + }; + // Generate a context for the test case + await timedCancellableF(f, true)(); }); test('refCapability', async () => { await gitTestUtils.createGitRepo({ @@ -137,57 +144,64 @@ describe('Git utils', () => { } }); test('listObjects', async () => { - await gitTestUtils.createGitRepo({ - ...gitDirs, - author: 'tester', - commits: [ - { - message: 'commit1', - files: [ - { - name: 'file1', - contents: 'this is a file', - }, - ], - }, - { - message: 'commit2', - files: [ - { - name: 'file2', - contents: 'this is another file', - }, - ], - }, - { - message: 'commit3', - files: [ - { - name: 'file1', - contents: 'this is a changed file', - }, - ], - }, - ], - }); - - const commitIds = ( - await git.log({ + const f = async (ctx: ContextTimed) => { + await gitTestUtils.createGitRepo({ ...gitDirs, - ref: 'HEAD', - }) - ).map((v) => v.oid); + author: 'tester', + commits: [ + { + message: 'commit1', + files: [ + { + name: 'file1', + contents: 'this is a file', + }, + ], + }, + { + message: 'commit2', + files: [ + { + name: 'file2', + contents: 'this is another file', + }, + ], + }, + { + message: 'commit3', + files: [ + { + name: 'file1', + contents: 'this is a changed file', + }, + ], + }, + ], + }); - const objectList = await gitUtils.listObjects({ - ...gitDirs, - wants: commitIds, - haves: [], - }); - const expectedObjectIds = await gitUtils.listObjectsAll(gitDirs); - // Found objects should include all the commits - expect(objectList).toIncludeAllMembers(commitIds); - // Since it was an exhaustive walk of all commits, all objectIds should be included - expect(objectList).toIncludeAllMembers(expectedObjectIds); + const commitIds = ( + await git.log({ + ...gitDirs, + ref: 'HEAD', + }) + ).map((v) => v.oid); + + const objectList = await gitUtils.listObjects( + { + ...gitDirs, + wants: commitIds, + haves: [], + }, + ctx, + ); + const expectedObjectIds = await gitUtils.listObjectsAll(gitDirs, ctx); + // Found objects should include all the commits + expect(objectList).toIncludeAllMembers(commitIds); + // Since it was an exhaustive walk of all commits, all objectIds should be included + expect(objectList).toIncludeAllMembers(expectedObjectIds); + }; + // Generate a context for the test case + await timedCancellableF(f, true)(); }); test.prop([ gitTestUtils.gitRequestDataArb, diff --git a/tests/git/utils.ts b/tests/git/utils.ts index 1311208ae..d5b6d81d9 100644 --- a/tests/git/utils.ts +++ b/tests/git/utils.ts @@ -153,15 +153,15 @@ function request({ headers: POJO; body: Array; }) => { - // Console.log('body', body.map(v => v.toString())) + const abortController = new AbortController(); + const ctx = { signal: abortController.signal }; switch (method) { case 'GET': { // Send back the GET request info response - const advertiseRefGen = gitHttp.advertiseRefGenerator({ - efs, - dir, - gitDir, - }); + const advertiseRefGen = gitHttp.advertiseRefGenerator( + { efs, dir, gitDir }, + ctx, + ); return { url: url, @@ -173,12 +173,10 @@ function request({ }; } case 'POST': { - const packGen = gitHttp.generatePackRequest({ - efs, - dir, - gitDir, - body, - }); + const packGen = gitHttp.generatePackRequest( + { efs, dir, gitDir, body }, + ctx, + ); return { url: url, method: method, diff --git a/tests/vaults/VaultInternal.test.ts b/tests/vaults/VaultInternal.test.ts index 2d487da30..92d4beb47 100644 --- a/tests/vaults/VaultInternal.test.ts +++ b/tests/vaults/VaultInternal.test.ts @@ -1,20 +1,22 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { VaultId } from '@/vaults/types'; import type { Vault } from '@/vaults/Vault'; -import type KeyRing from '@/keys/KeyRing'; import type { LevelPath } from '@matrixai/db'; import type { Key } from '@/keys/types'; +import type KeyRing from '@/keys/KeyRing'; import os from 'os'; import path from 'path'; import fs from 'fs'; +import git from 'isomorphic-git'; +import { EncryptedFS } from 'encryptedfs'; +import { timedCancellable as timedCancellableF } from '@matrixai/contexts/dist/functions'; import { DB } from '@matrixai/db'; import { withF } from '@matrixai/resources'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { EncryptedFS } from 'encryptedfs'; -import git from 'isomorphic-git'; import { tagLast } from '@/vaults/types'; +import { sleep } from '@/utils'; import VaultInternal from '@/vaults/VaultInternal'; import * as vaultsErrors from '@/vaults/errors'; -import { sleep } from '@/utils'; import * as keysUtils from '@/keys/utils'; import * as vaultsUtils from '@/vaults/utils'; import * as utils from '@/utils'; @@ -639,42 +641,62 @@ describe('VaultInternal', () => { await expect(vault.version(newRef2)).rejects.toThrow(); }); test('commit added if mutation in writeG', async () => { - const commit = (await vault.log())[0].commitId; - const gen = vault.writeG(async function* (efs): AsyncGenerator { - yield await efs.writeFile('secret-1', 'secret-content'); - }); - for await (const _ of gen) { - // Do nothing - } - const log = await vault.log(); - expect(log).toHaveLength(2); - expect(log[0].commitId).not.toStrictEqual(commit); + const f = async (ctx: ContextTimed) => { + const commit = (await vault.log())[0].commitId; + const gen = vault.writeG( + async function* (efs): AsyncGenerator { + yield await efs.writeFile('secret-1', 'secret-content'); + }, + undefined, + ctx, + ); + for await (const _ of gen) { + // Do nothing + } + const log = await vault.log(); + expect(log).toHaveLength(2); + expect(log[0].commitId).not.toStrictEqual(commit); + }; + await timedCancellableF(f, true)(); }); test('no commit added if no mutation in writeG', async () => { - const commit = (await vault.log())[0].commitId; - const gen = vault.writeG(async function* (_efs): AsyncGenerator {}); - for await (const _ of gen) { - // Do nothing - } - const log = await vault.log(); - expect(log).toHaveLength(1); - expect(log[0].message).not.toContain('secret-1'); - expect(log[0].commitId).toStrictEqual(commit); + const f = async (ctx: ContextTimed) => { + const commit = (await vault.log())[0].commitId; + const gen = vault.writeG( + async function* (_efs): AsyncGenerator {}, + undefined, + ctx, + ); + for await (const _ of gen) { + // Do nothing + } + const log = await vault.log(); + expect(log).toHaveLength(1); + expect(log[0].message).not.toContain('secret-1'); + expect(log[0].commitId).toStrictEqual(commit); + }; + await timedCancellableF(f, true)(); }); test('no mutation to vault when part of a commit operation fails in writeG', async () => { - const gen = vault.writeG(async function* (efs): AsyncGenerator { - yield await efs.writeFile(secret1.name, secret1.content); - yield await efs.rename('notValid', 'randomName'); // Throws - }); - // Failing commit operation - await expect(() => consumeGenerator(gen)).rejects.toThrow(); - - // Make sure secret1 wasn't written when the above commit failed - await vault.readF(async (efs) => { - expect(await efs.readdir('.')).not.toContain(secret1.name); - }); - // No new commit - expect(await vault.log()).toHaveLength(1); + const f = async (ctx: ContextTimed) => { + const gen = vault.writeG( + async function* (efs): AsyncGenerator { + yield await efs.writeFile(secret1.name, secret1.content); + yield await efs.rename('notValid', 'randomName'); // Throws + }, + undefined, + ctx, + ); + // Failing commit operation + await expect(() => consumeGenerator(gen)).rejects.toThrow(); + // Make sure secret1 wasn't written when the above commit failed + await vault.readF(async (efs) => { + expect(await efs.readdir('.')).not.toContain(secret1.name); + }); + // No new commit + expect(await vault.log()).toHaveLength(1); + }; + await timedCancellableF(f, true)(); }); test('no commit after readG', async () => { await vault.writeF(async (efs) => { @@ -734,8 +756,10 @@ describe('VaultInternal', () => { for (const logElement of log) { refs.push(await quickCommit(logElement.commitId, `secret-${num++}`)); } + const abortController = new AbortController(); + const ctx = { signal: abortController.signal }; // @ts-ignore: protected method - await vault.garbageCollectGitObjectsGlobal(); + await vault.garbageCollectGitObjectsGlobal(ctx); for (const ref of refs) { await expect( @@ -778,35 +802,46 @@ describe('VaultInternal', () => { expect(finished).toBe(true); }); test('writeG respects read and write locking', async () => { - const lock = vault.getLock(); - // Hold a write lock - const [releaseWrite] = await lock.write()(); + const f = async (ctx: ContextTimed) => { + // Hold a write lock + const lock = vault.getLock(); + const [releaseWrite] = await lock.write()(); - let finished = false; - const writeGen = vault.writeG(async function* () { - yield; - finished = true; - yield; - }); - const runP = consumeGenerator(writeGen); - await sleep(waitDelay); - expect(finished).toBe(false); - await releaseWrite(); - await runP; - expect(finished).toBe(true); + let finished = false; + const writeGen = vault.writeG( + async function* () { + yield; + finished = true; + yield; + }, + undefined, + ctx, + ); + const runP = consumeGenerator(writeGen); + await sleep(waitDelay); + expect(finished).toBe(false); + await releaseWrite(); + await runP; + expect(finished).toBe(true); - const [releaseRead] = await lock.read()(); - finished = false; - const writeGen2 = vault.writeG(async function* () { - yield; - finished = true; - yield; - }); - const runP2 = consumeGenerator(writeGen2); - await sleep(waitDelay); - await releaseRead(); - await runP2; - expect(finished).toBe(true); + const [releaseRead] = await lock.read()(); + finished = false; + const writeGen2 = vault.writeG( + async function* () { + yield; + finished = true; + yield; + }, + undefined, + ctx, + ); + const runP2 = consumeGenerator(writeGen2); + await sleep(waitDelay); + await releaseRead(); + await runP2; + expect(finished).toBe(true); + }; + await timedCancellableF(f, true)(); }); test('readF respects write locking', async () => { const lock = vault.getLock(); @@ -917,40 +952,49 @@ describe('VaultInternal', () => { await releaseRead(); }); test('can acquire a write resource', async () => { - const acquireWrite = vault.acquireWrite(); - await withF([acquireWrite], async ([efs]) => { - await efs.writeFile(secret1.name, secret1.content); - }); - await vault.readF(async (efs) => { - const content = await efs.readFile(secret1.name); - expect(content.toString()).toEqual(secret1.content); - }); + const f = async (ctx: ContextTimed) => { + const acquireWrite = vault.acquireWrite(undefined, ctx); + await withF([acquireWrite], async ([efs]) => { + await efs.writeFile(secret1.name, secret1.content); + }); + await vault.readF(async (efs) => { + const content = await efs.readFile(secret1.name); + expect(content.toString()).toEqual(secret1.content); + }); + }; + await timedCancellableF(f, true)(); }); test('acquiring write resource respects write locking', async () => { - const lock = vault.getLock(); - const [releaseWrite] = await lock.write()(); - let finished = false; - const writeP = withF([vault.acquireWrite()], async () => { - finished = true; - }); - await sleep(waitDelay); - expect(finished).toBe(false); - await releaseWrite(); - await writeP; - expect(finished).toBe(true); + const f = async (ctx: ContextTimed) => { + const lock = vault.getLock(); + const [releaseWrite] = await lock.write()(); + let finished = false; + const writeP = withF([vault.acquireWrite(undefined, ctx)], async () => { + finished = true; + }); + await sleep(waitDelay); + expect(finished).toBe(false); + await releaseWrite(); + await writeP; + expect(finished).toBe(true); + }; + await timedCancellableF(f, true)(); }); test('acquiring write resource respects read locking', async () => { - const lock = vault.getLock(); - const [releaseRead] = await lock.read()(); - let finished = false; - const writeP = withF([vault.acquireWrite()], async () => { - finished = true; - }); - await sleep(waitDelay); - expect(finished).toBe(false); - await releaseRead(); - await writeP; - expect(finished).toBe(true); + const f = async (ctx: ContextTimed) => { + const lock = vault.getLock(); + const [releaseRead] = await lock.read()(); + let finished = false; + const writeP = withF([vault.acquireWrite(undefined, ctx)], async () => { + finished = true; + }); + await sleep(waitDelay); + expect(finished).toBe(false); + await releaseRead(); + await writeP; + expect(finished).toBe(true); + }; + await timedCancellableF(f, true)(); }); // Life-cycle test('can create with CreateVaultInternal', async () => { diff --git a/tests/vaults/VaultManager.test.ts b/tests/vaults/VaultManager.test.ts index 1032ba16e..1385c14ea 100644 --- a/tests/vaults/VaultManager.test.ts +++ b/tests/vaults/VaultManager.test.ts @@ -1,3 +1,4 @@ +import type { ContextTimed } from '@matrixai/contexts'; import type { NodeId } from '@/ids/types'; import type { VaultAction, @@ -12,12 +13,12 @@ import type { AgentServerManifest } from '@/nodes/agent/handlers'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import git from 'isomorphic-git'; import { IdInternal } from '@matrixai/id'; import { DB } from '@matrixai/db'; import { destroyed, running } from '@matrixai/async-init'; -import git from 'isomorphic-git'; import { RWLockWriter } from '@matrixai/async-locks'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import TaskManager from '@/tasks/TaskManager'; import ACL from '@/acl/ACL'; import GestaltGraph from '@/gestalts/GestaltGraph'; @@ -26,12 +27,12 @@ import NodeConnectionManager from '@/nodes/NodeConnectionManager'; import KeyRing from '@/keys/KeyRing'; import PolykeyAgent from '@/PolykeyAgent'; import VaultManager from '@/vaults/VaultManager'; -import * as vaultsErrors from '@/vaults/errors'; import NodeGraph from '@/nodes/NodeGraph'; -import * as vaultsUtils from '@/vaults/utils'; -import { sleep } from '@/utils'; import VaultInternal from '@/vaults/VaultInternal'; +import { sleep } from '@/utils'; import * as keysUtils from '@/keys/utils'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; import * as nodeTestUtils from '../nodes/utils'; import * as testUtils from '../utils'; import * as tlsTestsUtils from '../utils/tls'; @@ -351,8 +352,10 @@ describe('VaultManager', () => { await acl.setVaultAction(vault2, nodeId1, 'clone'); // No permissions for vault3 - // scanning vaults - const gen = vaultManager.handleScanVaults(nodeId1); + // Scanning vaults + const abortController = new AbortController(); + const ctx = { signal: abortController.signal } as ContextTimed; + const gen = vaultManager.handleScanVaults(nodeId1, undefined, ctx); const vaults: Record = {}; for await (const vault of gen) { vaults[vault.vaultId] = [vault.vaultName, vault.vaultPermissions]; @@ -363,14 +366,26 @@ describe('VaultManager', () => { // Should throw due to no permission await expect(async () => { - for await (const _ of vaultManager.handleScanVaults(nodeId2)) { + const abortController = new AbortController(); + const ctx = { signal: abortController.signal } as ContextTimed; + for await (const _ of vaultManager.handleScanVaults( + nodeId2, + undefined, + ctx, + )) { // Should throw } }).rejects.toThrow(vaultsErrors.ErrorVaultsPermissionDenied); // Should throw due to lack of scan permission await gestaltGraph.setGestaltAction(['node', nodeId2], 'notify'); await expect(async () => { - for await (const _ of vaultManager.handleScanVaults(nodeId2)) { + const abortController = new AbortController(); + const ctx = { signal: abortController.signal } as ContextTimed; + for await (const _ of vaultManager.handleScanVaults( + nodeId2, + undefined, + ctx, + )) { // Should throw } }).rejects.toThrow(vaultsErrors.ErrorVaultsPermissionDenied); diff --git a/tests/vaults/VaultOps/updatesecret.test.ts b/tests/vaults/VaultOps/updatesecret.test.ts deleted file mode 100644 index 89befde9a..000000000 --- a/tests/vaults/VaultOps/updatesecret.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { VaultId } from '@/vaults/types'; -import type { Vault } from '@/vaults/Vault'; -import type KeyRing from '@/keys/KeyRing'; -import type { LevelPath } from '@matrixai/db'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { EncryptedFS } from 'encryptedfs'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { DB } from '@matrixai/db'; -import VaultInternal from '@/vaults/VaultInternal'; -import * as vaultOps from '@/vaults/VaultOps'; -import * as vaultsErrors from '@/vaults/errors'; -import * as vaultsUtils from '@/vaults/utils'; -import * as keysUtils from '@/keys/utils'; -import * as testNodesUtils from '../../nodes/utils'; -import * as testVaultsUtils from '../utils'; - -describe('updateSecret', () => { - const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); - - const secretName = 'secret'; - const secretNameHidden = '.secret'; - const secretContent = 'secret-content'; - const secretContentNew = 'secret-content-new'; - const dirName = 'dir'; - const dirNameHidden = '.dir'; - - let dataDir: string; - let baseEfs: EncryptedFS; - let vaultId: VaultId; - let vaultInternal: VaultInternal; - let vault: Vault; - let db: DB; - let vaultsDbPath: LevelPath; - const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); - const dummyKeyRing = { - getNodeId: () => { - return testNodesUtils.generateRandomNodeId(); - }, - } as KeyRing; - - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - const dbPath = path.join(dataDir, 'efsDb'); - const dbKey = keysUtils.generateKey(); - baseEfs = await EncryptedFS.createEncryptedFS({ - dbKey, - dbPath, - logger, - }); - await baseEfs.start(); - - vaultId = vaultIdGenerator(); - await baseEfs.mkdir( - path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), - { - recursive: true, - }, - ); - db = await DB.createDB({ - dbPath: path.join(dataDir, 'db'), - logger, - }); - vaultsDbPath = ['vaults']; - vaultInternal = await VaultInternal.createVaultInternal({ - keyRing: dummyKeyRing, - vaultId, - efs: baseEfs, - logger: logger.getChild(VaultInternal.name), - fresh: true, - db, - vaultsDbPath: vaultsDbPath, - vaultName: 'VaultName', - }); - vault = vaultInternal as Vault; - }); - afterEach(async () => { - await vaultInternal.stop(); - await vaultInternal.destroy(); - await db.stop(); - await db.destroy(); - await baseEfs.stop(); - await baseEfs.destroy(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - test('updating secret content', async () => { - await testVaultsUtils.writeSecret(vault, secretName, secretContent); - await vaultOps.updateSecret(vault, secretName, secretContentNew); - await testVaultsUtils.expectSecret(vault, secretName, secretContentNew); - }); - test('updating secret content within a directory', async () => { - const secretPath = path.join(dirName, secretName); - await testVaultsUtils.writeSecret(vault, secretPath, secretContent); - await vaultOps.updateSecret(vault, secretPath, secretContentNew); - await testVaultsUtils.expectSecret(vault, secretPath, secretContentNew); - }); - test( - 'updating a secret multiple times', - async () => { - await vaultOps.addSecret(vault, 'secret-1', 'secret-content'); - await testVaultsUtils.writeSecret(vault, secretName, secretContent); - for (let i = 0; i < 5; i++) { - const contentNew = `${secretContentNew}${i}`; - await vaultOps.updateSecret(vault, secretName, contentNew); - await testVaultsUtils.expectSecret(vault, secretName, contentNew); - } - }, - globalThis.defaultTimeout * 2, - ); - test('updating a secret that does not exist should fail', async () => { - await expect( - vaultOps.updateSecret(vault, secretName, secretContentNew), - ).rejects.toThrow(vaultsErrors.ErrorSecretsSecretUndefined); - }); - test('updating hidden secret content', async () => { - await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); - await vaultOps.updateSecret(vault, secretNameHidden, secretContentNew); - await testVaultsUtils.expectSecret( - vault, - secretNameHidden, - secretContentNew, - ); - }); - test('updating hidden secret content within a hidden directory', async () => { - const secretPathHidden = path.join(dirNameHidden, secretNameHidden); - await testVaultsUtils.writeSecret(vault, secretPathHidden, secretContent); - await vaultOps.updateSecret(vault, secretPathHidden, secretContentNew); - await testVaultsUtils.expectSecret( - vault, - secretPathHidden, - secretContentNew, - ); - }); -});