Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ amesh invite 482916

Trust is one-way: the controller can authenticate to the target, but not the reverse.

> **Remote device?** If you can't run interactive commands on the target, use `amesh provision` to generate a bootstrap token instead. See the [Integration Guide](./docs/integration-guide.md#pairing-remote-machines).

### 3. Sign requests (2 lines)

```typescript
Expand Down
20 changes: 20 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,26 @@ cd packages/relay
bun test
```

### Pairing Troubleshooting

**"Device already in allow list"**
The device was previously paired. The CLI will automatically update the existing entry with fresh handshake data. If you need to start clean, run `amesh revoke <device-id>` first.

**Pairing code expired**
Codes expire after 60 seconds. Run `amesh listen` again to generate a new code.

**SAS code mismatch**
The 6-digit verification code didn't match. This could indicate a MITM attack, or simply that the wrong code was entered. No changes were made — run `amesh listen` again to retry.

**One-sided trust (paired on one side but not the other)**
This can happen if one side crashed mid-pairing. Run `amesh list` on both machines. Revoke the stale entry with `amesh revoke <device-id>`, then re-pair.

**Can't run interactive commands on the target?**
Use `amesh provision` on the controller to generate a bootstrap token. Set `AMESH_BOOTSTRAP_TOKEN` on the target — pairing happens automatically. See [Integration Guide — Pairing Remote Machines](./integration-guide.md#pairing-remote-machines).

**Timed out waiting for the other device**
Both devices must be running and connected to the same relay. The default relay is `wss://relay.authmesh.dev/ws`. Use `--relay` to override on both sides.

---

## 8. Start the Relay Server
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export default class Grant extends Command {
const data = await allowList.read();
const device = data.devices.find((d) => d.deviceId === args.deviceId);
if (!device) {
this.error(`Device ${args.deviceId} not found in allow list.`);
this.error(
`Device ${args.deviceId} not found in allow list.\n` +
'Run `amesh list` to see paired devices.\n' +
"Note: grant runs on the target — you're granting a controller permission to access this device.",
);
}

await allowList.updatePermissions(args.deviceId, { shell: flags.shell });
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,14 @@ export default class Init extends Command {

this.log('');
this.log('Next steps:');
this.log(' Target: run `amesh listen`, then `amesh invite` from your controller');
this.log(' Controller: run `amesh listen` on a target first, then `amesh invite` here');
this.log(
' This is the server: run `amesh listen`, then `amesh invite <code>` from your laptop',
);
this.log(
' This is your laptop: run `amesh listen` on the server first, then `amesh invite <code>` here',
);
this.log(
' Remote / scripted: run `amesh provision` on the controller for non-interactive pairing',
);
}
}
36 changes: 31 additions & 5 deletions packages/cli/src/commands/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,20 @@ export default class Invite extends Command {
signFn,
);
} catch (err) {
this.error(`Handshake failed: ${(err as Error).message}`);
const msg = (err as Error).message;
if (msg.includes('Timeout')) {
this.error(
'Timed out waiting for the target device.\n' +
'Make sure `amesh listen` is running on the target and both devices use the same relay.',
);
} else if (msg.includes('otc_not_found') || msg.includes('otc_expired')) {
this.error(
'Pairing code not found or expired.\n' +
'Run `amesh listen` again on the target for a fresh code.',
);
} else {
this.error(`Handshake failed: ${msg}`);
}
}

this.log(' Peer found.');
Expand All @@ -65,14 +78,27 @@ export default class Invite extends Command {
this.log(' └──────────────────────────────────┘');
this.log('');

await allowList.addDevice({
const newDevice = {
deviceId: generateDeviceId(result.peerPublicKey),
publicKey: Buffer.from(result.peerPublicKey).toString('base64'),
friendlyName: result.peerFriendlyName,
addedAt: new Date().toISOString(),
addedBy: 'handshake',
role: 'target',
});
addedBy: 'handshake' as const,
role: 'target' as const,
};

try {
await allowList.addDevice(newDevice);
} catch (err) {
if ((err as Error).message.includes('already in allow list')) {
this.log(` "${result.peerFriendlyName}" is already in your allow list.`);
this.log(' Updating with fresh handshake data...');
await allowList.removeDevice(newDevice.deviceId);
await allowList.addDevice(newDevice);
} else {
throw err;
}
}

this.log('');
this.log(` "${result.peerFriendlyName}" added as target.`);
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export default class List extends Command {
);
}
this.log(' ' + '─'.repeat(55));
this.log('');
this.log(' [controller] = can authenticate TO this device');
this.log(' [target] = this device can authenticate TO it');
}

this.log('');
Expand Down
26 changes: 20 additions & 6 deletions packages/cli/src/commands/listen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export default class Listen extends Command {
this.log('');
this.log(' Share this code with your Controller device.');
this.log('');
this.log(" Tip: Can't run interactive commands? Use `amesh provision` instead.");
this.log(' Run `amesh provision --help` on the controller for details.');
this.log('');

const signFn = async (message: Uint8Array) => {
return keyStore.sign(keyAlias, message);
Expand All @@ -52,23 +55,34 @@ export default class Listen extends Command {
signFn,
);
} catch (err) {
this.error(`Handshake failed: ${(err as Error).message}`);
const msg = (err as Error).message;
if (msg.includes('Timeout')) {
this.error(
'Timed out waiting for a controller to connect.\n' +
'Make sure the controller runs `amesh invite <code>` within 60 seconds.',
);
} else {
this.error(`Handshake failed: ${msg}`);
}
}

this.log(' Controller connected.');
this.log(' Ephemeral P-256 ECDH tunnel established.');
this.log(' Keys exchanged and verified.');
this.log('');
this.log(' ┌──────────────────────────────────┐');
this.log(' │ Enter the 6-digit code shown │');
this.log(" │ on the Controller's screen. │");
this.log(' └──────────────────────────────────┘');
this.log(' ┌────────────────────────────────────────────┐');
this.log(' │ Enter the 6-digit code shown on the │');
this.log(" │ Controller's screen to complete pairing. │");
this.log(' │ │');
this.log(' │ Press Ctrl+C to cancel without changes. │');
this.log(' └────────────────────────────────────────────┘');
this.log('');

const entered = await this.prompt(' Verification code: ');
if (!verifySAS(entered.trim(), result.sas)) {
this.log('');
this.log(' Code mismatch — possible MITM. Pairing aborted.');
this.log(' Code mismatch — possible MITM attack. Pairing aborted.');
this.log(' No changes were made. Run `amesh listen` again to retry.');
return;
}

Expand Down
Loading