Merge pull request #5 from Infisical/daniel/less-verbose-errors

feat: less verbose errors
This commit is contained in:
Daniel Hougaard
2024-10-10 02:09:20 +04:00
committed by GitHub
8 changed files with 298 additions and 131 deletions

View File

@@ -44,6 +44,7 @@ The `Auth` component provides methods for authentication:
#### Universal Auth
#### Authenticating
```typescript
await client.auth().universalAuth.login({
clientId: "<machine-identity-client-id>",
@@ -56,6 +57,11 @@ await client.auth().universalAuth.login({
- `clientId` (string): The client ID of your Machine Identity.
- `clientSecret` (string): The client secret of your Machine Identity.
#### Renewing
You can renew the authentication token that is currently set by using the `renew()` method.
```typescript
await client.auth().universalAuth.renew();
#### Manually set access token
By default, when you run a successful `.login()` method call, the access token returned will be auto set for the client instance. However, if you wish to set the access token manually, you may use this method.
@@ -73,6 +79,7 @@ client.auth().accessToken("<your-access-token>")
> [!NOTE]
> AWS IAM auth only works when the SDK is being used from within an AWS service, such as Lambda, EC2, etc.
#### Authenticating
```typescript
await client.auth().awsIamAuth.login({
identityId: "<your-identity-id>"
@@ -83,6 +90,13 @@ await client.auth().awsIamAuth.login({
- `options` (object):
- `identityId` (string): The ID of your identity
#### Renewing
You can renew the authentication token that is currently set by using the `renew()` method.
```typescript
await client.auth().awsIamAuth.renew();
```
### `secrets`

View File

@@ -2,6 +2,7 @@ import { InfisicalSDK } from "..";
import { ApiV1AuthUniversalAuthLoginPostRequest } from "../infisicalapi_client";
import { DefaultApi as InfisicalApi } from "../infisicalapi_client";
import { MACHINE_IDENTITY_ID_ENV_NAME } from "./constants";
import { InfisicalSDKError, newInfisicalError } from "./errors";
import { getAwsRegion, performAwsIamLogin } from "./util";
type AuthenticatorFunction = (accessToken: string) => InfisicalSDK;
@@ -10,47 +11,90 @@ type AwsAuthLoginOptions = {
identityId?: string;
};
export const renewToken = async (apiClient: InfisicalApi, token?: string) => {
try {
if (!token) {
throw new InfisicalSDKError("Unable to renew access token, no access token set. Are you sure you're authenticated?");
}
const res = await apiClient.apiV1AuthTokenRenewPost({
apiV1AuthTokenRenewPostRequest: {
accessToken: token
}
});
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
};
export default class AuthClient {
#sdkAuthenticator: AuthenticatorFunction;
#apiClient: InfisicalApi;
#baseUrl: string;
#accessToken?: string;
constructor(authenticator: AuthenticatorFunction, apiInstance: InfisicalApi, baseUrl: string) {
constructor(authenticator: AuthenticatorFunction, apiInstance: InfisicalApi, accessToken?: string) {
this.#sdkAuthenticator = authenticator;
this.#apiClient = apiInstance;
this.#baseUrl = baseUrl;
this.#accessToken = accessToken;
}
awsIamAuth = {
login: async (options?: AwsAuthLoginOptions) => {
const identityId = options?.identityId || process.env[MACHINE_IDENTITY_ID_ENV_NAME];
try {
const identityId = options?.identityId || process.env[MACHINE_IDENTITY_ID_ENV_NAME];
if (!identityId) {
throw new Error("Identity ID is required for AWS IAM authentication");
}
const iamRequest = await performAwsIamLogin(await getAwsRegion());
const res = await this.#apiClient.apiV1AuthAwsAuthLoginPost({
apiV1AuthAwsAuthLoginPostRequest: {
iamHttpRequestMethod: iamRequest.iamHttpRequestMethod,
iamRequestBody: Buffer.from(iamRequest.iamRequestBody).toString("base64"),
iamRequestHeaders: Buffer.from(JSON.stringify(iamRequest.iamRequestHeaders)).toString("base64"),
identityId
if (!identityId) {
throw new InfisicalSDKError("Identity ID is required for AWS IAM authentication");
}
});
return this.#sdkAuthenticator(res.data.accessToken);
const iamRequest = await performAwsIamLogin(await getAwsRegion());
const res = await this.#apiClient.apiV1AuthAwsAuthLoginPost({
apiV1AuthAwsAuthLoginPostRequest: {
iamHttpRequestMethod: iamRequest.iamHttpRequestMethod,
iamRequestBody: Buffer.from(iamRequest.iamRequestBody).toString("base64"),
iamRequestHeaders: Buffer.from(JSON.stringify(iamRequest.iamRequestHeaders)).toString("base64"),
identityId
}
});
return this.#sdkAuthenticator(res.data.accessToken);
} catch (err) {
throw newInfisicalError(err);
}
},
renew: async () => {
try {
const refreshedToken = await renewToken(this.#apiClient, this.#accessToken);
return this.#sdkAuthenticator(refreshedToken.accessToken);
} catch (err) {
throw newInfisicalError(err);
}
}
};
universalAuth = {
login: async (options: ApiV1AuthUniversalAuthLoginPostRequest) => {
const res = await this.#apiClient.apiV1AuthUniversalAuthLoginPost({
apiV1AuthUniversalAuthLoginPostRequest: options
});
try {
const res = await this.#apiClient.apiV1AuthUniversalAuthLoginPost({
apiV1AuthUniversalAuthLoginPostRequest: options
});
return this.#sdkAuthenticator(res.data.accessToken);
return this.#sdkAuthenticator(res.data.accessToken);
} catch (err) {
throw newInfisicalError(err);
}
},
renew: async () => {
try {
const refreshedToken = await renewToken(this.#apiClient, this.#accessToken);
return this.#sdkAuthenticator(refreshedToken.accessToken);
} catch (err) {
throw newInfisicalError(err);
}
}
};

View File

@@ -9,6 +9,7 @@ import type {
} from "../infisicalapi_client";
import type { TDynamicSecretProvider } from "./schemas/dynamic-secrets";
import { newInfisicalError } from "./errors";
type CreateDynamicSecretOptions = Omit<DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"], "provider"> & {
provider: TDynamicSecretProvider;
@@ -23,67 +24,87 @@ export default class DynamicSecretsClient {
}
async create(options: CreateDynamicSecretOptions) {
const res = await this.#apiInstance.apiV1DynamicSecretsPost(
{
apiV1DynamicSecretsPostRequest: options as DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"]
},
this.#requestOptions
);
return res.data.dynamicSecret;
}
async delete(dynamicSecretName: string, options: DefaultApiApiV1DynamicSecretsNameDeleteRequest["apiV1DynamicSecretsNameDeleteRequest"]) {
const res = await this.#apiInstance.apiV1DynamicSecretsNameDelete(
{
name: dynamicSecretName,
apiV1DynamicSecretsNameDeleteRequest: options
},
this.#requestOptions
);
return res.data.dynamicSecret;
}
leases = {
create: async (options: DefaultApiApiV1DynamicSecretsLeasesPostRequest["apiV1DynamicSecretsLeasesPostRequest"]) => {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesPost(
try {
const res = await this.#apiInstance.apiV1DynamicSecretsPost(
{
apiV1DynamicSecretsLeasesPostRequest: options
apiV1DynamicSecretsPostRequest: options as DefaultApiApiV1DynamicSecretsPostRequest["apiV1DynamicSecretsPostRequest"]
},
this.#requestOptions
);
return res.data;
return res.data.dynamicSecret;
} catch (err) {
throw newInfisicalError(err);
}
}
async delete(dynamicSecretName: string, options: DefaultApiApiV1DynamicSecretsNameDeleteRequest["apiV1DynamicSecretsNameDeleteRequest"]) {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsNameDelete(
{
name: dynamicSecretName,
apiV1DynamicSecretsNameDeleteRequest: options
},
this.#requestOptions
);
return res.data.dynamicSecret;
} catch (err) {
throw newInfisicalError(err);
}
}
leases = {
create: async (options: DefaultApiApiV1DynamicSecretsLeasesPostRequest["apiV1DynamicSecretsLeasesPostRequest"]) => {
try {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesPost(
{
apiV1DynamicSecretsLeasesPostRequest: options
},
this.#requestOptions
);
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
},
delete: async (
leaseId: string,
options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdDeleteRequest["apiV1DynamicSecretsLeasesLeaseIdDeleteRequest"]
) => {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdDelete(
{
leaseId: leaseId,
apiV1DynamicSecretsLeasesLeaseIdDeleteRequest: options
},
this.#requestOptions
);
try {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdDelete(
{
leaseId: leaseId,
apiV1DynamicSecretsLeasesLeaseIdDeleteRequest: options
},
this.#requestOptions
);
return res.data;
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
},
renew: async (
leaseId: string,
options: DefaultApiApiV1DynamicSecretsLeasesLeaseIdRenewPostRequest["apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest"]
) => {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdRenewPost(
{
leaseId: leaseId,
apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest: options
},
this.#requestOptions
);
try {
const res = await this.#apiInstance.apiV1DynamicSecretsLeasesLeaseIdRenewPost(
{
leaseId: leaseId,
apiV1DynamicSecretsLeasesLeaseIdRenewPostRequest: options
},
this.#requestOptions
);
return res.data;
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
}
};
}

52
src/custom/errors.ts Normal file
View File

@@ -0,0 +1,52 @@
import { AxiosError } from "axios";
type TApiErrorResponse = {
statusCode: number;
message: string;
error: string;
};
export class InfisicalSDKError extends Error {
constructor(message: string) {
super(message);
this.message = message;
this.name = "InfisicalSDKError";
}
}
export class InfisicalSDKRequestError extends Error {
constructor(
message: string,
requestData: {
url: string;
method: string;
statusCode: number;
}
) {
super(message);
this.message = `[URL=${requestData.url}] [Method=${requestData.method}] [StatusCode=${requestData.statusCode}] ${message}`;
this.name = "InfisicalSDKRequestError";
}
}
export const newInfisicalError = (error: any) => {
if (error instanceof AxiosError) {
const data = error?.response?.data as TApiErrorResponse;
if (data?.message) {
return new InfisicalSDKRequestError(data.message, {
url: error.response?.config.url || "",
method: error.response?.config.method || "",
statusCode: error.response?.status || 0
});
} else if (error.message) {
return new InfisicalSDKError(error.message);
} else if (error.code) {
// If theres no message but a code is present, it's likely to be an aggregation error. This is not specific to Axios, but it falls under the AxiosError type
return new InfisicalSDKError(error.code);
} else {
return new InfisicalSDKError("Request failed with unknown error");
}
}
return new InfisicalSDKError(error?.message || "An error occurred");
};

View File

@@ -165,6 +165,17 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1)
});
export const LdapSchema = z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
});
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@@ -175,7 +186,8 @@ export enum DynamicSecretProviders {
ElasticSearch = "elastic-search",
MongoDB = "mongo-db",
RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id"
AzureEntraID = "azure-entra-id",
Ldap = "ldap"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -188,7 +200,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
]);
export type TDynamicSecretProvider = z.infer<typeof DynamicSecretProviderSchema>;

View File

@@ -5,6 +5,7 @@ import type {
DefaultApiApiV3SecretsRawSecretNamePatchRequest,
DefaultApiApiV3SecretsRawSecretNamePostRequest
} from "../infisicalapi_client";
import { newInfisicalError } from "./errors";
type SecretType = "shared" | "personal";
@@ -48,80 +49,101 @@ export default class SecretsClient {
#requestOptions: RawAxiosRequestConfig | undefined;
constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) {
this.#apiInstance = apiInstance;
this.#requestOptions = requestOptions;
}
listSecrets = async (options: ListSecretsOptions) => {
const res = await this.#apiInstance.apiV3SecretsRawGet(
{
environment: options.environment,
workspaceId: options.projectId,
expandSecretReferences: convertBool(options.expandSecretReferences),
includeImports: convertBool(options.includeImports),
recursive: convertBool(options.recursive),
secretPath: options.secretPath,
tagSlugs: options.tagSlugs ? options.tagSlugs.join(",") : undefined
},
this.#requestOptions
);
return res.data;
try {
const res = await this.#apiInstance.apiV3SecretsRawGet(
{
environment: options.environment,
workspaceId: options.projectId,
expandSecretReferences: convertBool(options.expandSecretReferences),
includeImports: convertBool(options.includeImports),
recursive: convertBool(options.recursive),
secretPath: options.secretPath,
tagSlugs: options.tagSlugs ? options.tagSlugs.join(",") : undefined
},
this.#requestOptions
);
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
};
getSecret = async (options: GetSecretOptions) => {
const res = await this.#apiInstance.apiV3SecretsRawSecretNameGet(
{
environment: options.environment,
secretName: options.secretName,
workspaceId: options.projectId,
expandSecretReferences: convertBool(options.expandSecretReferences),
includeImports: convertBool(options.includeImports),
secretPath: options.secretPath,
type: options.type,
version: options.version
},
this.#requestOptions
);
return res.data.secret;
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNameGet(
{
environment: options.environment,
secretName: options.secretName,
workspaceId: options.projectId,
expandSecretReferences: convertBool(options.expandSecretReferences),
includeImports: convertBool(options.includeImports),
secretPath: options.secretPath,
type: options.type,
version: options.version
},
this.#requestOptions
);
return res.data.secret;
} catch (err) {
throw newInfisicalError(err);
}
};
updateSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePatchRequest["secretName"], options: UpdateSecretOptions) => {
const res = await this.#apiInstance.apiV3SecretsRawSecretNamePatch(
{
secretName,
apiV3SecretsRawSecretNamePatchRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data;
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNamePatch(
{
secretName,
apiV3SecretsRawSecretNamePatchRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
};
createSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNamePostRequest["secretName"], options: CreateSecretOptions) => {
const res = await this.#apiInstance.apiV3SecretsRawSecretNamePost(
{
secretName,
apiV3SecretsRawSecretNamePostRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data;
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNamePost(
{
secretName,
apiV3SecretsRawSecretNamePostRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
};
deleteSecret = async (secretName: DefaultApiApiV3SecretsRawSecretNameDeleteRequest["secretName"], options: DeleteSecretOptions) => {
const res = await this.#apiInstance.apiV3SecretsRawSecretNameDelete(
{
secretName,
apiV3SecretsRawSecretNameDeleteRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data;
try {
const res = await this.#apiInstance.apiV3SecretsRawSecretNameDelete(
{
secretName,
apiV3SecretsRawSecretNameDeleteRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data;
} catch (err) {
throw newInfisicalError(err);
}
};
}

View File

@@ -1,6 +1,7 @@
import axios from "axios";
import { AWS_IDENTITY_DOCUMENT_URI, AWS_TOKEN_METADATA_URI } from "./constants";
import AWS from "aws-sdk";
import { InfisicalSDKError } from "./errors";
export const getAwsRegion = async () => {
const region = process.env.AWS_REGION; // Typically found in lambda runtime environment
@@ -36,13 +37,13 @@ export const performAwsIamLogin = async (region: string) => {
region
});
const creds = await new Promise<{ sessionToken?: string; accessKeyId: string; secretAccessKey: string }>((resolve, reject) => {
await new Promise<{ sessionToken?: string; accessKeyId: string; secretAccessKey: string }>((resolve, reject) => {
AWS.config.getCredentials((err, res) => {
if (err) {
throw err;
} else {
if (!res) {
throw new Error("Credentials not found");
throw new InfisicalSDKError("Credentials not found");
}
return resolve(res);
}

View File

@@ -38,7 +38,7 @@ class InfisicalSDK {
})
);
this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance, this.#basePath);
this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance);
this.#dynamicSecretsClient = new DynamicSecretsClient(this.#apiInstance, this.#requestOptions);
this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions);
this.rest = () => buildRestClient(this.#apiInstance, this.#requestOptions);
@@ -61,7 +61,7 @@ class InfisicalSDK {
this.rest = () => buildRestClient(this.#apiInstance, this.#requestOptions);
this.#secretsClient = new SecretsClient(this.#apiInstance, this.#requestOptions);
this.#dynamicSecretsClient = new DynamicSecretsClient(this.#apiInstance, this.#requestOptions);
this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance, this.#basePath);
this.#authClient = new AuthClient(this.authenticate.bind(this), this.#apiInstance, accessToken);
return this;
}