|
| 1 | +import { Command } from "commander" |
| 2 | +import { loadCurrentToken } from "../auth/store.js" |
| 3 | +import type { OutputFormat } from "../output.js" |
| 4 | +import { formatOutput } from "../output.js" |
| 5 | + |
| 6 | +const PROJECT_ROLES_CLAIM = "urn:zitadel:iam:org:project:roles" |
| 7 | + |
| 8 | +function decodeJwtPayload(token: string): Record<string, unknown> { |
| 9 | + const parts = token.split(".") |
| 10 | + if (parts.length !== 3) { |
| 11 | + throw new Error("Not a JWT") |
| 12 | + } |
| 13 | + const payload = Buffer.from(parts[1], "base64url").toString("utf8") |
| 14 | + return JSON.parse(payload) as Record<string, unknown> |
| 15 | +} |
| 16 | + |
| 17 | +function readScopesClaim(value: unknown): string[] { |
| 18 | + if (typeof value === "string") { |
| 19 | + return value.split(/\s+/).filter(Boolean) |
| 20 | + } |
| 21 | + if (Array.isArray(value)) { |
| 22 | + return value.filter((scope): scope is string => typeof scope === "string") |
| 23 | + } |
| 24 | + return [] |
| 25 | +} |
| 26 | + |
| 27 | +function readJwtIdentity(accessToken: string): { |
| 28 | + wallet?: string |
| 29 | + opensea_scopes: string[] |
| 30 | + project_roles?: unknown |
| 31 | +} { |
| 32 | + const claims = decodeJwtPayload(accessToken) |
| 33 | + const wallet = typeof claims.wallet === "string" ? claims.wallet : undefined |
| 34 | + const projectRoles = claims[PROJECT_ROLES_CLAIM] ?? claims.project_roles |
| 35 | + |
| 36 | + return { |
| 37 | + wallet, |
| 38 | + opensea_scopes: readScopesClaim(claims.opensea_scopes), |
| 39 | + ...(projectRoles === undefined ? {} : { project_roles: projectRoles }), |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +function difference(left: string[], right: string[]): string[] { |
| 44 | + const rightSet = new Set(right) |
| 45 | + return left.filter(scope => !rightSet.has(scope)) |
| 46 | +} |
| 47 | + |
| 48 | +export function whoamiCommand(getFormat: () => OutputFormat): Command { |
| 49 | + return new Command("whoami") |
| 50 | + .description("Show the current authenticated wallet and scopes") |
| 51 | + .option( |
| 52 | + "--diagnostic", |
| 53 | + "Include unverified JWT claims for troubleshooting; claims are not authorization data", |
| 54 | + ) |
| 55 | + .action((options: { diagnostic?: boolean }) => { |
| 56 | + const token = loadCurrentToken() |
| 57 | + if (!token) { |
| 58 | + console.log( |
| 59 | + formatOutput( |
| 60 | + { status: "not_authenticated", message: "No stored token" }, |
| 61 | + getFormat(), |
| 62 | + ), |
| 63 | + ) |
| 64 | + return |
| 65 | + } |
| 66 | + |
| 67 | + const expired = new Date(token.expiresAt) < new Date() |
| 68 | + let jwt: ReturnType<typeof readJwtIdentity> | undefined |
| 69 | + let jwtError: string | undefined |
| 70 | + try { |
| 71 | + jwt = readJwtIdentity(token.accessToken) |
| 72 | + } catch (error) { |
| 73 | + jwtError = error instanceof Error ? error.message : String(error) |
| 74 | + } |
| 75 | + |
| 76 | + const jwtScopes = jwt?.opensea_scopes ?? [] |
| 77 | + const diagnostic = options.diagnostic |
| 78 | + ? { |
| 79 | + unverified: true, |
| 80 | + jwt: jwt ?? { opensea_scopes: [] }, |
| 81 | + scope_difference: { |
| 82 | + only_in_token: difference(token.scopes, jwtScopes), |
| 83 | + only_in_jwt: difference(jwtScopes, token.scopes), |
| 84 | + }, |
| 85 | + ...(jwtError ? { jwt_error: jwtError } : {}), |
| 86 | + } |
| 87 | + : undefined |
| 88 | + console.log( |
| 89 | + formatOutput( |
| 90 | + { |
| 91 | + status: expired ? "expired" : "authenticated", |
| 92 | + address: token.address, |
| 93 | + auth_method: token.authMethod, |
| 94 | + scopes: token.scopes, |
| 95 | + scope_source: token.scopeSource ?? "unknown", |
| 96 | + ...(diagnostic ? { diagnostic } : {}), |
| 97 | + expires_at: token.expiresAt, |
| 98 | + expired, |
| 99 | + }, |
| 100 | + getFormat(), |
| 101 | + ), |
| 102 | + ) |
| 103 | + }) |
| 104 | +} |
0 commit comments