diff --git a/.changeset/long-rabbits-roll.md b/.changeset/long-rabbits-roll.md new file mode 100644 index 000000000..8589a10ce --- /dev/null +++ b/.changeset/long-rabbits-roll.md @@ -0,0 +1,33 @@ +--- +"@logto/cli": minor +--- + +add new cli command to setup Logto tunnel service for developing and debugging custom ui on your local machine + +This command will establish a tunnel service between the following 3 entities: Logto cloud auth services, your application, and your custom sign-in UI. + +Assuming you have a custom sign-in page running on `http://localhost:4000`, then you can execute the command this way: + +```bash +logto tunnel --endpoint https://.logto.app --port 9000 --experience-uri http://localhost:4000 +``` + +Or if you don't have your custom UI pages hosted on a dev server, you can use the `--experience-path` option to specify the path to your static files: + +```bash +logto tunnel --endpoint https://.logto.app --port 9000 --experience-path /path/to/your/custom/ui +``` + +This command also works if you have enabled custom domain in your Logto tenant. E.g.: + +```bash +logto tunnel --endpoint https://your-custom-domain.com --port 9000 --experience-path /path/to/your/custom/ui +``` + +This should set up the tunnel and it will be running on your local machine at `http://localhost:9000/`. + +Finally, run your application and set its endpoint in Logto config to the tunnel address `http://localhost:9000/` instead. + +If all set up correctly, when you click the "sign-in" button in your application, you should be navigated to your custom sign-in page instead of Logto's built-in UI, along with valid session (cookies) that allows you to further interact with Logto experience API. + +Refer to the [documentation](https://docs.logto.dev/docs/references/using-cli/tunnel/) for more details. diff --git a/packages/cli/src/commands/proxy/index.ts b/packages/cli/src/commands/tunnel/index.ts similarity index 82% rename from packages/cli/src/commands/proxy/index.ts rename to packages/cli/src/commands/tunnel/index.ts index a12428b3e..5ae0b2a68 100644 --- a/packages/cli/src/commands/proxy/index.ts +++ b/packages/cli/src/commands/tunnel/index.ts @@ -7,7 +7,7 @@ import type { CommandModule } from 'yargs'; import { consoleLog } from '../../utils.js'; -import { type ProxyCommandArgs } from './types.js'; +import { type TunnelCommandArgs } from './types.js'; import { checkExperienceInput, createLogtoResponseHandler, @@ -16,9 +16,9 @@ import { isLogtoRequestPath, } from './utils.js'; -const proxy: CommandModule = { - command: ['proxy'], - describe: 'Command for Logto proxy', +const tunnel: CommandModule = { + command: ['tunnel'], + describe: 'Command for Logto tunnel', builder: (yargs) => yargs .options({ @@ -40,7 +40,8 @@ const proxy: CommandModule = { }, port: { alias: 'p', - describe: 'The port number where the proxy server will be running on. Defaults to 9000.', + describe: + 'The port number where the tunnel service will be running on. Defaults to 9000.', type: 'number', default: 9000, }, @@ -59,7 +60,7 @@ const proxy: CommandModule = { consoleLog.fatal('A valid Logto endpoint URI must be provided.'); } const logtoEndpointUrl = new URL(endpoint); - const proxyUrl = new URL(`http://localhost:${port}`); + const tunnelServiceUrl = new URL(`http://localhost:${port}`); const proxyLogtoRequest = createProxy( logtoEndpointUrl.href, @@ -69,7 +70,7 @@ const proxy: CommandModule = { request, response, logtoEndpointUrl, - proxyUrl, + tunnelServiceUrl, verbose, }) ); @@ -81,7 +82,7 @@ const proxy: CommandModule = { consoleLog.info(`Incoming request: ${chalk.blue(request.method, request.url)}`); } - // Proxy the requests to Logto endpoint + // Tunneling the requests to Logto endpoint if (isLogtoRequestPath(request.url)) { void proxyLogtoRequest(request, response); return; @@ -98,9 +99,9 @@ const proxy: CommandModule = { }); server.listen(port, () => { - consoleLog.info(`Proxy server is running on ${chalk.blue(proxyUrl.href)}`); + consoleLog.info(`Logto tunnel is running on ${chalk.blue(tunnelServiceUrl.href)}`); }); }, }; -export default proxy; +export default tunnel; diff --git a/packages/cli/src/commands/proxy/types.ts b/packages/cli/src/commands/tunnel/types.ts similarity index 76% rename from packages/cli/src/commands/proxy/types.ts rename to packages/cli/src/commands/tunnel/types.ts index af80b4792..9f24b72d3 100644 --- a/packages/cli/src/commands/proxy/types.ts +++ b/packages/cli/src/commands/tunnel/types.ts @@ -1,6 +1,6 @@ import type * as http from 'node:http'; -export type ProxyCommandArgs = { +export type TunnelCommandArgs = { 'experience-uri'?: string; 'experience-path'?: string; endpoint?: string; @@ -8,11 +8,11 @@ export type ProxyCommandArgs = { verbose: boolean; }; -export type ProxyResponseHandler = { +export type LogtoResponseHandler = { proxyResponse: http.IncomingMessage; request: http.IncomingMessage; response: http.ServerResponse; logtoEndpointUrl: URL; - proxyUrl: URL; + tunnelServiceUrl: URL; verbose: boolean; }; diff --git a/packages/cli/src/commands/proxy/utils.ts b/packages/cli/src/commands/tunnel/utils.ts similarity index 93% rename from packages/cli/src/commands/proxy/utils.ts rename to packages/cli/src/commands/tunnel/utils.ts index 28283be84..22fe3ada0 100644 --- a/packages/cli/src/commands/proxy/utils.ts +++ b/packages/cli/src/commands/tunnel/utils.ts @@ -12,7 +12,7 @@ import mime from 'mime'; import { consoleLog } from '../../utils.js'; -import { type ProxyResponseHandler } from './types.js'; +import { type LogtoResponseHandler } from './types.js'; export const createProxy = (targetUrl: string, onProxyResponse?: OnProxyEvent['proxyRes']) => { const hasResponseHandler = Boolean(onProxyResponse); @@ -65,7 +65,7 @@ export const createStaticFileProxy = /** * Intercept the response from Logto endpoint and replace Logto endpoint URLs in the response with the - * proxy URL. The string replace happens in the following cases: + * tunnel service URL. The string replace happens in the following cases: * - The response is a redirect response, and the `location` property in response header may contain Logto * endpoint URI. * - The response body is JSON, which consists of properties such as `**_endpoint` and `redirectTo`. These @@ -80,13 +80,13 @@ export const createLogtoResponseHandler = async ({ request, response, logtoEndpointUrl, - proxyUrl, + tunnelServiceUrl, verbose, -}: ProxyResponseHandler) => { +}: LogtoResponseHandler) => { const { location } = proxyResponse.headers; if (location) { // eslint-disable-next-line @silverhand/fp/no-mutation - proxyResponse.headers.location = location.replace(logtoEndpointUrl.href, proxyUrl.href); + proxyResponse.headers.location = location.replace(logtoEndpointUrl.href, tunnelServiceUrl.href); } void responseInterceptor(async (responseBuffer, proxyResponse) => { @@ -96,7 +96,10 @@ export const createLogtoResponseHandler = async ({ } if (proxyResponse.headers['content-type']?.includes('text/html')) { - return responseBody.replace(`action="${logtoEndpointUrl.href}`, `action="${proxyUrl.href}`); + return responseBody.replace( + `action="${logtoEndpointUrl.href}`, + `action="${tunnelServiceUrl.href}` + ); } if (proxyResponse.headers['content-type']?.includes('application/json')) { @@ -106,7 +109,7 @@ export const createLogtoResponseHandler = async ({ const updatedEntries: Array<[string, unknown]> = Object.entries(jsonData).map( ([key, value]) => { if ((key === 'redirectTo' || key.endsWith('_endpoint')) && typeof value === 'string') { - return [key, value.replace(logtoEndpointUrl.href, proxyUrl.href)]; + return [key, value.replace(logtoEndpointUrl.href, tunnelServiceUrl.href)]; } return [key, value]; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7afd7c9ec..6ea9e51e2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,8 +6,8 @@ import { hideBin } from 'yargs/helpers'; import connector from './commands/connector/index.js'; import database from './commands/database/index.js'; import install from './commands/install/index.js'; -import proxy from './commands/proxy/index.js'; import translate from './commands/translate/index.js'; +import tunnel from './commands/tunnel/index.js'; import { packageJson } from './package-json.js'; import { cliConfig, ConfigKey, consoleLog } from './utils.js'; @@ -49,7 +49,7 @@ void yargs(hideBin(process.argv)) .command(database) .command(connector) .command(translate) - .command(proxy) + .command(tunnel) .demandCommand(1) .showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`) .strict()