diff --git a/CHANGELOG.md b/CHANGELOG.md index 7588c06..596d090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 22.1.3 + +* Added: `--resource` option to `oauth2 authorize`, `create-device-authorization`, and `create-token` for RFC 8707 resource indicators +* Added: Press Enter during device login to open the verification URL in your browser +* Updated: Bumped `@appwrite.io/console` dependency to `^15.1.0` + ## 22.1.2 * Added: OAuth2 login now requests the `account.admin` scope diff --git a/README.md b/README.md index 944c2f0..1494229 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Once the installation is complete, you can verify the install using ```sh $ appwrite -v -22.1.2 +22.1.3 ``` ### Install using prebuilt binaries @@ -83,7 +83,7 @@ $ scoop install https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/sc Once the installation completes, you can verify your install using ``` $ appwrite -v -22.1.2 +22.1.3 ``` ## Getting Started diff --git a/bun.lock b/bun.lock index c5131f0..7603f55 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "appwrite-cli", "dependencies": { - "@appwrite.io/console": "^15.0.0", + "@appwrite.io/console": "^15.1.0", "chalk": "4.1.2", "chokidar": "^3.6.0", "cli-progress": "^3.12.0", @@ -51,7 +51,7 @@ "tmp": "^0.2.6", }, "packages": { - "@appwrite.io/console": ["@appwrite.io/console@15.0.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-yjCjsUTcOUacR/O0/XEm9ax+EpcxxG3+h7TsY0YEd2fW32T2AimASOVMcRoEb8DrSHWqCn5kH1VRL/U6ZfAoJw=="], + "@appwrite.io/console": ["@appwrite.io/console@15.1.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-uQ8can9vDKoP6zxqhp8JzOYma/w40eKuYzZzsI9ATCRHSzRig0WQ6fue8rZEgOuD3eF3S2GbtHZK4drotAT8Ug=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], diff --git a/install.ps1 b/install.ps1 index 264c8ee..be4958e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -13,8 +13,8 @@ # You can use "View source" of this page to see the full script. # REPO -$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.2/appwrite-cli-win-x64.exe" -$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.2/appwrite-cli-win-arm64.exe" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.3/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.3/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index 02842c1..132bb11 100644 --- a/install.sh +++ b/install.sh @@ -120,7 +120,7 @@ verifyMacOSCodeSignature() { downloadBinary() { echo "[2/5] Downloading executable for $OS ($ARCH) ..." - GITHUB_LATEST_VERSION="22.1.2" + GITHUB_LATEST_VERSION="22.1.3" GITHUB_FILE="appwrite-cli-${OS}-${ARCH}" GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE" diff --git a/lib/auth/login.ts b/lib/auth/login.ts index 603f131..0433669 100644 --- a/lib/auth/login.ts +++ b/lib/auth/login.ts @@ -8,7 +8,11 @@ import { OAUTH2_SCOPES, } from "../constants.js"; import { success, hint, warn, log } from "../parser.js"; -import { isCloudLoginEndpoint, isRegionalCloudEndpoint } from "../utils.js"; +import { + isCloudLoginEndpoint, + isRegionalCloudEndpoint, + openBrowser, +} from "../utils.js"; import { isFlagEnabled } from "../flags.js"; import ID from "../id.js"; import { @@ -67,6 +71,58 @@ const startWaitingForApprovalSpinner = (): (() => void) => { }; }; +const listenForBrowserOpen = ( + url: string, + onCancel: () => void, +): (() => void) => { + const stdin = process.stdin as NodeJS.ReadStream & { + isRaw?: boolean; + setRawMode?: (mode: boolean) => void; + }; + if (!stdin.isTTY) { + return () => {}; + } + + // Raw mode so keypresses aren't echoed onto the spinner line; the trade-off + // is that the terminal no longer turns Ctrl+C/Ctrl+D into a signal for us. + const canSetRawMode = typeof stdin.setRawMode === "function"; + const shouldRestoreRawMode = canSetRawMode && stdin.isRaw !== true; + if (shouldRestoreRawMode) { + stdin.setRawMode?.(true); + } + const shouldPause = stdin.isPaused(); + stdin.resume(); + + const cleanup = (): void => { + stdin.off("data", onData); + if (shouldRestoreRawMode) { + stdin.setRawMode?.(false); + } + if (shouldPause) { + stdin.pause(); + } + }; + + // Open the browser at most once; keep listening afterwards so Ctrl+C still + // cancels cleanly even after the user has opened the URL. + let opened = false; + const onData = (data: Buffer): void => { + if (data.includes(0x03) || data.includes(0x04)) { + cleanup(); + onCancel(); + return; + } + if (!opened && (data.includes(0x0d) || data.includes(0x0a))) { + opened = true; + openBrowser(url); + } + }; + + stdin.on("data", onData); + + return cleanup; +}; + export const getCurrentAccount = async (): Promise => { if (globalConfig.getEndpoint() === "" || !hasAuthSession()) { return null; @@ -299,11 +355,24 @@ const loginWithOAuthDevice = async ({ process.stdout.write( "\nTo sign in, confirm the code below in your browser:\n\n" + ` Code: ${deviceAuth.user_code}\n` + - ` URL: ${verificationUri}\n\n`, + ` URL: ${verificationUri}\n\n` + + (process.stdin.isTTY + ? "Press Enter to open this URL in your browser.\n\n" + : ""), ); let token: Models.Oauth2Token | null = null; const stopWaitingForApprovalSpinner = startWaitingForApprovalSpinner(); + const stopListeningForBrowserOpen = listenForBrowserOpen( + verificationUri, + () => { + stopWaitingForApprovalSpinner(); + globalConfig.removeSession(id); + globalConfig.setCurrentSession(oldCurrent); + log("Login cancelled."); + process.exit(130); + }, + ); try { token = await pollForDeviceToken(oauth2, deviceAuth, clientId); @@ -313,6 +382,7 @@ const loginWithOAuthDevice = async ({ throw err; } finally { stopWaitingForApprovalSpinner(); + stopListeningForBrowserOpen(); } if (!token || !token.access_token) { diff --git a/lib/commands/services/oauth-2.ts b/lib/commands/services/oauth-2.ts index 3b028db..334294c 100644 --- a/lib/commands/services/oauth-2.ts +++ b/lib/commands/services/oauth-2.ts @@ -53,10 +53,11 @@ const oauth2AuthorizeCommand = oauth2 .option(`--prompt `, `OIDC prompt parameter for customization of consent screen. Space-separated list of: none, login, consent, select_account.`) .option(`--max-_age `, `OIDC max_age paraleter for customization of consent screen. Maximum allowable elapsed time in seconds since the user last authenticated. If exceeded, re-authentication is required.`, parseInteger) .option(`--authorization-_details `, `Rich authorization request. JSON array of objects, each with a \`type\` and project-defined fields`) + .option(`--resource `, `RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment.`) .action( actionRunner( - async ({ client_id, redirect_uri, response_type, scope, state, nonce, code_challenge, code_challenge_method, prompt, max_age, authorization_details }) => - parse(await (await getOauth2Client()).authorize(client_id, redirect_uri, response_type, scope, state, nonce, code_challenge, code_challenge_method, prompt, max_age, authorization_details)), + async ({ client_id, redirect_uri, response_type, scope, state, nonce, code_challenge, code_challenge_method, prompt, max_age, authorization_details, resource }) => + parse(await (await getOauth2Client()).authorize(client_id, redirect_uri, response_type, scope, state, nonce, code_challenge, code_challenge_method, prompt, max_age, authorization_details, resource)), ), ); @@ -67,10 +68,11 @@ const oauth2CreateDeviceAuthorizationCommand = oauth2 .option(`--client-_id `, `OAuth2 client ID.`) .option(`--scope `, `Space-separated OAuth2 scopes. Can include project scopes, and built-in scopes: \`openid\`, \`email\`, \`profile\`.`) .option(`--authorization-_details `, `Rich authorization request. JSON array of objects, each with a \`type\` and project-defined fields`) + .option(`--resource `, `RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment.`) .action( actionRunner( - async ({ client_id, scope, authorization_details }) => - parse(await (await getOauth2Client()).createDeviceAuthorization(client_id, scope, authorization_details)), + async ({ client_id, scope, authorization_details, resource }) => + parse(await (await getOauth2Client()).createDeviceAuthorization(client_id, scope, authorization_details, resource)), ), ); @@ -154,10 +156,11 @@ const oauth2CreateTokenCommand = oauth2 .option(`--client-_secret `, `OAuth2 client secret. Required for confidential apps.`) .option(`--code-_verifier `, `PKCE code verifier. Required for public apps.`) .option(`--redirect-_uri `, `Redirect URI. Required for \`authorization_code\` grant type.`) + .option(`--resource `, `RFC 8707 resource indicator URI or URI list. Each value must be an absolute URI without a fragment.`) .action( actionRunner( - async ({ grant_type, code, refresh_token, device_code, client_id, client_secret, code_verifier, redirect_uri }) => - parse(await (await getOauth2Client()).createToken(grant_type, code, refresh_token, device_code, client_id, client_secret, code_verifier, redirect_uri)), + async ({ grant_type, code, refresh_token, device_code, client_id, client_secret, code_verifier, redirect_uri, resource }) => + parse(await (await getOauth2Client()).createToken(grant_type, code, refresh_token, device_code, client_id, client_secret, code_verifier, redirect_uri, resource)), ), ); diff --git a/lib/constants.ts b/lib/constants.ts index 01fadb5..538e53f 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,7 +1,7 @@ // SDK export const SDK_TITLE = 'Appwrite'; export const SDK_TITLE_LOWER = 'appwrite'; -export const SDK_VERSION = '22.1.2'; +export const SDK_VERSION = '22.1.3'; export const SDK_NAME = 'Command Line'; export const SDK_PLATFORM = 'console'; export const SDK_LANGUAGE = 'cli'; diff --git a/lib/utils.ts b/lib/utils.ts index 6ce4f6e..7308983 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -821,6 +821,42 @@ export function systemHasCommand(command: string): boolean { return true; } +// Best-effort, fire-and-forget: spawn reports a missing/failed opener via an +// async `error` event (not a throw), so there's no reliable success value to +// return — failures just mean the user opens the printed URL manually. +export function openBrowser(url: string): void { + let command: string; + let args: string[]; + + switch (process.platform) { + case "win32": + command = "cmd"; + // "" is start's window-title arg; quoting the URL stops cmd from + // splitting it on `&` (and running the remainder as a command). + args = ["/c", "start", "", `"${url}"`]; + break; + case "darwin": + command = "open"; + args = [url]; + break; + default: + command = "xdg-open"; + args = [url]; + break; + } + + try { + const child = childProcess.spawn(command, args, { + stdio: "ignore", + detached: true, + }); + child.on("error", () => {}); + child.unref(); + } catch { + // Ignore synchronous spawn failures; opening the browser is best-effort. + } +} + type DeployLocalConfig = { keys: () => string[]; }; diff --git a/package-lock.json b/package-lock.json index c3bc2be..4747b19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "appwrite-cli", - "version": "22.1.2", + "version": "22.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite-cli", - "version": "22.1.2", + "version": "22.1.3", "license": "BSD-3-Clause", "dependencies": { - "@appwrite.io/console": "^15.0.0", + "@appwrite.io/console": "^15.1.0", "chalk": "4.1.2", "chokidar": "^3.6.0", "cli-progress": "^3.12.0", @@ -57,9 +57,9 @@ } }, "node_modules/@appwrite.io/console": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-15.0.0.tgz", - "integrity": "sha512-yjCjsUTcOUacR/O0/XEm9ax+EpcxxG3+h7TsY0YEd2fW32T2AimASOVMcRoEb8DrSHWqCn5kH1VRL/U6ZfAoJw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-15.1.0.tgz", + "integrity": "sha512-uQ8can9vDKoP6zxqhp8JzOYma/w40eKuYzZzsI9ATCRHSzRig0WQ6fue8rZEgOuD3eF3S2GbtHZK4drotAT8Ug==", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" diff --git a/package.json b/package.json index 93178d9..e5b4bcf 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "22.1.2", + "version": "22.1.3", "license": "BSD-3-Clause", "main": "dist/index.cjs", "module": "dist/index.js", @@ -51,7 +51,7 @@ "windows-arm64": "bun build cli.ts --compile --minify --target=bun-windows-arm64 --outfile build/appwrite-cli-win-arm64.exe" }, "dependencies": { - "@appwrite.io/console": "^15.0.0", + "@appwrite.io/console": "^15.1.0", "chalk": "4.1.2", "chokidar": "^3.6.0", "cli-progress": "^3.12.0", diff --git a/scoop/appwrite.config.json b/scoop/appwrite.config.json index 7defa62..66d637d 100644 --- a/scoop/appwrite.config.json +++ b/scoop/appwrite.config.json @@ -1,12 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/ScoopInstaller/Scoop/master/schema.json", - "version": "22.1.2", + "version": "22.1.3", "description": "The Appwrite CLI is a command-line application that allows you to interact with Appwrite and perform server-side tasks using your terminal.", "homepage": "https://github.com/appwrite/sdk-for-cli", "license": "BSD-3-Clause", "architecture": { "64bit": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.2/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.3/appwrite-cli-win-x64.exe", "bin": [ [ "appwrite-cli-win-x64.exe", @@ -15,7 +15,7 @@ ] }, "arm64": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.2/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.3/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe",