Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/nodes/CommandJoin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type PolykeyClient from 'polykey/PolykeyClient.js';
import type { Hostname } from 'polykey/network/types.js';
import type { NodeIdEncoded, NodeAddress } from 'polykey/nodes/types.js';
import CommandPolykey from '../CommandPolykey.js';
import * as binUtils from '../utils/index.js';
import * as binOptions from '../utils/options.js';
import * as binProcessors from '../utils/processors.js';

class CommandJoin extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('join');
this.description('Join a network');
this.argument('<network>', 'Name of the network to join');
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.action(async (network: string, options) => {
const { default: PolykeyClient } = await import(
'polykey/PolykeyClient.js'
);
const nodesUtils = await import('polykey/nodes/utils.js');
const utils = await import('polykey/utils/index.js');
const clientOptions = await binProcessors.processClientOptions(
options.nodePath,
options.nodeId,
options.clientHost,
options.clientPort,
this.fs,
this.logger.getChild(binProcessors.processClientOptions.name),
);
const auth = await binProcessors.processAuthentication(
options.passwordFile,
this.fs,
);

let pkClient: PolykeyClient;
this.exitHandlers.handlers.push(async () => {
if (pkClient != null) await pkClient.stop();
});
try {
pkClient = await PolykeyClient.createPolykeyClient({
nodeId: clientOptions.nodeId,
host: clientOptions.clientHost,
port: clientOptions.clientPort,
options: {
nodePath: options.nodePath,
},
logger: this.logger.getChild(PolykeyClient.name),
});
const seedNodes = await nodesUtils.resolveSeednodes(
network as Hostname,
);
const initialNodes = Object.entries(seedNodes) as Array<
[NodeIdEncoded, NodeAddress]
>;
await binUtils.retryAuthentication(
(auth) =>
pkClient.rpcClient.methods.nodesSyncGraph({
network,
initialNodes,
metadata: auth,
}),
auth,
);
process.stdout.write(`Switched to network ${network}`);
} finally {
if (pkClient! != null) await pkClient.stop();
}
});
}
}

export default CommandJoin;
2 changes: 2 additions & 0 deletions src/nodes/CommandNodes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CommandAdd from './CommandAdd.js';
import CommandClaim from './CommandClaim.js';
import CommandFind from './CommandFind.js';
import CommandJoin from './CommandJoin.js';
import CommandPing from './CommandPing.js';
import CommandGetAll from './CommandGetAll.js';
import CommandConnections from './CommandConnections.js';
Expand All @@ -14,6 +15,7 @@ class CommandNodes extends CommandPolykey {
this.addCommand(new CommandAdd(...args));
this.addCommand(new CommandClaim(...args));
this.addCommand(new CommandFind(...args));
this.addCommand(new CommandJoin(...args));
this.addCommand(new CommandPing(...args));
this.addCommand(new CommandGetAll(...args));
this.addCommand(new CommandConnections(...args));
Expand Down
125 changes: 125 additions & 0 deletions tests/nodes/join.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { NodeId } from 'polykey/ids/types.js';
import type { SeedNodes } from 'polykey/nodes/types.js';
import path from 'node:path';
import fs from 'node:fs';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import { encodeNodeId, decodeNodeId } from 'polykey/nodes/utils.js';
import { verifyClaimNetworkAuthority } from 'polykey/claims/payloads/claimNetworkAuthority.js';
import { jest } from '@jest/globals';
import PolykeyAgent from 'polykey/PolykeyAgent.js';
import * as keysUtils from 'polykey/keys/utils/index.js';
import * as testUtils from '../utils/index.js';

let seedNodeId: NodeId;
let seedNodeIdEncoded = '';
let seedNodeHost = '';
let seedNodePort = 0;
jest.unstable_mockModule('polykey/nodes/utils.js', () => {
return {
__esModule: true,
decodeNodeId,
resolveSeednodes: jest
.fn<() => Promise<SeedNodes>>()
.mockImplementation(async () => {
const nodes: SeedNodes = {};
nodes[seedNodeIdEncoded] = [seedNodeHost, seedNodePort];
return nodes;
}),
};
});

describe('join', () => {
const logger = new Logger('join test', LogLevel.WARN, [new StreamHandler()]);
const password = 'helloworld';
let dataDir: string;
let nodePath: string;
let polykeyAgent: PolykeyAgent;
let seedNode: PolykeyAgent;
let seedNodeClaimNetworkAuthority;
let networkKeyPair;
let networkNodeId;
let network = 'test.network.com';
beforeEach(async () => {
dataDir = await fs.promises.mkdtemp(
path.join(globalThis.tmpDir, 'polykey-test-'),
);
networkKeyPair = keysUtils.generateKeyPair();
networkNodeId = keysUtils.publicKeyToNodeId(networkKeyPair.publicKey);
network = 'test.network.com';
nodePath = path.join(dataDir, 'keynode');
polykeyAgent = await PolykeyAgent.createPolykeyAgent({
password,
options: {
seedNodes: {}, // Explicitly no seed nodes on startup
nodePath,
agentServiceHost: '127.0.0.1',
clientServiceHost: '127.0.0.1',
keys: {
passwordOpsLimit: keysUtils.passwordOpsLimits.min,
passwordMemLimit: keysUtils.passwordMemLimits.min,
strictMemoryLock: false,
},
},
logger,
});
// Setting up a remote seednode
seedNode = await PolykeyAgent.createPolykeyAgent({
password,
options: {
nodePath: path.join(dataDir, 'seednode'),
agentServiceHost: '127.0.0.1',
clientServiceHost: '127.0.0.1',
keys: {
passwordOpsLimit: keysUtils.passwordOpsLimits.min,
passwordMemLimit: keysUtils.passwordMemLimits.min,
strictMemoryLock: false,
},
},
logger,
});
[, seedNodeClaimNetworkAuthority] =
await seedNode.nodeManager.createClaimNetworkAuthority(
networkNodeId,
network,
false,
async (claim) => {
claim.signWithPrivateKey(networkKeyPair.privateKey);
return claim;
},
);
await seedNode.nodeManager.createSelfSignedClaimNetworkAccess(
seedNodeClaimNetworkAuthority,
);
await testUtils.nodesConnect(polykeyAgent, seedNode);
seedNodeId = seedNode.keyRing.getNodeId();
seedNodeHost = seedNode.agentServiceHost;
seedNodePort = seedNode.agentServicePort;
seedNodeIdEncoded = encodeNodeId(seedNodeId);
});
afterEach(async () => {
jest.restoreAllMocks();
await polykeyAgent.stop();
await seedNode.stop();
await fs.promises.rm(dataDir, {
force: true,
recursive: true,
});
});
test('should connect to a seednode', async () => {
const command = ['nodes', 'join', '-np', nodePath, network];
const result = await testUtils.pkStdio(command, {
env: { PK_PASSWORD: password },
cwd: dataDir,
});
expect(result.exitCode).toBe(0);

expect(() =>
verifyClaimNetworkAuthority(
networkNodeId,
seedNodeId,
network,
seedNodeClaimNetworkAuthority,
),
).not.toThrow();
});
});