Skip to content

feat: implicit init #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@phase.dev/phase-node",
"version": "3.1.1",
"description": "Node.js Server SDK for Phase",
"version": "3.2.0",
"description": "Node.js SDK for Phase",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": "https://github.com/phasehq/node-sdk",
Expand Down
51 changes: 31 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ export type {


export default class Phase {
token: string;
host: string;
tokenType: string | null = null;
version: string | null = null;
bearerToken: string | null = null;
keypair: PhaseKeyPair = {} as PhaseKeyPair;
apps: App[] = [];
private token: string;
private host: string;
private _tokenType: string | null = null;
private _version: string | null = null;
private _bearerToken: string | null = null;
private _keypair: PhaseKeyPair = {} as PhaseKeyPair;
private apps: App[] = [];
_isInitialized: boolean = false

constructor(token: string, host?: string) {
this.host = host || DEFAULT_HOST;
Expand All @@ -64,7 +65,7 @@ export default class Phase {
const data: SessionResponse = response.data;

// Set the keypair for the class instance
this.keypair = {
this._keypair = {
publicKey: this.token.split(":")[3],
privateKey: await reconstructPrivateKey(
data.wrapped_key_share,
Expand All @@ -80,7 +81,7 @@ export default class Phase {
const { publicKey, privateKey, salt } = await unwrapEnvKeys(
envData.wrapped_seed,
envData.wrapped_salt,
this.keypair
this._keypair
);

const { id, name } = envData.environment;
Expand All @@ -106,6 +107,7 @@ export default class Phase {

const apps: App[] = await Promise.all(appPromises);
this.apps = apps;
this._isInitialized = true
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
throw `Error: ${error.response.status}: ${
Expand Down Expand Up @@ -139,18 +141,18 @@ export default class Phase {
const [tokenType, version, bearerToken] = token.split(":");

// Assign the parsed values to the instance properties
this.tokenType = tokenType.includes("user")
this._tokenType = tokenType.includes("user")
? "User"
: version === "v1"
? "Service"
: "ServiceAccount";
this.version = version;
this.bearerToken = bearerToken;
this._version = version;
this._bearerToken = bearerToken;
}

private getAuthHeaders() {
return {
Authorization: `Bearer ${this.tokenType} ${this.bearerToken}`,
Authorization: `Bearer ${this._tokenType} ${this._bearerToken}`,
Accept: "application/json",
"X-Use-Camel-Case": true,
"User-agent": `phase-node-sdk/${LIB_VERSION}`,
Expand All @@ -159,6 +161,9 @@ export default class Phase {

async get(options: GetSecretOptions): Promise<Secret[]> {
return new Promise<Secret[]>(async (resolve, reject) => {

if (!this._isInitialized) await this.init()

const cache = new Map<string, string>();

const app = this.apps.find((app) => app.id === options.appId);
Expand All @@ -170,7 +175,7 @@ export default class Phase {
(e) => e.name.toLowerCase() === options.envName.toLowerCase()
);
if (!env) {
return reject(`Invalid environment name: ${options.envName}`);
return reject(`Invalid environment name: '${options.envName}'`);
}

try {
Expand Down Expand Up @@ -313,14 +318,16 @@ export default class Phase {
return new Promise<void>(async (resolve, reject) => {
const { appId, envName } = options;

if (!this._isInitialized) await this.init()

const app = this.apps.find((app) => app.id === appId);
if (!app) {
throw "Invalid app id";
}

const env = app?.environments.find((env) => env.name === envName);
const env = app?.environments.find((env) => env.name.toLowerCase() === envName.toLowerCase());
if (!env) {
throw "Invalid environment name";
throw `Invalid environment name: '${envName}'`;
}

try {
Expand Down Expand Up @@ -379,14 +386,16 @@ export default class Phase {
return new Promise<void>(async (resolve, reject) => {
const { appId, envName } = options;

if (!this._isInitialized) await this.init()

const app = this.apps.find((app) => app.id === appId);
if (!app) {
throw "Invalid app id";
}

const env = app?.environments.find((env) => env.name === envName);
const env = app?.environments.find((env) => env.name.toLowerCase() === envName.toLowerCase());
if (!env) {
throw "Invalid environment name";
throw `Invalid environment name: '${envName}'`;
}

try {
Expand Down Expand Up @@ -440,14 +449,16 @@ export default class Phase {
try {
const { appId, envName } = options;

if (!this._isInitialized) await this.init()

const app = this.apps.find((app) => app.id === appId);
if (!app) {
throw "Invalid app id";
}

const env = app?.environments.find((env) => env.name === envName);
const env = app?.environments.find((env) => env.name.toLowerCase() === envName.toLowerCase());
if (!env) {
throw "Invalid environment name";
throw `Invalid environment name: '${envName}'`;
}

const requestHeaders = { environment: env.id };
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const LIB_VERSION = "3.1.1";
export const LIB_VERSION = "3.2.0";
88 changes: 4 additions & 84 deletions tests/sdk/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,50 +243,10 @@ describe("Phase SDK - init() with valid user token", () => {
const phase = new Phase(validUserToken, mockHost);
await phase.init();

expect(phase.token).toBe(validUserToken);
expect(phase.host).toBe(mockHost);
expect(phase.tokenType).toBe("User"); // Assuming the token is a user token
expect(phase.version).toBe("v1");
expect(phase.bearerToken).toBe(validUserToken.split(":")[2]); // Extracted from the token

expect(phase.keypair).toHaveProperty("publicKey");
expect(phase.keypair).toHaveProperty("privateKey");
expect(phase.keypair.privateKey).toBeDefined();

expect(phase.apps.length).toBe(mockResponse.apps.length);
for (let i = 0; i < phase.apps.length; i++) {
expect(phase.apps[i].id).toBe(mockResponse.apps[i].id);
expect(phase.apps[i].name).toBe(mockResponse.apps[i].name);

expect(phase.apps[i].environments.length).toBe(
mockResponse.apps[i].environment_keys.length
);

for (let j = 0; j < phase.apps[i].environments.length; j++) {
expect(phase.apps[i].environments[j].keypair.publicKey).toBeDefined();
expect(phase.apps[i].environments[j].keypair.privateKey).toBeDefined();
expect(phase.apps[i].environments[j].salt).toBeDefined();
}
}
expect(phase._isInitialized).toBe(true);
});

it("should reconstruct the private key and decrypt environment keys correctly with a user token", async () => {
const phase = new Phase(validUserToken, mockHost);
await phase.init();

// Ensure reconstructPrivateKey was called with real data
expect(phase.keypair.privateKey).toBeDefined();
expect(phase.keypair.publicKey).toBe(validUserToken.split(":")[3]);

// Verify each environment key was unwrapped correctly
for (const app of phase.apps) {
for (const env of app.environments) {
expect(env.keypair.publicKey).toBeDefined();
expect(env.keypair.privateKey).toBeDefined();
expect(env.salt).toBeDefined();
}
}
});

});

describe("Phase SDK - init() with valid service token", () => {
Expand Down Expand Up @@ -386,48 +346,8 @@ describe("Phase SDK - init() with valid service token", () => {
const phase = new Phase(validServiceToken, mockHost);
await phase.init();

expect(phase.token).toBe(validServiceToken);
expect(phase.host).toBe(mockHost);
expect(phase.tokenType).toBe("ServiceAccount");
expect(phase.version).toBe("v2");
expect(phase.bearerToken).toBe(validServiceToken.split(":")[2]); // Extracted from the token

expect(phase.keypair).toHaveProperty("publicKey");
expect(phase.keypair).toHaveProperty("privateKey");
expect(phase.keypair.privateKey).toBeDefined();

expect(phase.apps.length).toBe(mockResponse.apps.length);
for (let i = 0; i < phase.apps.length; i++) {
expect(phase.apps[i].id).toBe(mockResponse.apps[i].id);
expect(phase.apps[i].name).toBe(mockResponse.apps[i].name);

expect(phase.apps[i].environments.length).toBe(
mockResponse.apps[i].environment_keys.length
);

for (let j = 0; j < phase.apps[i].environments.length; j++) {
expect(phase.apps[i].environments[j].keypair.publicKey).toBeDefined();
expect(phase.apps[i].environments[j].keypair.privateKey).toBeDefined();
expect(phase.apps[i].environments[j].salt).toBeDefined();
}
}
expect(phase._isInitialized).toBe(true);
});

it("should reconstruct the private key and decrypt environment keys correctly with a service token", async () => {
const phase = new Phase(validServiceToken, mockHost);
await phase.init();

// Ensure reconstructPrivateKey was called with real data
expect(phase.keypair.privateKey).toBeDefined();
expect(phase.keypair.publicKey).toBe(validServiceToken.split(":")[3]);

// Verify each environment key was unwrapped correctly
for (const app of phase.apps) {
for (const env of app.environments) {
expect(env.keypair.publicKey).toBeDefined();
expect(env.keypair.privateKey).toBeDefined();
expect(env.salt).toBeDefined();
}
}
});

});