compass-bot/auth/authlib.mjs
2023-10-29 20:44:36 +08:00

184 lines
4.9 KiB
JavaScript

import axios from 'axios';
import { readJsonFile, writeJsonFile } from '../utils/index.mjs';
export class NoCredentialError extends Error {
constructor(profile) {
super(`No credentials configured for profile ${profile}.`);
}
};
export class ProfileNotSelectedError extends Error {
constructor() {
super('No profile was selected.');
}
};
export class ProfileAlreadySelectedError extends Error {
constructor(profile) {
super(`Profile was already selected to ${profile}.`);
}
};
export class YggdrasilEndpoint {
constructor(endpoint, endpoint_auth, endpoint_session) {
this.endpoint = endpoint;
this.endpoint_auth = endpoint_auth || this.endpoint + '/authserver';
this.endpoint_session = endpoint_session || this.endpoint + '/sessionserver';
}
auth_url(path) {
return this.endpoint_auth + '/' + path;
}
};
export class Account {
constructor(handle, password) {
this.handle = handle;
this.password = password;
}
async auth(endpoint) {
let auth_info = (await axios.post(endpoint.auth_url('authenticate'), {
username: this.handle,
password: this.password,
})).data;
return new SessionCache(auth_info.accessToken, auth_info.clientToken
, auth_info.selectedProfile, auth_info.availableProfiles);
}
};
export class Credentials {
constructor(options) {
this.endpoint = new YggdrasilEndpoint(options.endpoint, options.endpoint_auth, options.endpoint_session);
this.profiles = new Map();
this.accounts = [];
for (let account_option of options.accounts) {
let account = new Account(account_option.handle || account_option.profiles[0]
, account_option.password);
this.accounts.push(account);
for (let profile of account_option.profiles) {
this.profiles.set(profile, account);
}
}
}
credentialOf(profile) {
return this.profiles.get(profile);
}
static async fromFile(path) {
let options = await readJsonFile(path);
return new Credentials(options);
}
async authProfile(profile) {
let sc = await SessionCache.load(profile);
if (sc != null && !(await sc.validate(this.endpoint))) { sc = null; }
if (sc == null) {
let account = this.credentialOf(profile);
if (account == null) { throw new NoCredentialError(profile); }
sc = await account.auth(this.endpoint);
await sc.selectProfile(this.endpoint, profile);
await sc.store();
}
return sc;
}
};
export class SessionCache {
constructor(accessToken, clientToken, selectedProfile, availableProfiles) {
this.accessToken = accessToken;
this.clientToken = clientToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
}
name() {
return this.selectedProfile.name;
}
session() {
return {
accessToken: this.accessToken,
clientToken: this.clientToken,
selectedProfile: this.selectedProfile,
};
}
async store() {
if (this.selectedProfile == null) { throw new ProfileNotSelectedError(); }
await writeJsonFile(`.cache/${this.name()}.json`, this.session());
}
static async load(profile) {
let cache_data = await readJsonFile(`.cache/${profile}.json`);
if (cache_data == null) { return null; }
return new SessionCache(cache_data.accessToken, cache_data.clientToken, cache_data.selectedProfile);
}
async validate(endpoint) {
let vres = await axios.post(endpoint.auth_url('validate'), {
accessToken: this.accessToken,
clientToken: this.clientToken,
}, { validateStatus: (status) => (status === 403 || status === 204) });
return vres.status == 204;
}
async selectProfile(endpoint, profile) {
if (this.selectedProfile != null) {
if (this.name() != profile) {
throw new ProfileAlreadySelectedError(this.name());
} else { return; }
}
if (this.availableProfiles == null) {
throw new NoCredentialError(profile);
}
let profile_info = null;
for (let info of this.availableProfiles) {
if (info.name == profile) {
profile_info = info;
break;
}
}
if (profile_info == null) { throw new NoCredentialError(profile); }
let session_info = (await axios.post(endpoint.auth_url('refresh'), {
accessToken: this.accessToken,
clientToken: this.clientToken,
selectedProfile: profile_info,
})).data;
this.selectedProfile = session_info.selectedProfile;
this.accessToken = session_info.accessToken;
this.availableProfiles = null;
}
mineflayer(endpoint) {
if (this.selectedProfile == null) { throw new ProfileNotSelectedError(); }
return {
username: this.name(),
authServer: endpoint.endpoint_auth,
sessionServer: endpoint.endpoint_session,
auth: (client, options) => {
client.username = this.name();
client.session = this.session();
options.accessToken = this.accessToken;
options.clientToken = this.clientToken;
options.haveCredentials = true;
client.emit('session', client.session);
options.connect(client);
},
};
}
};
export default {
NoCredentialError, ProfileNotSelectedError, ProfileAlreadySelectedError
, YggdrasilEndpoint, Account, Credentials, SessionCache
};