Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ http.get("http://my-service.namespace/path", { agent }, (res) => {

## API

### `new CodezeroAgent(orgID: string, orgAPIKey: string, spaceID: string)`
### `new CodezeroAgent({ orgID: string, orgAPIKey: string, spaceID: string })`

Returns implementation of an `http.Agent` that connects to the Teamspace with the given `spaceID`.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@c6o/codezero-agent",
"version": "1.0.0",
"version": "1.0.1",
"description": "Codezero http.Agent implementation for NodeJS",
"main": "dist/index.mjs",
"types": "dist/index.d.mts",
Expand Down
67 changes: 42 additions & 25 deletions src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,34 @@ interface SpaceCredentials {
cert: string;
}

type CodezeroParams = {
orgID?: string;
orgApiKey?: string;
spaceID?: string;
};

export class CodezeroAgent extends HttpsProxyAgent<string> {
private _credentials: SpaceCredentials | null = null;
private _hubServerBaseUrl: string = 'https://hub.codezero.io';
private _hubServerBaseUrl: string = "https://hub.codezero.io";
private orgID: string;
private orgApiKey: string;
private spaceID: string;

constructor(params: CodezeroParams = {}) {
const orgID = params.orgID || process.env.CZ_ORG_ID;
const orgApiKey = params.orgApiKey || process.env.CZ_ORG_API_KEY;
const spaceID = params.spaceID || process.env.CZ_SPACE_ID;

constructor(
private orgID = process.env.CZ_ORG_ID,
private orgApiKey = process.env.CZ_ORG_API_KEY,
private spaceID = process.env.CZ_SPACE_ID
) {
if (!orgID || !orgApiKey || !spaceID) {
throw new Error("Missing CZ_ORG_ID, CZ_ORG_API_KEY or CZ_SPACE_ID");
}

super("https://127.0.0.1");

this.orgID = orgID;
this.orgApiKey = orgApiKey;
this.spaceID = spaceID;

if (process.env.CZ_HUB_SERVER_BASE_URL) {
this._hubServerBaseUrl = process.env.CZ_HUB_SERVER_BASE_URL!;
}
Expand All @@ -43,35 +57,38 @@ export class CodezeroAgent extends HttpsProxyAgent<string> {
return super.connect(req, opts);
}
private async getSpaceCredentials(): Promise<SpaceCredentials> {
if (!this._credentials || this.tokenExpired(this._credentials.token)) {
const spaceResponse = await fetch(
`${this._hubServerBaseUrl}/api/c6o/connect/c6oapi.v1.C6OService/GetSpaceConnection`,
{
method: "POST",
headers: {
Authorization: `${this.orgID}:${this.orgApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
spaceId: this.spaceID,
}),
}
);
const response = (await spaceResponse.json()) as any;
if (this._credentials && !this.tokenExpired(this._credentials.token)) {
return this._credentials;
}

if (!spaceResponse.ok) {
throw new Error(response.message);
const spaceResponse = await fetch(
`${this._hubServerBaseUrl}/api/c6o/connect/c6oapi.v1.C6OService/GetSpaceConnection`,
{
method: "POST",
headers: {
Authorization: `${this.orgID}:${this.orgApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
spaceId: this.spaceID,
}),
}
this._credentials = response as SpaceCredentials;
);
const response = (await spaceResponse.json()) as any;

if (!spaceResponse.ok) {
throw new Error(response.message);
}
this._credentials = response as SpaceCredentials;

return this._credentials;
}

private tokenExpired(token: string): boolean {
const [, payload] = token.split(".");
if (!payload) return true;

const decoded = JSON.parse(Buffer.from(payload, "base64").toString());
return decoded.exp - 2 * 60 * 1000 < Date.now() / 1000;
return decoded.exp - 2 * 60 < Date.now() / 1000;
}
}
52 changes: 44 additions & 8 deletions test/test.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Server, createServer } from 'http';
import { createServer as createSSLServer } from 'https';
import { readFileSync } from 'fs';
Expand All @@ -10,27 +10,41 @@ import { CodezeroAgent } from '../src/index.mts';
describe('CodezeroAgent', () => {
describe ('constructor', () => {
it('should not throw given org id, org api key and space id', () => {
expect(() => new CodezeroAgent("orgId", "orgApiKey", "spaceId")).not.toThrow("Missing CZ_ORG_ID, CZ_ORG_API_KEY or CZ_SPACE_ID");
expect(() => new CodezeroAgent({orgID: "orgId", orgApiKey: "orgApiKey", spaceID: "spaceId"})).not.toThrow("Missing CZ_ORG_ID, CZ_ORG_API_KEY or CZ_SPACE_ID");
});

it('should throw if org id, org api key or space id are missing', () => {
expect(() => new CodezeroAgent("1", "2")).toThrow("Missing CZ_ORG_ID, CZ_ORG_API_KEY or CZ_SPACE_ID");
expect(() => new CodezeroAgent({orgID: "orgId", orgApiKey: "orgApiKey"})).toThrow("Missing CZ_ORG_ID, CZ_ORG_API_KEY or CZ_SPACE_ID");
});
});

describe('node-fetch', () => {
let hubServer: Server;
let hubServerUrl: URL;
let receivedAuth: string;
let hubRequestCount = 0;

let proxy: ProxyServer;
let proxyUrl: URL;

let targetServer: Server;
let targetServerUrl: URL;
let spaceCreds: any

const createToken = (offsetSeconds: number) => {
const time = Date.now() / 1000 + offsetSeconds;
const payload = Buffer.from(JSON.stringify({ exp: time })).toString('base64');
return `header.${payload}`;
}

beforeAll(async() => {
hubServer = createServer((_req, res) => {
res.end(JSON.stringify({host: '127.0.0.1', token: 'token', cert: readFileSync(`${__dirname}/server.crt`).toString()}));
beforeEach(async() => {
spaceCreds = {host: '127.0.0.1', token: createToken(3*60), cert: readFileSync(`${__dirname}/server.crt`).toString()};

hubRequestCount = 0;
hubServer = createServer((req, res) => {
hubRequestCount++;
receivedAuth = req.headers.authorization!;
res.end(JSON.stringify(spaceCreds));
});
hubServerUrl = await listen(hubServer);
process.env.CZ_HUB_SERVER_BASE_URL = hubServerUrl.href;
Expand All @@ -50,7 +64,7 @@ describe('CodezeroAgent', () => {
targetServerUrl = await listen(targetServer);
})

afterAll(() => {
afterEach(() => {
hubServer.close();
proxy.close();
targetServer.close();
Expand All @@ -59,11 +73,33 @@ describe('CodezeroAgent', () => {
});

it('should forward fetch requests via a proxy', async () => {
const agent = new CodezeroAgent("orgId", "orgApiKey", "spaceId");
const agent = new CodezeroAgent({orgID: "orgId", orgApiKey: "orgApiKey", spaceID: "spaceId"});
const response = await fetch(targetServerUrl.href, { agent });

expect(response.status).toBe(200);
expect(await response.text()).toBe('Hello!');

expect(receivedAuth).toBe('orgId:orgApiKey');
});

it('should cache credentials in the agent instance', async () => {
const agent = new CodezeroAgent({orgID: "orgId", orgApiKey: "orgApiKey", spaceID: "spaceId"});
await fetch(targetServerUrl.href, { agent });
await fetch(targetServerUrl.href, { agent });

expect(hubRequestCount).toBe(1);
});

it('should refetch credentials if token is expired', async () => {
const agent = new CodezeroAgent({orgID: "orgId", orgApiKey: "orgApiKey", spaceID: "spaceId"});

await fetch(targetServerUrl.href, { agent });
expect(hubRequestCount).toBe(1);

agent['_credentials']!['token'] = createToken(-3*60);
await fetch(targetServerUrl.href, { agent });

expect(hubRequestCount).toBe(2);
});
});
});