Skip to content

Commit

Permalink
Typescript, initial settings, conversion and base core libs
Browse files Browse the repository at this point in the history
  • Loading branch information
hrax committed Jul 25, 2024
1 parent 794681a commit 4f0bb31
Show file tree
Hide file tree
Showing 16 changed files with 2,187 additions and 970 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
*.docx
/node_modules
/.vscode
/build
/dist
6 changes: 3 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const Linter = require("./modules/linter/Linter.js");
const Profile = require("./modules/linter/Profile.js");
const Linter = require("./build/modules/linter/Linter.js");
const Profile = require("./build/modules/linter/Profile.js");

// eslint-disable-next-line no-multi-assign
exports = module.exports = Linter;

// export modules + explicit access to Linter
exports.Linter = Linter;
exports.Profile = Profile;
exports.Profile = Profile;
26 changes: 26 additions & 0 deletions modules/core/InstanceManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

export type InstanceData = {
name: string,
base: string,
auth: {
type: "oauth-token" | "oauth-password",
clientID: string,
clientSecret: string,
token?: {
"access_token": string,
"refresh_token": string,
"scope": string,
"token_type": string,
"expires_in": number,
"loaded_at": Date
}
}
}

export default class InstanceManager {
#instance: InstanceData;

constructor(instance: InstanceData) {
this.#instance = instance;
}
}
137 changes: 137 additions & 0 deletions modules/core/OAuthClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import crypto from "crypto";
import { URL, URLSearchParams } from "url";
import { InstanceData } from "./InstanceManager";
import Request, { RESPONSE_STATUS } from "./Request";
import pkg from "../../package.json";
import { RequestOptions } from "https";

const CLIENT_BASE_URL: string = "/oauth_entity.do";
const CLIENT_LIST_BASE_URL: string = "/oauth_entity.do";
const AUTH_BASE_URL: string = "/oauth_auth.do";
const TOKEN_BASE_URL: string = "/oauth_token.do";

const REDIRECT_URI: string = "http://localhost:696969/oauth_client";

const TOKEN_CONTENT_TYPE: string = "application/x-www-form-urlencoded"

export type OAuthTokenResponse = {
"access_token": string,
"refresh_token": string,
"scope": string,
"token_type": string,
"expires_in": number
};

