@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
184 lines (183 loc) • 7.32 kB
JavaScript
import { Args, Flags } from "@oclif/core";
import * as https from "node:https";
import { AblyBaseCommand } from "../../base-command.js";
export default class RevokeTokenCommand extends AblyBaseCommand {
static args = {
token: Args.string({
description: "Token to revoke",
name: "token",
required: true,
}),
};
static description = "Revokes the token provided";
static examples = [
"$ ably auth revoke-token TOKEN",
"$ ably auth revoke-token TOKEN --client-id clientid",
"$ ably auth revoke-token TOKEN --json",
"$ ably auth revoke-token TOKEN --pretty-json",
];
static flags = {
...AblyBaseCommand.globalFlags,
app: Flags.string({
description: "App ID to use (uses current app if not specified)",
env: "ABLY_APP_ID",
}),
"client-id": Flags.string({
char: "c",
description: "Client ID to revoke tokens for",
}),
debug: Flags.boolean({
default: false,
description: "Show debug information",
}),
};
// Property to store the Ably client
ablyClient;
async run() {
const { args, flags } = await this.parse(RevokeTokenCommand);
// Get app and key
const appAndKey = await this.ensureAppAndKey(flags);
if (!appAndKey) {
return;
}
const { apiKey } = appAndKey;
const { token } = args;
try {
if (flags.debug) {
this.log(`Debug: Using API key: ${apiKey.replace(/:.+/, ":***")}`);
}
// Create Ably Realtime client
const client = await this.createAblyRealtimeClient(flags);
if (!client)
return;
this.ablyClient = client;
const clientId = flags["client-id"] || token;
if (!flags["client-id"]) {
// We need to warn the user that we're using the token as a client ID
this.warn("Revoking a specific token is only possible if it has a client ID or revocation key");
this.warn("For advanced token revocation options, see: https://ably.com/docs/auth/revocation");
this.warn("Using the token argument as a client ID for this operation");
}
// Extract the keyName (appId.keyId) from the API key
const keyParts = apiKey.split(":");
if (keyParts.length !== 2) {
this.error("Invalid API key format. Expected format: appId.keyId:secret");
return;
}
const keyName = keyParts[0]; // This gets the appId.keyId portion
const secret = keyParts[1];
// Create the properly formatted body for token revocation
const requestBody = {
targets: [`clientId:${clientId}`],
};
if (flags.debug) {
this.log(`Debug: Sending request to endpoint: /keys/${keyName}/revokeTokens`);
this.log(`Debug: Request body: ${JSON.stringify(requestBody)}`);
}
try {
// Make direct HTTPS request to Ably REST API
const response = await this.makeHttpRequest(keyName, secret, requestBody, flags.debug);
if (flags.debug) {
this.log(`Debug: Response received:`);
this.log(JSON.stringify(response, null, 2));
}
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
message: "Token revocation processed successfully",
response,
success: true,
}, flags));
}
else {
this.log("Token successfully revoked");
}
}
catch (requestError) {
// Handle specific API errors
if (flags.debug) {
this.log(`Debug: API Error:`);
this.log(JSON.stringify(requestError, null, 2));
}
const error = requestError;
if (error.message && error.message.includes("token_not_found")) {
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
error: "Token not found or already revoked",
success: false,
}, flags));
}
else {
this.log("Error: Token not found or already revoked");
}
}
else {
throw requestError;
}
}
}
catch (error) {
let errorMessage = "Unknown error";
if (error instanceof Error) {
errorMessage = error.message;
}
else if (typeof error === "string") {
errorMessage = error;
}
else if (error && typeof error === "object") {
errorMessage = JSON.stringify(error);
}
this.error(`Error revoking token: ${errorMessage}`);
}
finally {
// Close the client if it exists
if (this.ablyClient) {
this.ablyClient.close();
}
}
}
// Helper method to make a direct HTTP request to the Ably REST API
makeHttpRequest(keyName, secret, requestBody, debug) {
return new Promise((resolve, reject) => {
const encodedAuth = Buffer.from(`${keyName}:${secret}`).toString("base64");
const options = {
headers: {
Accept: "application/json",
Authorization: `Basic ${encodedAuth}`,
"Content-Type": "application/json",
},
hostname: "rest.ably.io",
method: "POST",
path: `/keys/${keyName}/revokeTokens`,
port: 443,
};
if (debug) {
this.log(`Debug: Making HTTPS request to: https://${options.hostname}${options.path}`);
}
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
const jsonResponse = data.length > 0 ? JSON.parse(data) : null;
resolve(jsonResponse);
}
catch {
resolve(data);
}
}
else {
reject(new Error(`Request failed with status code ${res.statusCode}: ${data}`));
}
});
});
req.on("error", (error) => {
reject(error);
});
req.write(JSON.stringify(requestBody));
req.end();
});
}
}