Skip to content

feat: create projects/project memberships/environments/folders #17

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

Merged
merged 4 commits into from
Apr 11, 2025
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
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<name-of-project>",
type: "secret-manager", // cert-manager, secret-manager, kms, ssh
projectDescription: "<project-description>", // Optional
slug: "<slug-of-project-to-create>", // Optional
template: "<project-template-name>", // 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: ["[email protected]", "[email protected]"], // 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: "<environment-name>",
projectId: "<your-project-id>",
slug: "<environment-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: "<folder-name>",
path: "<folder-path>",
projectId: "<your-project-id>",
environment: "<environment-slug>",
description: "<folder-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.
33 changes: 33 additions & 0 deletions src/custom/environments.ts
Original file line number Diff line number Diff line change
@@ -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<CreateEnvironmentResult["environment"]> => {
try {
const res = await this.#apiInstance.apiV1WorkspaceWorkspaceIdEnvironmentsPost(
{
workspaceId: options.projectId,
apiV1WorkspaceWorkspaceIdEnvironmentsPostRequest: options
},
this.#requestOptions
);
return res.data.environment;
} catch (err) {
throw newInfisicalError(err);
}
};
}
7 changes: 6 additions & 1 deletion src/custom/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions src/custom/folders.ts
Original file line number Diff line number Diff line change
@@ -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<ApiV1FoldersPostRequest, "workspaceId" | "directory">;
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<CreateFolderResult["folder"]> => {
try {
const res = await this.#apiInstance.apiV1FoldersPost(
{
apiV1FoldersPostRequest: {
...options,
workspaceId: options.projectId
}
},
this.#requestOptions
);
return res.data.folder;
} catch (err) {
throw newInfisicalError(err);
}
};
}
56 changes: 56 additions & 0 deletions src/custom/projects.ts
Original file line number Diff line number Diff line change
@@ -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<CreateProjectResult["project"]> => {
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<InviteMemberToProjectResult["memberships"]> => {
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);
}
};
}
15 changes: 15 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,6 +29,9 @@ class InfisicalSDK {
#requestOptions: RawAxiosRequestConfig | undefined;
#secretsClient: SecretsClient;
#dynamicSecretsClient: DynamicSecretsClient;
#environmentsClient: EnvironmentsClient;
#projectsClient: ProjectsClient;
#foldersClient: FoldersClient;
#authClient: AuthClient;
#basePath: string;

Expand All @@ -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);
}

Expand All @@ -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);
Expand Down
79 changes: 33 additions & 46 deletions test/index.ts
Original file line number Diff line number Diff line change
@@ -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
});

await client.auth().universalAuth.login({
clientId: "CLIENT_ID",
clientSecret: "CLIENT_SECRET"
});
const EMAIL_TO_INVITE = "<your-email>";

const allSecrets = await client.secrets().listSecrets({
environment: "dev",
projectId: PROJECT_ID,
expandSecretReferences: true,
includeImports: false,
recursive: false
});
console.log(allSecrets.secrets);
const universalAuthClientId = process.env.UNIVERSAL_AUTH_CLIENT_ID;
const universalAuthClientSecret = process.env.UNIVERSAL_AUTH_CLIENT_SECRET;

const singleSecret = await client.secrets().getSecret({
environment: "dev",
projectId: PROJECT_ID,
secretName: "TEST1",
expandSecretReferences: true, // Optional
includeImports: true, // Optional
if (!universalAuthClientId || !universalAuthClientSecret) {
throw new Error("UNIVERSAL_AUTH_CLIENT_ID and UNIVERSAL_AUTH_CLIENT_SECRET must be set");
}

type: "shared", // Optional
version: 1 // Optional
await client.auth().universalAuth.login({
clientId: universalAuthClientId,
clientSecret: universalAuthClientSecret
});
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 project");
const project = await client.projects().create({
projectDescription: "test description",
projectName: "test project1344assdfd",
type: "secret-manager",
slug: "test-project1assdfd43"
});
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
const environment = await client.environments().create({
position: 100,
slug: "test-environment-custom-slug",
name: "test environment",
projectId: project.id
});

secretReminderNote: "This is an updated reminder note", // Optional
secretReminderRepeatDays: 14, // Optional
skipMultilineEncoding: false, // Optional
metadata: {
// Optional
extra: "metadata"
}
console.log("Creating folder");
const folder = await client.folders().create({
name: "test-folder",
projectId: project.id,
environment: environment.slug
});
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("Inviting member to project");
const memberships = await client.projects().inviteMembers({
projectId: project.id,
emails: [EMAIL_TO_INVITE],
roleSlugs: ["admin"]
});
console.log(`You deleted the secret: ${deletedSecret.secret.secretKey}`);

console.log("Memberships", memberships);
})();