export default class OAuthClient {
#instance: InstanceData;

// create Server?

static generateRandomState(): string {
return crypto.randomBytes(16).toString("hex");
}

constructor(instance: InstanceData) {
this.#instance = instance;
}

getNewClientURL(): string {
const query: Array<string> = [
"type=client",
`name=${pkg.name}`,
`comments=OAuth Client for ${pkg.name} v${pkg.version}`,
"refresh_token_lifespan=31536000",
"redirect_url=" + REDIRECT_URI,
"logo_url="
];

const url: URL = new URL(CLIENT_BASE_URL, this.#instance.base);
url.searchParams.set("sys_id", "-1");
url.searchParams.set("sysparm_transaction_scope", "global");
url.searchParams.set("sysparm_query", query.join("^"));
return url.toString();
}

getListClientURL(): string {
const query: Array<string> = [
"type=client",
"name=" + pkg.name
];
const url: URL = new URL(CLIENT_LIST_BASE_URL, this.#instance.base);
url.searchParams.set("sys_id", "-1");
url.searchParams.set("sysparm_transaction_scope", "global");
url.searchParams.set("sysparm_query", query.join("^"));
return url.toString();
}

getAuthCodeURL(state: string): string {
const url: URL = new URL(AUTH_BASE_URL, this.#instance.base);
url.searchParams.set("response_type", "code");
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("client_id", this.#instance.auth.clientID);
url.searchParams.set("state", state);
return url.toString();
}

async requestTokenByCode(authCode: string): Promise<OAuthTokenResponse> {
const url: URL = new URL(TOKEN_BASE_URL, this.#instance.base);

const body: URLSearchParams = new URLSearchParams();
body.set("grant_type", "authorization_code");
body.set("code", authCode);
//body.set("redirect_uri", REDIRECT_URI);
body.set("client_id", this.#instance.auth.clientID);
body.set("client_secret", this.#instance.auth.clientSecret);

const options: RequestOptions = {
method: "POST",
headers: {
"content-type": TOKEN_CONTENT_TYPE,
"content-length": Buffer.byteLength(body.toString())
}
};

return await Request.json(url, options, body.toString());
}

async requestTokenByUsername(username: string, password: string): Promise<OAuthTokenResponse> {
const url: URL = new URL(TOKEN_BASE_URL, this.#instance.base);

const body: URLSearchParams = new URLSearchParams();
body.set("grant_type", "password");
body.set("username", username);
body.set("password", password);
body.set("client_id", this.#instance.auth.clientID);
body.set("client_secret", this.#instance.auth.clientSecret);

const options: RequestOptions = {
method: "POST",
headers: {
"content-type": TOKEN_CONTENT_TYPE,
"content-length": Buffer.byteLength(body.toString())
}
};

return await Request.json(url, options, body.toString());
}

async refreshToken(): Promise<OAuthTokenResponse> {
const url: URL = new URL(TOKEN_BASE_URL, this.#instance.base);

const body: URLSearchParams = new URLSearchParams();
body.set("grant_type", "refresh_token");
body.set("refresh_token", this.#instance.auth.token?.refresh_token!);
body.set("client_id", this.#instance.auth.clientID);
body.set("client_secret", this.#instance.auth.clientSecret);

const options: RequestOptions = {
method: "POST",
headers: {
"content-type": TOKEN_CONTENT_TYPE,
"content-length": Buffer.byteLength(body.toString())
}
};

return await Request.json(url, options, body.toString());
}
}
29 changes: 29 additions & 0 deletions modules/core/RESTRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { InstanceData } from "./InstanceManager";

import http from "https";
import HttpsProxyAgent from "https-proxy-agent";
import Assert from "../util/Assert.js";

export enum RESPONSE_STATUS {
OK = 200,
NOT_FOUND = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
ERROR = 500
};

export default class RESTRequest {
#instance;

constructor(instance: InstanceData) {
this.#instance = instance;
}

#requestOAuthToken() {

}

#refreshOAuthToken() {

}
}
82 changes: 82 additions & 0 deletions modules/core/Request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import http, { RequestOptions } from "https";
import agent, { HttpsProxyAgent } from "https-proxy-agent";
import Assert from "../util/Assert.js";
import { IncomingMessage } from "http";

export enum RESPONSE_STATUS {
OK = 200,
NOT_FOUND = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
ERROR = 500
};

/* export type RequestOptions = HttpRequestOptions & {
} */

/**
* See https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0534905
* In the REST world, PUT and PATCH have different semantics. PUT means replace the entire resource with given data
* (so null out fields if they are not provided in the request), while PATCH means replace only specified fields.
* For the Table API, however, PUT and PATCH mean the same thing. PUT and PATCH modify only the fields specified in the request.
*/
export default class Request {

static readonly ENCODING: BufferEncoding = "utf8";

static createProxy(proxy: string): HttpsProxyAgent {
return new HttpsProxyAgent(proxy);
}

static async request(url: URL, options: RequestOptions, body?: string): Promise<string> {
return new Promise((resolve, reject) => {
const request = http.request(
url,
options,
(response: IncomingMessage) => {
let data: string = "";
response.setEncoding(Request.ENCODING);

response.on("data", (chunk: string) => {
data += chunk;
});

response.on("end", () => {
// If response status code is not 200 resolve reject promise with an error
if (response.statusCode !== RESPONSE_STATUS.OK) {
reject([response, data]);
return;
}

resolve(data);
});
}
);

request.on("error", (e) => {
reject([null, e]);
});

request.on('timeout', function () {
// Timeout happend. Server received request, but not handled it
// (i.e. doesn't send any response or it took to long).
// You don't know what happend.
// It will emit 'error' message as well (with ECONNRESET code).

request.destroy();
});

if (body != null) {
request.write(body, Request.ENCODING);
}

request.end();
});
}

static async json<T>(url: URL, options: RequestOptions, body?: string): Promise<T> {
const data = await Request.request(url, options, body);
return JSON.parse(data);
}
}
File renamed without changes.
File renamed without changes.
7 changes: 0 additions & 7 deletions modules/core/index.js

This file was deleted.

2 changes: 1 addition & 1 deletion modules/linter/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const os = require("os");
const path = require("path");

const Assert = require("../util/Assert.js");
const Instance = require("../core/Instance.js");
const Instance = require("../core/_Instance.js/index.js");
const Request = require("../core/Request.js");

/**
Expand Down
17 changes: 17 additions & 0 deletions modules/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { InstanceData } from "./core/InstanceManager";
import OAuthClient from "./core/OAuthClient";

(function() {
const data: InstanceData = {
"name": "test",
"base": "https://dev263075.service-now.com/",
"auth": {
type: "oauth-token",
clientID: "aaa",
clientSecret: "bbb"
}
}

const oauth: OAuthClient = new OAuthClient(data);
console.log(oauth.getNewClientURL());
})();
Loading

0 comments on commit 4f0bb31

Please sign in to comment.