@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
129 lines (128 loc) • 5.58 kB
JavaScript
import { Flags } from "@oclif/core";
import jwt from "jsonwebtoken";
import { randomUUID } from "node:crypto";
import { AblyBaseCommand } from "../../base-command.js";
export default class IssueJwtTokenCommand extends AblyBaseCommand {
static description = "Creates an Ably JWT token with capabilities";
static examples = [
"$ ably auth issue-jwt-token",
'$ ably auth issue-jwt-token --capability \'{"*":["*"]}\'',
'$ ably auth issue-jwt-token --capability \'{"chat:*":["publish","subscribe"], "status:*":["subscribe"]}\' --ttl 3600',
"$ ably auth issue-jwt-token --client-id client123 --ttl 86400",
"$ ably auth issue-jwt-token --json",
"$ ably auth issue-jwt-token --pretty-json",
"$ ably auth issue-jwt-token --token-only",
'$ ably channels publish --token "$(ably auth issue-jwt-token --token-only)" my-channel "Hello"',
];
static flags = {
...AblyBaseCommand.globalFlags,
app: Flags.string({
description: "App ID to use (uses current app if not specified)",
env: "ABLY_APP_ID",
}),
capability: Flags.string({
default: '{"*":["*"]}',
description: 'Capabilities JSON string (e.g. {"channel":["publish","subscribe"]})',
}),
"client-id": Flags.string({
description: 'Client ID to associate with the token. Use "none" to explicitly issue a token with no client ID, otherwise a default will be generated.',
}),
"token-only": Flags.boolean({
default: false,
description: "Output only the token string without any formatting or additional information",
}),
ttl: Flags.integer({
default: 3600, // 1 hour
description: "Time to live in seconds",
}),
};
async run() {
const { flags } = await this.parse(IssueJwtTokenCommand);
// Get app and key
const appAndKey = await this.ensureAppAndKey(flags);
if (!appAndKey) {
return;
}
const { apiKey, appId } = appAndKey;
try {
// Parse the API key to get keyId and keySecret
const [keyId, keySecret] = apiKey.split(":");
if (!keyId || !keySecret) {
this.error("Invalid API key format. Expected format: keyId:keySecret");
}
// Parse capabilities
let capabilities;
try {
capabilities = JSON.parse(flags.capability);
}
catch (error) {
this.error(`Invalid capability JSON: ${error instanceof Error ? error.message : String(error)}`);
}
// Create JWT payload
const jwtPayload = {
exp: Math.floor(Date.now() / 1000) + flags.ttl, // expiration
iat: Math.floor(Date.now() / 1000), // issued at
jti: randomUUID(), // unique token ID
"x-ably-appId": appId,
"x-ably-capability": capabilities,
};
// Handle client ID - use special "none" value to explicitly indicate no clientId
let clientId = null;
if (flags["client-id"]) {
if (flags["client-id"].toLowerCase() === "none") {
// No client ID - don't add it to the token
clientId = null;
}
else {
// Use the provided client ID
jwtPayload["x-ably-clientId"] = flags["client-id"];
clientId = flags["client-id"];
}
}
else {
// Generate a default client ID
const defaultClientId = `ably-cli-${randomUUID().slice(0, 8)}`;
jwtPayload["x-ably-clientId"] = defaultClientId;
clientId = defaultClientId;
}
// Sign the JWT
const token = jwt.sign(jwtPayload, keySecret, {
algorithm: "HS256",
keyid: keyId,
});
// If token-only flag is set, output just the token string
if (flags["token-only"]) {
this.log(token);
return;
}
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
appId,
capability: capabilities,
clientId,
expires: new Date(jwtPayload.exp * 1000).toISOString(),
issued: new Date(jwtPayload.iat * 1000).toISOString(),
keyId,
token,
ttl: flags.ttl,
type: "jwt",
}, flags));
}
else {
this.log("Generated Ably JWT Token:");
this.log(`Token: ${token}`);
this.log(`Type: JWT`);
this.log(`Issued: ${new Date(jwtPayload.iat * 1000).toISOString()}`);
this.log(`Expires: ${new Date(jwtPayload.exp * 1000).toISOString()}`);
this.log(`TTL: ${flags.ttl} seconds`);
this.log(`App ID: ${appId}`);
this.log(`Key ID: ${keyId}`);
this.log(`Client ID: ${clientId || "None"}`);
this.log(`Capability: ${this.formatJsonOutput(capabilities, flags)}`);
}
}
catch (error) {
this.error(`Error issuing JWT token: ${error instanceof Error ? error.message : String(error)}`);
}
}
}