diff --git a/README.md b/README.md index c9dbeb8..fe655b6 100644 --- a/README.md +++ b/README.md @@ -432,3 +432,95 @@ const renewedLease = await client.dynamicSecrets().leases.renew(newLease.lease.i **Returns:** - `ApiV1DynamicSecretsLeasesLeaseIdDelete200Response`: The renewed lease response _(doesn't contain new credentials)_. +### `projects` + +#### Create a new project + +```typescript +const project = await client.projects().create({ + projectName: "", + type: "secret-manager", // cert-manager, secret-manager, kms, ssh + projectDescription: "", // Optional + slug: "", // Optional + template: "", // Optional + kmsKeyId: "kms-key-id" // Optional +}); +``` + +**Parameters:** +- `projectName` (string): The name of the project to create. +- `type` (string): The type of project to create. Valid options are `secret-manager`, `cert-manager`, `kms`, `ssh` +- `projectDescription` (string): An optional description of the project to create. +- `slug` (string): An optional slug for the project to create. If not provided, one will be generated automatically. +- `template` (string): Optionally provide a project template name to use for creating this project. +- `kmsKeyId` (string): The ID of the KMS key to use for the project. Will use the Infisical KMS by default. + +**Returns:** +- `ApiV1WorkspaceWorkspaceIdGet200ResponseWorkspace`: The project that was created. + + +#### Invite members to a project + +When inviting members to projects, you must either specify the `emails` or `usernames`. If neither are specified, the SDK will throw an error. + +```typescript +const memberships = await client.projects().inviteMembers({ + projectId: project.id, + emails: ["test1@example.com", "test2@example.com"], // Optional + usernames: ["example-user3", "example-user4"] // Optional + roleSlugs: ["member"] // Optional +}); +``` + +**Parameters:** +- `projectId`: (string): The ID of the project to invite members to +- `emails`: (string[]): An array of emails of the users to invite to the project. +- `usernames`: (string[]) An array of usernames of the users to invite to the project. +- `roleSlugs`: (string[]): An array of role slugs to assign to the members. If not specified, this will default to `member`. + +**Returns:** +- `ApiV1OrganizationAdminProjectsProjectIdGrantAdminAccessPost200ResponseMembership`: An array of the created project memberships. + +### `environments` + +#### Create a new environment + +```typescript +const environment = await client.environments().create({ + name: "", + projectId: "", + slug: "", + position: 1 // Optional +}); +``` + +**Parameters:** +- `name` (string): The name of the environment to be created. +- `projectId` (string): The ID of the project to create the environment within. +- `slug`: (string): The slug of the environment to be created. +- `position` (number): An optional position of the environment to be created. The position is used in the Infisical UI to display environments in order. Environments with the lowest position come first. + +**Returns:** +- `ApiV1WorkspaceWorkspaceIdEnvironmentsEnvIdGet200ResponseEnvironment`: The environment that was created. + +#### Create a new folder + +```typescript +const folder = await client.folders().create({ + name: "", + path: "", + projectId: "", + environment: "", + description: "" // Optional +}); +``` + +**Parameters:** +- `name` (string): The name of the folder to create. +- `path` (string): The path where of where to create the folder. Defaults to `/`, which is the root folder. +- `projectId` (string): The ID of the project to create the folder within. +- `environment` (string): The slug of the environment to create the folder within. +- `description` (string): An optional folder description. + +**Returns:** +- `ApiV1FoldersPost200ResponseFolder`: The folder that was created. diff --git a/src/custom/environments.ts b/src/custom/environments.ts new file mode 100644 index 0000000..ee4c151 --- /dev/null +++ b/src/custom/environments.ts @@ -0,0 +1,33 @@ +import { RawAxiosRequestConfig } from "axios"; +import { DefaultApi as InfisicalApi } from "../infisicalapi_client"; +import type { ApiV1WorkspaceWorkspaceIdEnvironmentsPostRequest, ApiV1WorkspaceWorkspaceIdEnvironmentsPost200Response } from "../infisicalapi_client"; +import { newInfisicalError } from "./errors"; + +export type CreateEnvironmentOptions = { + projectId: string; +} & ApiV1WorkspaceWorkspaceIdEnvironmentsPostRequest; +export type CreateEnvironmentResult = ApiV1WorkspaceWorkspaceIdEnvironmentsPost200Response; + +export default class EnvironmentsClient { + #apiInstance: InfisicalApi; + #requestOptions: RawAxiosRequestConfig | undefined; + constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) { + this.#apiInstance = apiInstance; + this.#requestOptions = requestOptions; + } + + create = async (options: CreateEnvironmentOptions): Promise => { + try { + const res = await this.#apiInstance.apiV1WorkspaceWorkspaceIdEnvironmentsPost( + { + workspaceId: options.projectId, + apiV1WorkspaceWorkspaceIdEnvironmentsPostRequest: options + }, + this.#requestOptions + ); + return res.data.environment; + } catch (err) { + throw newInfisicalError(err); + } + }; +} diff --git a/src/custom/errors.ts b/src/custom/errors.ts index 9b75307..b01bbe4 100644 --- a/src/custom/errors.ts +++ b/src/custom/errors.ts @@ -33,7 +33,12 @@ export const newInfisicalError = (error: any) => { const data = error?.response?.data as TApiErrorResponse; if (data?.message) { - return new InfisicalSDKRequestError(data.message, { + let message = data.message; + if (error.status === 422) { + message = JSON.stringify(data); + } + + return new InfisicalSDKRequestError(message, { url: error.response?.config.url || "", method: error.response?.config.method || "", statusCode: error.response?.status || 0 diff --git a/src/custom/folders.ts b/src/custom/folders.ts new file mode 100644 index 0000000..30340d6 --- /dev/null +++ b/src/custom/folders.ts @@ -0,0 +1,35 @@ +import { RawAxiosRequestConfig } from "axios"; +import { DefaultApi as InfisicalApi } from "../infisicalapi_client"; +import type { ApiV1FoldersPostRequest, ApiV1FoldersPost200Response } from "../infisicalapi_client"; +import { newInfisicalError } from "./errors"; + +export type CreateFolderOptions = { + projectId: string; +} & Omit; +export type CreateFolderResult = ApiV1FoldersPost200Response; + +export default class FoldersClient { + #apiInstance: InfisicalApi; + #requestOptions: RawAxiosRequestConfig | undefined; + constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) { + this.#apiInstance = apiInstance; + this.#requestOptions = requestOptions; + } + + create = async (options: CreateFolderOptions): Promise => { + try { + const res = await this.#apiInstance.apiV1FoldersPost( + { + apiV1FoldersPostRequest: { + ...options, + workspaceId: options.projectId + } + }, + this.#requestOptions + ); + return res.data.folder; + } catch (err) { + throw newInfisicalError(err); + } + }; +} diff --git a/src/custom/projects.ts b/src/custom/projects.ts new file mode 100644 index 0000000..ce6cd57 --- /dev/null +++ b/src/custom/projects.ts @@ -0,0 +1,56 @@ +import { RawAxiosRequestConfig } from "axios"; +import { DefaultApi as InfisicalApi } from "../infisicalapi_client"; +import type { + ApiV2WorkspacePost200Response, + ApiV2WorkspacePostRequest, + ApiV2WorkspaceProjectIdMembershipsPost200Response, + ApiV2WorkspaceProjectIdMembershipsPostRequest +} from "../infisicalapi_client"; +import { newInfisicalError } from "./errors"; + +export type CreateProjectOptions = ApiV2WorkspacePostRequest; +export type CreateProjectResult = ApiV2WorkspacePost200Response; + +export type InviteMemberToProjectOptions = { projectId: string } & ApiV2WorkspaceProjectIdMembershipsPostRequest; +export type InviteMemberToProjectResult = ApiV2WorkspaceProjectIdMembershipsPost200Response; +export default class ProjectsClient { + #apiInstance: InfisicalApi; + #requestOptions: RawAxiosRequestConfig | undefined; + constructor(apiInstance: InfisicalApi, requestOptions: RawAxiosRequestConfig | undefined) { + this.#apiInstance = apiInstance; + this.#requestOptions = requestOptions; + } + + create = async (options: CreateProjectOptions): Promise => { + try { + const res = await this.#apiInstance.apiV2WorkspacePost( + { + apiV2WorkspacePostRequest: options + }, + this.#requestOptions + ); + return res.data.project; + } catch (err) { + throw newInfisicalError(err); + } + }; + + inviteMembers = async (options: InviteMemberToProjectOptions): Promise => { + try { + if (!options.usernames?.length && !options.emails?.length) { + throw new Error("Either usernames or emails must be provided"); + } + + const res = await this.#apiInstance.apiV2WorkspaceProjectIdMembershipsPost( + { + projectId: options.projectId, + apiV2WorkspaceProjectIdMembershipsPostRequest: options + }, + this.#requestOptions + ); + return res.data.memberships; + } catch (err) { + throw newInfisicalError(err); + } + }; +} diff --git a/src/index.ts b/src/index.ts index 94ee7a1..64d5965 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,9 @@ import { RawAxiosRequestConfig } from "axios"; import DynamicSecretsClient from "./custom/dynamic-secrets"; import * as ApiClient from "./infisicalapi_client"; +import EnvironmentsClient from "./custom/environments"; +import ProjectsClient from "./custom/projects"; +import FoldersClient from "./custom/folders"; const buildRestClient = (apiClient: InfisicalApi, requestOptions?: RawAxiosRequestConfig) => { return { @@ -26,6 +29,9 @@ class InfisicalSDK { #requestOptions: RawAxiosRequestConfig | undefined; #secretsClient: SecretsClient; #dynamicSecretsClient: DynamicSecretsClient; + #environmentsClient: EnvironmentsClient; + #projectsClient: ProjectsClient; + #foldersClient: FoldersClient; #authClient: AuthClient; #basePath: string; @@ -41,6 +47,9 @@ class InfisicalSDK { 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.#environmentsClient = new EnvironmentsClient(this.#apiInstance, this.#requestOptions); + this.#projectsClient = new ProjectsClient(this.#apiInstance, this.#requestOptions); + this.#foldersClient = new FoldersClient(this.#apiInstance, this.#requestOptions); this.rest = () => buildRestClient(this.#apiInstance, this.#requestOptions); } @@ -62,11 +71,17 @@ class InfisicalSDK { 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, accessToken); + this.#environmentsClient = new EnvironmentsClient(this.#apiInstance, this.#requestOptions); + this.#projectsClient = new ProjectsClient(this.#apiInstance, this.#requestOptions); + this.#foldersClient = new FoldersClient(this.#apiInstance, this.#requestOptions); return this; } secrets = () => this.#secretsClient; + environments = () => this.#environmentsClient; + projects = () => this.#projectsClient; + folders = () => this.#foldersClient; dynamicSecrets = () => this.#dynamicSecretsClient; auth = () => this.#authClient; rest = () => buildRestClient(this.#apiInstance, this.#requestOptions); diff --git a/test/index.ts b/test/index.ts index 725a7b0..caea1a1 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,65 +1,52 @@ import { InfisicalSDK } from "../src"; -const PROJECT_ID = "PROJECT_ID"; - (async () => { const client = new InfisicalSDK({ siteUrl: "http://localhost:8080" // Optional, defaults to https://app.infisical.com }); + const EMAIL_TO_INVITE = ""; + + const universalAuthClientId = process.env.UNIVERSAL_AUTH_CLIENT_ID; + const universalAuthClientSecret = process.env.UNIVERSAL_AUTH_CLIENT_SECRET; + + if (!universalAuthClientId || !universalAuthClientSecret) { + throw new Error("UNIVERSAL_AUTH_CLIENT_ID and UNIVERSAL_AUTH_CLIENT_SECRET must be set"); + } + await client.auth().universalAuth.login({ - clientId: "CLIENT_ID", - clientSecret: "CLIENT_SECRET" + clientId: universalAuthClientId, + clientSecret: universalAuthClientSecret }); - const allSecrets = await client.secrets().listSecrets({ - environment: "dev", - projectId: PROJECT_ID, - expandSecretReferences: true, - includeImports: false, - recursive: false + console.log("Creating project"); + const project = await client.projects().create({ + projectDescription: "test description", + projectName: "test project1344assdfd", + type: "secret-manager", + slug: "test-project1assdfd43" }); - console.log(allSecrets.secrets); - const singleSecret = await client.secrets().getSecret({ - environment: "dev", - projectId: PROJECT_ID, - secretName: "TEST1", - expandSecretReferences: true, // Optional - includeImports: true, // Optional - - type: "shared", // Optional - version: 1 // Optional + const environment = await client.environments().create({ + position: 100, + slug: "test-environment-custom-slug", + name: "test environment", + projectId: project.id }); - console.log(`Fetched single secret, ${singleSecret}=${singleSecret.secretValue}`); - const newSecret = await client.secrets().createSecret("NEW_SECRET_NAME22423423", { - environment: "dev", - projectId: PROJECT_ID, - secretValue: "SECRET_VALUE" + console.log("Creating folder"); + const folder = await client.folders().create({ + name: "test-folder", + projectId: project.id, + environment: environment.slug }); - console.log(`You created a new secret: ${newSecret.secret.secretKey}`); - const updatedSecret = await client.secrets().updateSecret("NEW_SECRET_NAME22423423", { - environment: "dev", - projectId: PROJECT_ID, - secretValue: "UPDATED_SECRET_VALUE", - newSecretName: "NEW_SECRET_NAME22222", // Optional - secretComment: "This is an updated secret", // Optional - - secretReminderNote: "This is an updated reminder note", // Optional - secretReminderRepeatDays: 14, // Optional - skipMultilineEncoding: false, // Optional - metadata: { - // Optional - extra: "metadata" - } + console.log("Inviting member to project"); + const memberships = await client.projects().inviteMembers({ + projectId: project.id, + emails: [EMAIL_TO_INVITE], + roleSlugs: ["admin"] }); - console.log(`You updated the secret: ${updatedSecret.secret.secretKey}`); - const deletedSecret = await client.secrets().deleteSecret("NEW_SECRET_NAME22222", { - environment: "dev", - projectId: PROJECT_ID - }); - console.log(`You deleted the secret: ${deletedSecret.secret.secretKey}`); + console.log("Memberships", memberships); })();