diff --git a/apps/server/src/cli/environment.test.ts b/apps/server/src/cli/environment.test.ts new file mode 100644 index 0000000000000..2b94b4d860310 --- /dev/null +++ b/apps/server/src/cli/environment.test.ts @@ -0,0 +1,55 @@ +import { NodeHttpServer } from "@effect/platform-node"; +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as NetService from "@t3tools/shared/Net"; +import { expect, it } from "@effect/vitest"; +import * as ConfigProvider from "effect/ConfigProvider"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as TestConsole from "effect/testing/TestConsole"; +import { Command } from "effect/cli"; + +import { cli } from "../binCli.ts"; +import { descriptorOf, servePeer } from "../peer/PeerLinks.testkit.ts"; + +const runCli = (args: ReadonlyArray, env: Record) => + Command.runWith(cli, { version: "0.0.0" })(args).pipe( + Effect.andThen(TestConsole.logLines), + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + NodeCrypto.layer, + NetService.layer, + TestConsole.layer, + ConfigProvider.layer(ConfigProvider.fromEnv({ env })), + ), + ), + ); + +it.effect("links with the pairing code from T3CODE_PAIRING_CODE, or --pairing-code over it", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const peer = yield* servePeer(descriptorOf("cli-link-peer", "Box"), Layer.empty); + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-environment-link-" }); + yield* fs.makeDirectory(path.join(baseDir, "userdata")); + yield* fs.writeFileString( + path.join(baseDir, "userdata", "settings.json"), + JSON.stringify({ linkedEnvironmentsEnabled: true }), + ); + const link = ["environment", "link", peer.url, "--base-dir", baseDir]; + + const spent = (yield* peer.auth.issuePairingCredential()).credential; + expect(yield* runCli(link, { T3CODE_PAIRING_CODE: spent })).toContainEqual( + expect.stringContaining("Linked Box (cli-link-peer)"), + ); + + // The env code is spent now, so this links only if the flag wins. + const fresh = (yield* peer.auth.issuePairingCredential()).credential; + expect( + yield* runCli([...link, "--pairing-code", fresh], { T3CODE_PAIRING_CODE: spent }), + ).toContainEqual(expect.stringContaining("Linked Box (cli-link-peer)")); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), +); diff --git a/apps/server/src/cli/environment.ts b/apps/server/src/cli/environment.ts index 7d7e35b0bea20..fd3a0064f991e 100644 --- a/apps/server/src/cli/environment.ts +++ b/apps/server/src/cli/environment.ts @@ -1,8 +1,10 @@ import { AuthMcpClientAccess, EnvironmentId, type PeerLinkSummary } from "@t3tools/contracts"; +import * as Config from "effect/Config"; import * as Console from "effect/Console"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; import * as References from "effect/References"; import * as Schema from "effect/Schema"; import * as Argument from "effect/cli/Argument"; @@ -93,8 +95,11 @@ const environmentLinkCommand = Command.make("link", { Flag.withDescription("Another address for the same environment, tried after the first."), Flag.atLeast(0), ), - pairingCode: Flag.String("pairing-code").pipe( - Flag.withDescription("A pairing code from the other environment (`t3 pair` there)."), + pairingCode: Flag.Redacted("pairing-code").pipe( + Flag.withDescription( + "A pairing code from `t3 pair` in the other environment (equivalent to T3CODE_PAIRING_CODE).", + ), + Flag.withFallbackConfig(Config.Redacted("T3CODE_PAIRING_CODE")), ), access: Flag.Literals("access", AuthMcpClientAccess.literals).pipe( Flag.withDescription( @@ -112,7 +117,7 @@ const environmentLinkCommand = Command.make("link", { const linked = yield* links.link({ url: flags.url, alternateUrls: flags.alternateUrl, - pairingCode: flags.pairingCode, + pairingCode: Redacted.value(flags.pairingCode), access: flags.access, }); yield* Console.log( diff --git a/docs/user/remote-access.md b/docs/user/remote-access.md index 29b7650d41907..ff6a72f196b38 100644 --- a/docs/user/remote-access.md +++ b/docs/user/remote-access.md @@ -369,8 +369,29 @@ holds the machines this app knows and those on your T3 Connect account. If this app has an address for that machine and is connected to it with a session that manages access there, it makes the pairing code itself; otherwise paste one from that machine's **Settings → Sharing & access**. An address you type always -needs a pasted code. From a terminal, use -`t3 environment link
--pairing-code --access `. +needs a pasted code. From a terminal, read the code into the environment +variable without echoing it, then run the link. In bash or zsh: + +```sh +IFS= read -rs T3CODE_PAIRING_CODE # paste the code, then press Enter +export T3CODE_PAIRING_CODE +t3 environment link
--access +unset T3CODE_PAIRING_CODE +``` + +In PowerShell 7.1 or later: + +```powershell +$env:T3CODE_PAIRING_CODE = Read-Host -MaskInput "Pairing code" +t3 environment link
--access +Remove-Item Env:T3CODE_PAIRING_CODE +``` + +In fish, read it with `read --global --silent --export T3CODE_PAIRING_CODE` and +clear it afterwards with `set --erase --global T3CODE_PAIRING_CODE`. + +You can pass the code with `--pairing-code ` instead, but entering it on +the command line exposes it in process arguments and shell history. You can also ask an agent to link a machine by name, such as "delegate this to my vps". It shows a card in the thread where you pick the machine the same way