Skip to content

Instantly share code, notes, and snippets.

@mootari

mootari/index.js Secret

Last active January 12, 2022 12:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mootari/511b751e325db8316bb3138dcb0a7393 to your computer and use it in GitHub Desktop.
Save mootari/511b751e325db8316bb3138dcb0a7393 to your computer and use it in GitHub Desktop.
Observable API
const request = require('superagent');
const crypto = require('crypto');
class ObservableAPI {
constructor() {
this.SITE_URL = 'https://observablehq.com';
this.API_URL = 'https://api.observablehq.com';
this.GITHUB_CLIENT_ID = '1a8619df27715d9d2c97';
// Cookie jar access info.
this.accessInfo = {
domain: '.observablehq.com',
path: '/',
secure: true,
};
this.agent = request.agent().withCredentials();
}
async get(route, data = null) { return this.request('get', route, data); }
async post(route, data = null) { return this.request('post', route, data); }
async put(route, data = null) { return this.request('put', route, data); }
async patch(route, data = null) { return this.request('patch', route, data); }
async delete(route) { return this.request('delete', route); }
async request(method, route, data = null) {
const m = method.toLowerCase();
const path = this.API_URL + '/' + route.replace(/^\//, '');
await this.ensureToken();
let r;
switch(m) {
case 'get':
r = this.agent.get(path);
if(data !== null) r = r.query(data);
break;
case 'post':
case 'put':
case 'patch':
r = this.agent[m](path);
if(data !== null) r = r.send(data);
break;
case 'delete':
r = this.agent[m](path);
break;
default:
throw Error(`Invalid request method "${method}"`);
}
r = r.set({
Accept: 'application/json',
Origin: this.SITE_URL,
});
try {
return (await r).body;
}
catch(e) {
// todo: throw?
return false;
}
}
async isAuthorized() {
return !!(await this.request('get', '/user'));
}
getToken() {
const c = this.agent.jar.getCookie('T', this.accessInfo);
return c && c.value !== '' ? c : null;
}
getSession() {
return this.agent.jar.getCookie('S', this.accessInfo) || null;
}
ensureToken(regenerate = false) {
if (!regenerate && this.getToken()) return;
const {domain, path, secure} = this.accessInfo;
const n = 16;
const token = Array.from(crypto.randomFillSync(new Uint8Array(n)), e => e.toString(16).padStart(2, '0')).join('');
const expires = new Date(Date.now() + 1728e5).toUTCString();
const cookie = `T=${token}; Domain=${domain}; Path=${path}; Expires=${expires}; ${secure ? 'Secure' : ''}`;
this.agent.jar.setCookie(cookie, domain, path);
}
async authorizeWithGithub(name, pass, refresh = false) {
await this.ensureToken();
if(!refresh && await this.isAuthorized()) return true;
const res = await this.agent.get('https://github.com/login/oauth/authorize').query({
client_id: this.GITHUB_CLIENT_ID,
state: this.getToken().value,
redirect_uri: `${this.API_URL}/github/oauth?path=/`
});
const token = res.text.match(/ name="authenticity_token" value="([^"]+)"/)[1];
await this.agent.post('https://github.com/session')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
authenticity_token: token,
login: name,
password: pass,
});
return !!this.getSession();
}
}
(async () => {
const api = new ObservableAPI();
if(await api.authorizeWithGithub('USER_NAME', 'PASSWORD')) {
console.log('User info', await api.get('/user'));
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment