Merge branch 'main' into snyk-upgrade-785ded2802fbc87ba8e754f66e326fb6

This commit is contained in:
Maidul Islam
2024-02-21 12:00:34 -05:00
committed by GitHub
38 changed files with 2522 additions and 303 deletions
+4
View File
@@ -0,0 +1,4 @@
REDIS_URL=redis://localhost:6379
DB_CONNECTION_URI=postgres://infisical:infisical@localhost/infisical?sslmode=disable
AUTH_SECRET=4bnfe4e407b8921c104518903515b218
ENCRYPTION_KEY=4bnfe4e407b8921c104518903515b218
@@ -5,9 +5,14 @@ on:
- "infisical/v*.*.*-postgres" - "infisical/v*.*.*-postgres"
jobs: jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone: infisical-standalone:
name: Build infisical standalone image postgres name: Build infisical standalone image postgres
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [infisical-tests]
steps: steps:
- name: Extract version from tag - name: Extract version from tag
id: extract_version id: extract_version
+47
View File
@@ -0,0 +1,47 @@
name: "Run backend tests"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
workflow_call:
jobs:
check-be-pr:
name: Run integration test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: npm install
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
run: npm run test:e2e
working-directory: backend
env:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
AUTH_SECRET: something-random
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
+1
View File
@@ -1,2 +1,3 @@
vitest-environment-infisical.ts vitest-environment-infisical.ts
vitest.config.ts vitest.config.ts
vitest.e2e.config.ts
+12
View File
@@ -21,6 +21,18 @@ module.exports = {
tsconfigRootDir: __dirname tsconfigRootDir: __dirname
}, },
root: true, root: true,
overrides: [
{
files: ["./e2e-test/**/*"],
rules: {
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off",
}
}
],
rules: { rules: {
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off", "@typescript-eslint/no-unsafe-enum-comparison": "off",
@@ -0,0 +1,71 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { seedData1 } from "@app/db/seed-data";
export const createIdentity = async (name: string, role: string) => {
const createIdentityRes = await testServer.inject({
method: "POST",
url: "/api/v1/identities",
body: {
name,
role,
organizationId: seedData1.organization.id
},
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(createIdentityRes.statusCode).toBe(200);
return createIdentityRes.json().identity;
};
export const deleteIdentity = async (id: string) => {
const deleteIdentityRes = await testServer.inject({
method: "DELETE",
url: `/api/v1/identities/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(deleteIdentityRes.statusCode).toBe(200);
return deleteIdentityRes.json().identity;
};
describe("Identity v1", async () => {
test("Create identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull();
await deleteIdentity(newIdentity.id);
});
test("Update identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull();
const updatedIdentity = await testServer.inject({
method: "PATCH",
url: `/api/v1/identities/${newIdentity.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: "updated-mac-1",
role: OrgMembershipRole.Member
}
});
expect(updatedIdentity.statusCode).toBe(200);
expect(updatedIdentity.json().identity.name).toBe("updated-mac-1");
await deleteIdentity(newIdentity.id);
});
test("Delete Identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
const deletedIdentity = await deleteIdentity(newIdentity.id);
expect(deletedIdentity.name).toBe("mac1");
});
});
+2 -1
View File
@@ -1,6 +1,7 @@
import { seedData1 } from "@app/db/seed-data";
import jsrp from "jsrp"; import jsrp from "jsrp";
import { seedData1 } from "@app/db/seed-data";
describe("Login V1 Router", async () => { describe("Login V1 Router", async () => {
// eslint-disable-next-line // eslint-disable-next-line
const client = new jsrp.client(); const client = new jsrp.client();
+47 -66
View File
@@ -1,6 +1,40 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project"; import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
const createProjectEnvironment = async (name: string, slug: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name,
slug
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
return payload.environment;
};
const deleteProjectEnvironment = async (envId: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${envId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
return payload.environment;
};
describe("Project Environment Router", async () => { describe("Project Environment Router", async () => {
test("Get default environments", async () => { test("Get default environments", async () => {
const res = await testServer.inject({ const res = await testServer.inject({
@@ -31,24 +65,10 @@ describe("Project Environment Router", async () => {
expect(payload.workspace.environments.length).toBe(3); expect(payload.workspace.environments.length).toBe(3);
}); });
const mockProjectEnv = { name: "temp", slug: "temp", id: "" }; // id will be filled in create op const mockProjectEnv = { name: "temp", slug: "temp" }; // id will be filled in create op
test("Create environment", async () => { test("Create environment", async () => {
const res = await testServer.inject({ const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
method: "POST", expect(newEnvironment).toEqual(
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: mockProjectEnv.name,
slug: mockProjectEnv.slug
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
name: mockProjectEnv.name, name: mockProjectEnv.name,
@@ -59,14 +79,15 @@ describe("Project Environment Router", async () => {
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
); );
mockProjectEnv.id = payload.environment.id; await deleteProjectEnvironment(newEnvironment.id);
}); });
test("Update environment", async () => { test("Update environment", async () => {
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
const updatedName = { name: "temp#2", slug: "temp2" }; const updatedName = { name: "temp#2", slug: "temp2" };
const res = await testServer.inject({ const res = await testServer.inject({
method: "PATCH", method: "PATCH",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`, url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`,
headers: { headers: {
authorization: `Bearer ${jwtAuthToken}` authorization: `Bearer ${jwtAuthToken}`
}, },
@@ -82,7 +103,7 @@ describe("Project Environment Router", async () => {
expect(payload).toHaveProperty("environment"); expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual( expect(payload.environment).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: newEnvironment.id,
name: updatedName.name, name: updatedName.name,
slug: updatedName.slug, slug: updatedName.slug,
projectId: seedData1.project.id, projectId: seedData1.project.id,
@@ -91,61 +112,21 @@ describe("Project Environment Router", async () => {
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
); );
mockProjectEnv.name = updatedName.name; await deleteProjectEnvironment(newEnvironment.id);
mockProjectEnv.slug = updatedName.slug;
}); });
test("Delete environment", async () => { test("Delete environment", async () => {
const res = await testServer.inject({ const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
method: "DELETE", const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id);
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`, expect(deletedProjectEnvironment).toEqual(
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: deletedProjectEnvironment.id,
name: mockProjectEnv.name, name: mockProjectEnv.name,
slug: mockProjectEnv.slug, slug: mockProjectEnv.slug,
position: 1, position: 4,
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
); );
}); });
// after all these opreations the list of environment should be still same
test("Default list of environment", async () => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("workspace");
// check for default environments
expect(payload).toEqual({
workspace: expect.objectContaining({
name: seedData1.project.name,
id: seedData1.project.id,
slug: seedData1.project.slug,
environments: expect.arrayContaining([
expect.objectContaining(DEFAULT_PROJECT_ENVS[0]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[1]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[2])
])
})
});
// ensure only two default environments exist
expect(payload.workspace.environments.length).toBe(3);
});
}); });
@@ -1,5 +1,40 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
const createFolder = async (dto: { path: string; name: string }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: dto.name,
path: dto.path
}
});
expect(res.statusCode).toBe(200);
return res.json().folder;
};
const deleteFolder = async (dto: { path: string; id: string }) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${dto.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: dto.path
}
});
expect(res.statusCode).toBe(200);
return res.json().folder;
};
describe("Secret Folder Router", async () => { describe("Secret Folder Router", async () => {
test.each([ test.each([
{ name: "folder1", path: "/" }, // one in root { name: "folder1", path: "/" }, // one in root
@@ -7,30 +42,15 @@ describe("Secret Folder Router", async () => {
{ name: "folder2", path: "/" }, { name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing { name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
])("Create folder $name in $path", async ({ name, path }) => { ])("Create folder $name in $path", async ({ name, path }) => {
const res = await testServer.inject({ const createdFolder = await createFolder({ path, name });
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name,
path
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folder");
// check for default environments // check for default environments
expect(payload).toEqual({ expect(createdFolder).toEqual(
folder: expect.objectContaining({ expect.objectContaining({
name, name,
id: expect.any(String) id: expect.any(String)
}) })
}); );
await deleteFolder({ path, id: createdFolder.id });
}); });
test.each([ test.each([
@@ -43,6 +63,8 @@ describe("Secret Folder Router", async () => {
}, },
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } } { path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
])("Get folders $path", async ({ path, expected }) => { ])("Get folders $path", async ({ path, expected }) => {
const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path })));
const res = await testServer.inject({ const res = await testServer.inject({
method: "GET", method: "GET",
url: `/api/v1/folders`, url: `/api/v1/folders`,
@@ -59,36 +81,22 @@ describe("Secret Folder Router", async () => {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload); const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folders"); expect(payload).toHaveProperty("folders");
expect(payload.folders.length).toBe(expected.length); expect(payload.folders.length >= expected.folders.length).toBeTruthy();
expect(payload).toEqual({ folders: expected.folders.map((el) => expect.objectContaining(el)) }); expect(payload).toEqual({
}); folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
let toBeDeleteFolderId = "";
test("Update a deep folder", async () => {
const res = await testServer.inject({
method: "PATCH",
url: `/api/v1/folders/folder1`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-updated",
path: "/level1/level2"
}
}); });
expect(res.statusCode).toBe(200); await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
const payload = JSON.parse(res.payload); });
expect(payload).toHaveProperty("folder");
expect(payload.folder).toEqual( test("Update a deep folder", async () => {
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
expect(newFolder).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
name: "folder-updated" name: "folder-updated"
}) })
); );
toBeDeleteFolderId = payload.folder.id;
const resUpdatedFolders = await testServer.inject({ const resUpdatedFolders = await testServer.inject({
method: "GET", method: "GET",
@@ -106,14 +114,16 @@ describe("Secret Folder Router", async () => {
expect(resUpdatedFolders.statusCode).toBe(200); expect(resUpdatedFolders.statusCode).toBe(200);
const updatedFolderList = JSON.parse(resUpdatedFolders.payload); const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
expect(updatedFolderList).toHaveProperty("folders"); expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(1);
expect(updatedFolderList.folders[0].name).toEqual("folder-updated"); expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
}); });
test("Delete a deep folder", async () => { test("Delete a deep folder", async () => {
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
const res = await testServer.inject({ const res = await testServer.inject({
method: "DELETE", method: "DELETE",
url: `/api/v1/folders/${toBeDeleteFolderId}`, url: `/api/v1/folders/${newFolder.id}`,
headers: { headers: {
authorization: `Bearer ${jwtAuthToken}` authorization: `Bearer ${jwtAuthToken}`
}, },
@@ -1,32 +1,57 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
describe("Secret Folder Router", async () => { const createSecretImport = async (importPath: string, importEnv: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
environment: importEnv,
path: importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
const deleteSecretImport = async (id: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
describe("Secret Import Router", async () => {
test.each([ test.each([
{ importEnv: "dev", importPath: "/" }, // one in root { importEnv: "dev", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
environment: importEnv,
path: importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments // check for default environments
expect(payload.secretImport).toEqual( const payload = await createSecretImport(importPath, importEnv);
expect(payload).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath: expect.any(String), importPath: expect.any(String),
@@ -37,10 +62,12 @@ describe("Secret Folder Router", async () => {
}) })
}) })
); );
await deleteSecretImport(payload.id);
}); });
let testSecretImportId = "";
test("Get secret imports", async () => { test("Get secret imports", async () => {
const createdImport1 = await createSecretImport("/", "dev");
const createdImport2 = await createSecretImport("/", "staging");
const res = await testServer.inject({ const res = await testServer.inject({
method: "GET", method: "GET",
url: `/api/v1/secret-imports`, url: `/api/v1/secret-imports`,
@@ -58,7 +85,6 @@ describe("Secret Folder Router", async () => {
const payload = JSON.parse(res.payload); const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImports"); expect(payload).toHaveProperty("secretImports");
expect(payload.secretImports.length).toBe(2); expect(payload.secretImports.length).toBe(2);
testSecretImportId = payload.secretImports[0].id;
expect(payload.secretImports).toEqual( expect(payload.secretImports).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -72,12 +98,20 @@ describe("Secret Folder Router", async () => {
}) })
]) ])
); );
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
}); });
test("Update secret import position", async () => { test("Update secret import position", async () => {
const res = await testServer.inject({ const devImportDetails = { path: "/", envSlug: "dev" };
const stagingImportDetails = { path: "/", envSlug: "staging" };
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
const updateImportRes = await testServer.inject({
method: "PATCH", method: "PATCH",
url: `/api/v1/secret-imports/${testSecretImportId}`, url: `/api/v1/secret-imports/${createdImport1.id}`,
headers: { headers: {
authorization: `Bearer ${jwtAuthToken}` authorization: `Bearer ${jwtAuthToken}`
}, },
@@ -91,8 +125,8 @@ describe("Secret Folder Router", async () => {
} }
}); });
expect(res.statusCode).toBe(200); expect(updateImportRes.statusCode).toBe(200);
const payload = JSON.parse(res.payload); const payload = JSON.parse(updateImportRes.payload);
expect(payload).toHaveProperty("secretImport"); expect(payload).toHaveProperty("secretImport");
// check for default environments // check for default environments
expect(payload.secretImport).toEqual( expect(payload.secretImport).toEqual(
@@ -102,7 +136,7 @@ describe("Secret Folder Router", async () => {
position: 2, position: 2,
importEnv: expect.objectContaining({ importEnv: expect.objectContaining({
name: expect.any(String), name: expect.any(String),
slug: expect.any(String), slug: expect.stringMatching(devImportDetails.envSlug),
id: expect.any(String) id: expect.any(String)
}) })
}) })
@@ -124,28 +158,19 @@ describe("Secret Folder Router", async () => {
expect(secretImportsListRes.statusCode).toBe(200); expect(secretImportsListRes.statusCode).toBe(200);
const secretImportList = JSON.parse(secretImportsListRes.payload); const secretImportList = JSON.parse(secretImportsListRes.payload);
expect(secretImportList).toHaveProperty("secretImports"); expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports[1].id).toEqual(testSecretImportId); expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
}); });
test("Delete secret import position", async () => { test("Delete secret import position", async () => {
const res = await testServer.inject({ const createdImport1 = await createSecretImport("/", "dev");
method: "DELETE", const createdImport2 = await createSecretImport("/", "staging");
url: `/api/v1/secret-imports/${testSecretImportId}`, const deletedImport = await deleteSecretImport(createdImport1.id);
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments // check for default environments
expect(payload.secretImport).toEqual( expect(deletedImport).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath: expect.any(String), importPath: expect.any(String),
@@ -175,5 +200,7 @@ describe("Secret Folder Router", async () => {
expect(secretImportList).toHaveProperty("secretImports"); expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports.length).toEqual(1); expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1); expect(secretImportList.secretImports[0].position).toEqual(1);
await deleteSecretImport(createdImport2.id);
}); });
}); });
@@ -0,0 +1,579 @@
import crypto from "node:crypto";
import { SecretType, TSecrets } from "@app/db/schemas";
import { decryptSecret, encryptSecret, getUserPrivateKey, seedData1 } from "@app/db/seed-data";
import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
const createServiceToken = async (
scopes: { environment: string; secretPath: string }[],
permissions: ("read" | "write")[]
) => {
const projectKeyRes = await testServer.inject({
method: "GET",
url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
const projectKeyEnc = JSON.parse(projectKeyRes.payload);
const userInfoRes = await testServer.inject({
method: "GET",
url: "/api/v2/users/me",
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
const { user: userInfo } = JSON.parse(userInfoRes.payload);
const privateKey = await getUserPrivateKey(seedData1.password, userInfo);
const projectKey = decryptAsymmetric({
ciphertext: projectKeyEnc.encryptedKey,
nonce: projectKeyEnc.nonce,
publicKey: projectKeyEnc.sender.publicKey,
privateKey
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(projectKey, randomBytes);
const serviceTokenRes = await testServer.inject({
method: "POST",
url: "/api/v2/service-token",
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: "test-token",
workspaceId: seedData1.project.id,
scopes,
encryptedKey: ciphertext,
iv,
tag,
permissions,
expiresIn: null
}
});
expect(serviceTokenRes.statusCode).toBe(200);
const serviceTokenInfo = serviceTokenRes.json();
expect(serviceTokenInfo).toHaveProperty("serviceToken");
expect(serviceTokenInfo).toHaveProperty("serviceTokenData");
return `${serviceTokenInfo.serviceToken}.${randomBytes}`;
};
const deleteServiceToken = async () => {
const serviceTokenListRes = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(serviceTokenListRes.statusCode).toBe(200);
const serviceTokens = JSON.parse(serviceTokenListRes.payload).serviceTokenData as { name: string; id: string }[];
expect(serviceTokens.length).toBeGreaterThan(0);
const serviceTokenInfo = serviceTokens.find(({ name }) => name === "test-token");
expect(serviceTokenInfo).toBeDefined();
const deleteTokenRes = await testServer.inject({
method: "DELETE",
url: `/api/v2/service-token/${serviceTokenInfo?.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(deleteTokenRes.statusCode).toBe(200);
};
const createSecret = async (dto: {
projectKey: string;
path: string;
key: string;
value: string;
comment: string;
type?: SecretType;
token: string;
}) => {
const createSecretReqBody = {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: dto.type || SecretType.Shared,
secretPath: dto.path,
...encryptSecret(dto.projectKey, dto.key, dto.value, dto.comment)
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${dto.token}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret;
};
const deleteSecret = async (dto: { path: string; key: string; token: string }) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${dto.token}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: dto.path
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret;
};
describe("Service token secret ops", async () => {
let serviceToken = "";
let projectKey = "";
let folderId = "";
beforeAll(async () => {
serviceToken = await createServiceToken(
[{ secretPath: "/**", environment: seedData1.environment.slug }],
["read", "write"]
);
// this is ensure cli service token decryptiong working fine
const serviceTokenInfoRes = await testServer.inject({
method: "GET",
url: "/api/v2/service-token",
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(serviceTokenInfoRes.statusCode).toBe(200);
const serviceTokenInfo = serviceTokenInfoRes.json();
const serviceTokenParts = serviceToken.split(".");
projectKey = decryptSymmetric128BitHexKeyUTF8({
key: serviceTokenParts[3],
tag: serviceTokenInfo.tag,
ciphertext: serviceTokenInfo.encryptedKey,
iv: serviceTokenInfo.iv
});
// create a deep folder
const folderCreate = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder",
path: "/nested1/nested2"
}
});
expect(folderCreate.statusCode).toBe(200);
folderId = folderCreate.json().folder.id;
});
afterAll(async () => {
await deleteServiceToken();
// create a deep folder
const deleteFolder = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${folderId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/nested1/nested2"
}
});
expect(deleteFolder.statusCode).toBe(200);
});
const testSecrets = [
{
path: "/",
secret: {
key: "ST-SEC",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "NESTED-ST-SEC",
value: "something-secret",
comment: "some comment"
}
}
];
const getSecrets = async (environment: string, secretPath = "/") => {
const res = await testServer.inject({
method: "GET",
url: `/api/v3/secrets`,
headers: {
authorization: `Bearer ${serviceToken}`
},
query: {
secretPath,
environment,
workspaceId: seedData1.project.id
}
});
const secrets: TSecrets[] = JSON.parse(res.payload).secrets || [];
return secrets.map((el) => ({ ...decryptSecret(projectKey, el), type: el.type }));
};
test.each(testSecrets)("Create secret in path $path", async ({ secret, path }) => {
const createdSecret = await createSecret({ projectKey, path, ...secret, token: serviceToken });
const decryptedSecret = decryptSecret(projectKey, createdSecret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual(secret.value);
expect(decryptedSecret.comment).toEqual(secret.comment);
expect(decryptedSecret.version).toEqual(1);
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: secret.value,
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Get secret by name in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const getSecByNameRes = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${serviceToken}`
},
query: {
secretPath: path,
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug
}
});
expect(getSecByNameRes.statusCode).toBe(200);
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
expect(getSecretByNamePayload).toHaveProperty("secret");
const decryptedSecret = decryptSecret(projectKey, getSecretByNamePayload.secret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual(secret.value);
expect(decryptedSecret.comment).toEqual(secret.comment);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Update secret in path $path", async ({ path, secret }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const updateSecretReqBody = {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: path,
...encryptSecret(projectKey, secret.key, "new-value", secret.comment)
};
const updateSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: updateSecretReqBody
});
expect(updateSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
const decryptedSecret = decryptSecret(projectKey, updatedSecretPayload.secret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual("new-value");
expect(decryptedSecret.comment).toEqual(secret.comment);
// list secret should have updated value
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: "new-value",
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Delete secret in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const deletedSecret = await deleteSecret({ path, key: secret.key, token: serviceToken });
const decryptedSecret = decryptSecret(projectKey, deletedSecret);
expect(decryptedSecret.key).toEqual(secret.key);
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
key: secret.key,
type: SecretType.Shared
})
])
);
});
test.each(testSecrets)("Bulk create secrets in path $path", async ({ secret, path }) => {
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
}))
}
});
expect(createSharedSecRes.statusCode).toBe(200);
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
expect(createSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.key}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
deleteSecret({ path, token: serviceToken, key: `BULK-${secret.key}-${i + 1}` })
)
);
});
test.each(testSecrets)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-1`, path, token: serviceToken });
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
}))
}
});
expect(createSharedSecRes.statusCode).toBe(400);
await deleteSecret({ path, key: `BULK-${secret.key}-1`, token: serviceToken });
});
test.each(testSecrets)("Bulk update secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
)
);
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, "update-value", secret.comment)
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.key}-${i + 1}`,
value: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}`, token: serviceToken })
)
);
});
test.each(testSecrets)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
)
);
const deletedSharedSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`
}))
}
});
expect(deletedSharedSecRes.statusCode).toBe(200);
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
expect(deletedSecretPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.value}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
});
});
describe("Service token fail cases", async () => {
test("Unauthorized secret path access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: "/nested/deep"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(401);
expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken();
});
test("Unauthorized secret environment access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: "prod",
secretPath: "/"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(401);
expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken();
});
test("Unauthorized write operation", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read"]
);
const writeSecrets = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/NEW`,
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: "/",
// doesn't matter project key because this will fail before that due to read only access
...encryptSecret(crypto.randomBytes(16).toString("hex"), "NEW", "value", "")
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(writeSecrets.statusCode).toBe(401);
expect(writeSecrets.json().error).toBe("PermissionDenied");
// but read access should still work fine
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: "/"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(200);
await deleteServiceToken();
});
});
File diff suppressed because it is too large Load Diff
+15 -13
View File
@@ -1,26 +1,30 @@
// import { main } from "@app/server/app"; // eslint-disable-next-line
import { initEnvConfig } from "@app/lib/config/env"; import "ts-node/register";
import dotenv from "dotenv"; import dotenv from "dotenv";
import jwt from "jsonwebtoken";
import knex from "knex"; import knex from "knex";
import path from "path"; import path from "path";
import { mockSmtpServer } from "./mocks/smtp";
import { initLogger } from "@app/lib/logger";
import jwt from "jsonwebtoken";
import "ts-node/register";
import { main } from "@app/server/app";
import { mockQueue } from "./mocks/queue";
import { AuthTokenType } from "@app/services/auth/auth-type";
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
import { AuthTokenType } from "@app/services/auth/auth-type";
dotenv.config({ path: path.join(__dirname, "../.env.test") }); import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default { export default {
name: "knex-env", name: "knex-env",
transformMode: "ssr", transformMode: "ssr",
async setup() { async setup() {
const logger = await initLogger();
const cfg = initEnvConfig(logger);
const db = knex({ const db = knex({
client: "pg", client: "pg",
connection: process.env.DB_CONNECTION_URI, connection: cfg.DB_CONNECTION_URI,
migrations: { migrations: {
directory: path.join(__dirname, "../src/db/migrations"), directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts", extension: "ts",
@@ -37,8 +41,6 @@ export default {
await db.seed.run(); await db.seed.run();
const smtp = mockSmtpServer(); const smtp = mockSmtpServer();
const queue = mockQueue(); const queue = mockQueue();
const logger = await initLogger();
const cfg = initEnvConfig(logger);
const server = await main({ db, smtp, logger, queue }); const server = await main({ db, smtp, logger, queue });
// @ts-expect-error type // @ts-expect-error type
globalThis.testServer = server; globalThis.testServer = server;
+269 -59
View File
@@ -29,7 +29,7 @@
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1532.0", "aws-sdk": "^2.1545.0",
"axios": "^1.6.4", "axios": "^1.6.4",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
@@ -102,7 +102,7 @@
"tsx": "^4.4.0", "tsx": "^4.4.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2", "vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.0.4" "vitest": "^1.2.2"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@@ -3881,6 +3881,12 @@
"@types/ms": "*" "@types/ms": "*"
} }
}, },
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/express": { "node_modules/@types/express": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@@ -4543,13 +4549,13 @@
"dev": true "dev": true
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
"integrity": "sha512-/NRN9N88qjg3dkhmFcCBwhn/Ie4h064pY3iv7WLRsDJW7dXnEgeoa8W9zy7gIPluhz6CkgqiB3HmpIXgmEY5dQ==", "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/spy": "1.0.4", "@vitest/spy": "1.2.2",
"@vitest/utils": "1.0.4", "@vitest/utils": "1.2.2",
"chai": "^4.3.10" "chai": "^4.3.10"
}, },
"funding": { "funding": {
@@ -4557,12 +4563,12 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
"integrity": "sha512-rhOQ9FZTEkV41JWXozFM8YgOqaG9zA7QXbhg5gy6mFOVqh4PcupirIJ+wN7QjeJt8S8nJRYuZH1OjJjsbxAXTQ==", "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/utils": "1.0.4", "@vitest/utils": "1.2.2",
"p-limit": "^5.0.0", "p-limit": "^5.0.0",
"pathe": "^1.1.1" "pathe": "^1.1.1"
}, },
@@ -4598,9 +4604,9 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
"integrity": "sha512-vkfXUrNyNRA/Gzsp2lpyJxh94vU2OHT1amoD6WuvUAA12n32xeVZQ0KjjQIf8F6u7bcq2A2k969fMVxEsxeKYA==", "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
@@ -4612,9 +4618,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
"integrity": "sha512-9ojTFRL1AJVh0hvfzAQpm0QS6xIS+1HFIw94kl/1ucTfGCaj1LV/iuJU4Y6cdR03EzPDygxTHwE1JOm+5RCcvA==", "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"tinyspy": "^2.2.0" "tinyspy": "^2.2.0"
@@ -4624,12 +4630,13 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
"integrity": "sha512-gsswWDXxtt0QvtK/y/LWukN7sGMYmnCcv1qv05CsY6cU/Y1zpGX1QuvLs+GO1inczpE6Owixeel3ShkjhYtGfA==", "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"diff-sequences": "^29.6.3", "diff-sequences": "^29.6.3",
"estree-walker": "^3.0.3",
"loupe": "^2.3.7", "loupe": "^2.3.7",
"pretty-format": "^29.7.0" "pretty-format": "^29.7.0"
}, },
@@ -4700,9 +4707,9 @@
} }
}, },
"node_modules/acorn-walk": { "node_modules/acorn-walk": {
"version": "8.3.0", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -4787,7 +4794,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -5152,9 +5158,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/aws-sdk": { "node_modules/aws-sdk": {
"version": "2.1532.0", "version": "2.1545.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1532.0.tgz", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1545.0.tgz",
"integrity": "sha512-4QVQs01LEAxo7UpSHlq/HaO+SJ1WrYF8W1otO2WhKpVRYXkSxXIgZgfYaK+sQ762XTtB6tSuD2ZS2HGsKNXVLw==", "integrity": "sha512-iDUv6ksG7lTA0l/HlOgYdO6vfYFA1D2/JzAEXSdgKY0C901WgJqBtfs2CncOkCgDe2CjmlMuqciBzAfxCIiKFA==",
"dependencies": { "dependencies": {
"buffer": "4.9.2", "buffer": "4.9.2",
"events": "1.1.1", "events": "1.1.1",
@@ -5165,7 +5171,7 @@
"url": "0.10.3", "url": "0.10.3",
"util": "^0.12.4", "util": "^0.12.4",
"uuid": "8.0.0", "uuid": "8.0.0",
"xml2js": "0.5.0" "xml2js": "0.6.2"
}, },
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
@@ -5212,6 +5218,26 @@
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/aws-sdk/node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/aws-sdk/node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.6.4", "version": "1.6.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz",
@@ -5292,6 +5318,29 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bl": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
"integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
"dependencies": {
"buffer": "^6.0.3",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@@ -5490,9 +5539,9 @@
} }
}, },
"node_modules/chai": { "node_modules/chai": {
"version": "4.3.10", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"assertion-error": "^1.1.0", "assertion-error": "^1.1.0",
@@ -5507,6 +5556,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/check-error": { "node_modules/check-error": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -5571,6 +5631,31 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cli-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
"dependencies": {
"restore-cursor": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clone": { "node_modules/clone": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -5936,8 +6021,7 @@
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
"dev": true
}, },
"node_modules/ecdsa-sig-formatter": { "node_modules/ecdsa-sig-formatter": {
"version": "1.0.11", "version": "1.0.11",
@@ -5952,6 +6036,11 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
}, },
"node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -6646,6 +6735,15 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": { "node_modules/esutils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -8006,6 +8104,17 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-interactive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-negative-zero": { "node_modules/is-negative-zero": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -8151,6 +8260,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-unicode-supported": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-weakref": { "node_modules/is-weakref": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -8642,6 +8762,21 @@
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"dev": true "dev": true
}, },
"node_modules/log-symbols": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
"integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
"dependencies": {
"chalk": "^5.0.0",
"is-unicode-supported": "^1.1.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long": { "node_modules/long": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
@@ -8681,9 +8816,9 @@
} }
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.5", "version": "0.30.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -8825,7 +8960,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@@ -9396,7 +9530,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"dependencies": { "dependencies": {
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
}, },
@@ -9429,6 +9562,28 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/ora": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz",
"integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==",
"dependencies": {
"chalk": "^5.3.0",
"cli-cursor": "^4.0.0",
"cli-spinners": "^2.9.0",
"is-interactive": "^2.0.0",
"is-unicode-supported": "^1.3.0",
"log-symbols": "^5.1.0",
"stdin-discarder": "^0.1.0",
"string-width": "^6.1.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -9458,6 +9613,17 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-throttle": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.1.0.tgz",
"integrity": "sha512-+N+s2g01w1Zch4D0K3OpnPDqLOKmLcQ4BvIFq3JC0K29R28vUOjWpO+OJZBNt8X9i3pFCksZJZ0YXkUGjaFE6g==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": { "node_modules/p-try": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -10549,6 +10715,21 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
} }
}, },
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ret": { "node_modules/ret": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz",
@@ -11071,6 +11252,20 @@
"integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==",
"dev": true "dev": true
}, },
"node_modules/stdin-discarder": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
"integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
"dependencies": {
"bl": "^5.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stream-shift": { "node_modules/stream-shift": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
@@ -11084,6 +11279,22 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/string-width": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz",
"integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^10.2.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": { "node_modules/string-width-cjs": {
"name": "string-width", "name": "string-width",
"version": "4.2.3", "version": "4.2.3",
@@ -11175,7 +11386,6 @@
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
}, },
@@ -11428,18 +11638,18 @@
"dev": true "dev": true
}, },
"node_modules/tinypool": { "node_modules/tinypool": {
"version": "0.8.1", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
"integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tinyspy": { "node_modules/tinyspy": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
"integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -12572,9 +12782,9 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.0.4.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
"integrity": "sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg==", "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"cac": "^6.7.14", "cac": "^6.7.14",
@@ -13048,17 +13258,17 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "1.0.4", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.0.4.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
"integrity": "sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg==", "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/expect": "1.0.4", "@vitest/expect": "1.2.2",
"@vitest/runner": "1.0.4", "@vitest/runner": "1.2.2",
"@vitest/snapshot": "1.0.4", "@vitest/snapshot": "1.2.2",
"@vitest/spy": "1.0.4", "@vitest/spy": "1.2.2",
"@vitest/utils": "1.0.4", "@vitest/utils": "1.2.2",
"acorn-walk": "^8.3.0", "acorn-walk": "^8.3.2",
"cac": "^6.7.14", "cac": "^6.7.14",
"chai": "^4.3.10", "chai": "^4.3.10",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -13070,9 +13280,9 @@
"std-env": "^3.5.0", "std-env": "^3.5.0",
"strip-literal": "^1.3.0", "strip-literal": "^1.3.0",
"tinybench": "^2.5.1", "tinybench": "^2.5.1",
"tinypool": "^0.8.1", "tinypool": "^0.8.2",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-node": "1.0.4", "vite-node": "1.2.2",
"why-is-node-running": "^2.2.2" "why-is-node-running": "^2.2.2"
}, },
"bin": { "bin": {
+4 -4
View File
@@ -24,8 +24,8 @@
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts", "seed:new": "tsx ./scripts/create-seed-file.ts",
"seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run" "db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -67,7 +67,7 @@
"tsx": "^4.4.0", "tsx": "^4.4.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2", "vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.0.4" "vitest": "^1.2.2"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.485.0", "@aws-sdk/client-secrets-manager": "^3.485.0",
@@ -90,7 +90,7 @@
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1532.0", "aws-sdk": "^2.1545.0",
"axios": "^1.6.4", "axios": "^1.6.4",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
+4 -5
View File
@@ -7,11 +7,10 @@ import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true }); const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for seedfile: "); const migrationName = prompt("Enter name for seedfile: ");
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1; const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1;
execSync( execSync(
`npx knex seed:make --knexfile ${path.join( `npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${
__dirname, fileCounter + 1
"../src/db/knexfile.ts" }-${migrationName}`,
)} -x ts ${fileCounter}-${migrationName}`,
{ stdio: "inherit" } { stdio: "inherit" }
); );
+4
View File
@@ -10,6 +10,10 @@ dotenv.config({
path: path.join(__dirname, "../../../.env.migration"), path: path.join(__dirname, "../../../.env.migration"),
debug: true debug: true
}); });
dotenv.config({
path: path.join(__dirname, "../../../.env"),
debug: true
});
export default { export default {
development: { development: {
client: "postgres", client: "postgres",
+97 -18
View File
@@ -6,13 +6,14 @@ import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import { import {
decryptAsymmetric,
// decryptAsymmetric, // decryptAsymmetric,
decryptSymmetric, decryptSymmetric128BitHexKeyUTF8,
encryptAsymmetric, encryptAsymmetric,
encryptSymmetric encryptSymmetric128BitHexKeyUTF8
} from "@app/lib/crypto"; } from "@app/lib/crypto";
import { TUserEncryptionKeys } from "./schemas"; import { TSecrets, TUserEncryptionKeys } from "./schemas";
export const seedData1 = { export const seedData1 = {
id: "3dafd81d-4388-432b-a4c5-f735616868c1", id: "3dafd81d-4388-432b-a4c5-f735616868c1",
@@ -31,6 +32,14 @@ export const seedData1 = {
name: "Development", name: "Development",
slug: "dev" slug: "dev"
}, },
machineIdentity: {
id: "88fa7aed-9288-401e-a4c9-fa9430be62a0",
name: "mac1",
clientCredentials: {
id: "3f6135db-f237-421d-af66-a8f4e80d443b",
secret: "da35a5a5a7b57f977a9a73394506e878a7175d06606df43dc93e1472b10cf339"
}
},
token: { token: {
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36" id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
} }
@@ -73,7 +82,7 @@ export const generateUserSrpKeys = async (password: string) => {
ciphertext: encryptedPrivateKey, ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag tag: encryptedPrivateKeyTag
} = encryptSymmetric(privateKey, key.toString("base64")); } = encryptSymmetric128BitHexKeyUTF8(privateKey, key);
// create the protected key by encrypting the symmetric key // create the protected key by encrypting the symmetric key
// [key] with the derived key // [key] with the derived key
@@ -81,7 +90,7 @@ export const generateUserSrpKeys = async (password: string) => {
ciphertext: protectedKey, ciphertext: protectedKey,
iv: protectedKeyIV, iv: protectedKeyIV,
tag: protectedKeyTag tag: protectedKeyTag
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64")); } = encryptSymmetric128BitHexKeyUTF8(key.toString("hex"), derivedKey);
return { return {
protectedKey, protectedKey,
@@ -107,32 +116,102 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
raw: true raw: true
}); });
if (!derivedKey) throw new Error("Failed to derive key from password"); if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.protectedKey as string, ciphertext: user.protectedKey as string,
iv: user.protectedKeyIV as string, iv: user.protectedKeyIV as string,
tag: user.protectedKeyTag as string, tag: user.protectedKeyTag as string,
key: derivedKey.toString("base64") key: derivedKey
}); });
const privateKey = decryptSymmetric({
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey, ciphertext: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag, tag: user.tag,
key key: Buffer.from(key, "hex")
}); });
return privateKey; return privateKey;
}; };
export const buildUserProjectKey = async (privateKey: string, publickey: string) => { export const buildUserProjectKey = (privateKey: string, publickey: string) => {
const randomBytes = crypto.randomBytes(16).toString("hex"); const randomBytes = crypto.randomBytes(16).toString("hex");
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey); const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
return { nonce, ciphertext }; return { nonce, ciphertext };
}; };
// export const getUserProjectKey = async (privateKey: string) => { export const getUserProjectKey = async (privateKey: string, ciphertext: string, nonce: string, publicKey: string) => {
// const key = decryptAsymmetric({ return decryptAsymmetric({
// ciphertext: decryptFileKey.encryptedKey, ciphertext,
// nonce: decryptFileKey.nonce, nonce,
// publicKey: decryptFileKey.sender.publicKey, publicKey,
// privateKey: PRIVATE_KEY privateKey
// }); });
// }; };
export const encryptSecret = (encKey: string, key: string, value?: string, comment?: string) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric128BitHexKeyUTF8(key, encKey);
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric128BitHexKeyUTF8(value ?? "", encKey);
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric128BitHexKeyUTF8(comment ?? "", encKey);
return {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
};
};
export const decryptSecret = (decryptKey: string, encSecret: TSecrets) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
key: decryptKey,
ciphertext: encSecret.secretKeyCiphertext,
tag: encSecret.secretKeyTag,
iv: encSecret.secretKeyIV
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
key: decryptKey,
ciphertext: encSecret.secretValueCiphertext,
tag: encSecret.secretValueTag,
iv: encSecret.secretValueIV
});
const secretComment =
encSecret.secretCommentIV && encSecret.secretCommentTag && encSecret.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
key: decryptKey,
ciphertext: encSecret.secretCommentCiphertext,
tag: encSecret.secretCommentTag,
iv: encSecret.secretCommentIV
})
: "";
return {
key: secretKey,
value: secretValue,
comment: secretComment,
version: encSecret.version
};
};
+4 -2
View File
@@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
{ {
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.id, id: seedData1.id,
email: seedData1.email, email: seedData1.email,
superAdmin: true, superAdmin: true,
@@ -48,7 +49,8 @@ export async function seed(knex: Knex): Promise<void> {
]); ]);
await knex(TableName.AuthTokenSession).insert({ await knex(TableName.AuthTokenSession).insert({
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.token.id, id: seedData1.token.id,
userId: seedData1.id, userId: seedData1.id,
ip: "151.196.220.213", ip: "151.196.220.213",
+2 -1
View File
@@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
const [org] = await knex(TableName.Organization) const [org] = await knex(TableName.Organization)
.insert([ .insert([
{ {
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.organization.id, id: seedData1.organization.id,
name: "infisical", name: "infisical",
slug: "infisical", slug: "infisical",
+38 -8
View File
@@ -1,7 +1,11 @@
import crypto from "node:crypto";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, TableName } from "../schemas"; import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { seedData1 } from "../seed-data";
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" }, { name: "Development", slug: "dev" },
@@ -20,21 +24,32 @@ export async function seed(knex: Knex): Promise<void> {
name: seedData1.project.name, name: seedData1.project.name,
orgId: seedData1.organization.id, orgId: seedData1.organization.id,
slug: "first-project", slug: "first-project",
// @ts-expect-error exluded type id needs to be inserted here to keep it testable // eslint-disable-next-line
// @ts-ignore
id: seedData1.project.id id: seedData1.project.id
}) })
.returning("*"); .returning("*");
// await knex(TableName.ProjectKeys).insert({
// projectId: project.id,
// senderId: seedData1.id
// });
await knex(TableName.ProjectMembership).insert({ await knex(TableName.ProjectMembership).insert({
projectId: project.id, projectId: project.id,
role: OrgMembershipRole.Admin, role: OrgMembershipRole.Admin,
userId: seedData1.id userId: seedData1.id
}); });
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found");
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
nonce: projectKey.nonce,
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
// create default environments and default folders
const envs = await knex(TableName.Environment) const envs = await knex(TableName.Environment)
.insert( .insert(
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({ DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
@@ -46,4 +61,19 @@ export async function seed(knex: Knex): Promise<void> {
) )
.returning("*"); .returning("*");
await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null }))); await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null })));
// save secret secret blind index
const encKey = process.env.ENCRYPTION_KEY;
if (!encKey) throw new Error("Missing ENCRYPTION_KEY");
const salt = crypto.randomBytes(16).toString("base64");
const secretBlindIndex = encryptSymmetric128BitHexKeyUTF8(salt, encKey);
// insert secret blind index for project
await knex(TableName.SecretBlindIndex).insert({
projectId: project.id,
encryptedSaltCipherText: secretBlindIndex.ciphertext,
saltIV: secretBlindIndex.iv,
saltTag: secretBlindIndex.tag,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
});
} }
@@ -0,0 +1,83 @@
import bcrypt from "bcrypt";
import { Knex } from "knex";
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del();
// Inserts seed entries
await knex(TableName.Identity).insert([
{
// eslint-disable-next-line
// @ts-ignore
id: seedData1.machineIdentity.id,
name: seedData1.machineIdentity.name,
authMethod: IdentityAuthMethod.Univeral
}
]);
const identityUa = await knex(TableName.IdentityUniversalAuth)
.insert([
{
identityId: seedData1.machineIdentity.id,
clientId: seedData1.machineIdentity.clientCredentials.id,
clientSecretTrustedIps: JSON.stringify([
{
type: "ipv4",
prefix: 0,
ipAddress: "0.0.0.0"
},
{
type: "ipv6",
prefix: 0,
ipAddress: "::"
}
]),
accessTokenTrustedIps: JSON.stringify([
{
type: "ipv4",
prefix: 0,
ipAddress: "0.0.0.0"
},
{
type: "ipv6",
prefix: 0,
ipAddress: "::"
}
]),
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0
}
])
.returning("*");
const clientSecretHash = await bcrypt.hash(seedData1.machineIdentity.clientCredentials.secret, 10);
await knex(TableName.IdentityUaClientSecret).insert([
{
identityUAId: identityUa[0].id,
description: "",
clientSecretTTL: 0,
clientSecretNumUses: 0,
clientSecretNumUsesLimit: 0,
clientSecretPrefix: seedData1.machineIdentity.clientCredentials.secret.slice(0, 4),
clientSecretHash,
isClientSecretRevoked: false
}
]);
await knex(TableName.IdentityOrgMembership).insert([
{
identityId: seedData1.machineIdentity.id,
orgId: seedData1.organization.id,
role: OrgMembershipRole.Admin
}
]);
await knex(TableName.IdentityProjectMembership).insert({
identityId: seedData1.machineIdentity.id,
role: ProjectMembershipRole.Admin,
projectId: seedData1.project.id
});
}
@@ -0,0 +1,27 @@
export const getDefaultOnPremFeatures = () => {
return {
_id: null,
slug: null,
tier: -1,
workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: false,
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretRotation: true
};
};
+7 -2
View File
@@ -44,7 +44,7 @@ export const encryptSymmetric = (plaintext: string, key: string) => {
}; };
}; };
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => { export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string | Buffer) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16); const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv); const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
@@ -58,7 +58,12 @@ export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string)
}; };
}; };
export const decryptSymmetric128BitHexKeyUTF8 = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => { export const decryptSymmetric128BitHexKeyUTF8 = ({
ciphertext,
iv,
tag,
key
}: Omit<TDecryptSymmetricInput, "key"> & { key: string | Buffer }): string => {
const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64")); const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64"));
decipher.setAuthTag(Buffer.from(tag, "base64")); decipher.setAuthTag(Buffer.from(tag, "base64"));
+1 -1
View File
@@ -37,7 +37,7 @@ type TMain = {
export const main = async ({ db, smtp, logger, queue }: TMain) => { export const main = async ({ db, smtp, logger, queue }: TMain) => {
const appCfg = getConfig(); const appCfg = getConfig();
const server = fasitfy({ const server = fasitfy({
logger, logger: appCfg.NODE_ENV === "test" ? false : logger,
trustProxy: true, trustProxy: true,
connectionTimeout: 30 * 1000, connectionTimeout: 30 * 1000,
ignoreTrailingSlash: true ignoreTrailingSlash: true
+4 -2
View File
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
@@ -72,6 +72,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
user: UsersSchema, user: UsersSchema,
organization: OrganizationsSchema,
token: z.string(), token: z.string(),
new: z.string() new: z.string()
}) })
@@ -82,7 +83,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.initialized) if (serverCfg.initialized)
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" }); throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
const { user, token } = await server.services.superAdmin.adminSignUp({ const { user, token, organization } = await server.services.superAdmin.adminSignUp({
...req.body, ...req.body,
ip: req.realIp, ip: req.realIp,
userAgent: req.headers["user-agent"] || "" userAgent: req.headers["user-agent"] || ""
@@ -109,6 +110,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
message: "Successfully set up admin account", message: "Successfully set up admin account",
user: user.user, user: user.user,
token: token.access, token: token.access,
organization,
new: "123" new: "123"
}; };
} }
@@ -9,7 +9,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/", url: "/",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Create identity", description: "Create identity",
security: [ security: [
@@ -56,7 +56,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:identityId", url: "/:identityId",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Update identity", description: "Update identity",
security: [ security: [
@@ -105,7 +105,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:identityId", url: "/:identityId",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Delete identity", description: "Delete identity",
security: [ security: [
+1 -1
View File
@@ -197,7 +197,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => { handler: async (req) => {
const user = await server.services.user.getMe(req.permission.id); const user = await server.services.user.getMe(req.permission.id);
return { user }; return { user };
@@ -1092,7 +1092,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: z secrets: z
.object({ .object({
secretName: z.string().trim(), secretName: z.string().trim(),
type: z.nativeEnum(SecretType).default(SecretType.Shared),
secretKeyCiphertext: z.string().trim(), secretKeyCiphertext: z.string().trim(),
secretKeyIV: z.string().trim(), secretKeyIV: z.string().trim(),
secretKeyTag: z.string().trim(), secretKeyTag: z.string().trim(),
@@ -1139,7 +1138,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId, projectId,
policy, policy,
data: { data: {
[CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared") [CommitType.Create]: inputSecrets
} }
}); });
@@ -41,13 +41,12 @@ export const fnSecretsFromImports = async ({
environment: importEnv.slug, environment: importEnv.slug,
environmentInfo: importEnv, environmentInfo: importEnv,
folderId: importedFolders?.[i]?.id, folderId: importedFolders?.[i]?.id,
secrets: importedFolders?.[i]?.id // this will ensure for cases when secrets are empty. Could be due to missing folder for a path or when emtpy secrets inside a given path
? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string].map((item) => ({ secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({
...item, ...item,
environment: importEnv.slug, environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
})) }))
: []
})); }));
}; };
@@ -57,6 +57,12 @@ export const secretDALFactory = (db: TDbClient) => {
type: el.type, type: el.type,
...(el.type === SecretType.Personal ? { userId } : {}) ...(el.type === SecretType.Personal ? { userId } : {})
}); });
if (el.type === SecretType.Shared) {
void bd.orWhere({
secretBlindIndex: el.blindIndex,
type: SecretType.Personal
});
}
}); });
}) })
.delete() .delete()
@@ -96,7 +96,11 @@ export const superAdminServiceFactory = ({
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org"; const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName); const organization = await orgService.createOrganization(
userInfo.user.id,
userInfo.user.email,
initialOrganizationName
);
await updateServerCfg({ initialized: true }); await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens({ const token = await authService.generateUserTokens({
@@ -106,7 +110,7 @@ export const superAdminServiceFactory = ({
organizationId: undefined organizationId: undefined
}); });
// TODO(akhilmhdh-pg): telemetry service // TODO(akhilmhdh-pg): telemetry service
return { token, user: userInfo }; return { token, user: userInfo, organization };
}; };
return { return {
+5 -1
View File
@@ -4,12 +4,16 @@ import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
test: { test: {
globals: true, globals: true,
env: {
NODE_ENV: "test"
},
environment: "./e2e-test/vitest-environment-knex.ts", environment: "./e2e-test/vitest-environment-knex.ts",
include: ["./e2e-test/**/*.spec.ts"], include: ["./e2e-test/**/*.spec.ts"],
poolOptions: { poolOptions: {
threads: { threads: {
singleThread: true, singleThread: true,
useAtomics: true useAtomics: true,
isolate: false
} }
} }
}, },
+1 -1
View File
@@ -34,7 +34,7 @@ In addition to scanning for past leaks, this new addition also actively aids in
infisical scan git-changes infisical scan git-changes
# Display the full secret findings # Display the full secret findings
infisical git-changes --verbose infisical scan git-changes --verbose
``` ```
Scanning for secrets before you commit your changes is great way to prevent leaks. Infisical makes this easy with the sub command `git-changes`. Scanning for secrets before you commit your changes is great way to prevent leaks. Infisical makes this easy with the sub command `git-changes`.
+5 -5
View File
@@ -94,7 +94,7 @@
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.3", "zod": "^3.22.3",
"zustand": "^4.4.1" "zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.5.2", "@storybook/addon-essentials": "^7.5.2",
@@ -23536,9 +23536,9 @@
} }
}, },
"node_modules/zustand": { "node_modules/zustand": {
"version": "4.4.7", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz",
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==",
"dependencies": { "dependencies": {
"use-sync-external-store": "1.2.0" "use-sync-external-store": "1.2.0"
}, },
@@ -23547,7 +23547,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": ">=16.8", "@types/react": ">=16.8",
"immer": ">=9.0", "immer": ">=9.0.6",
"react": ">=16.8" "react": ">=16.8"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
+1 -1
View File
@@ -102,7 +102,7 @@
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.3", "zod": "^3.22.3",
"zustand": "^4.4.1" "zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.5.2", "@storybook/addon-essentials": "^7.5.2",
+7 -1
View File
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries";
import { User } from "../users/types"; import { User } from "../users/types";
import { adminQueryKeys } from "./queries"; import { adminQueryKeys } from "./queries";
import { TCreateAdminUserDTO, TServerConfig } from "./types"; import { TCreateAdminUserDTO, TServerConfig } from "./types";
@@ -9,7 +10,11 @@ import { TCreateAdminUserDTO, TServerConfig } from "./types";
export const useCreateAdminUser = () => { export const useCreateAdminUser = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{ user: User; token: string }, {}, TCreateAdminUserDTO>({ return useMutation<
{ user: User; token: string; organization: { id: string } },
{},
TCreateAdminUserDTO
>({
mutationFn: async (opt) => { mutationFn: async (opt) => {
const { data } = await apiRequest.post("/api/v1/admin/signup", opt); const { data } = await apiRequest.post("/api/v1/admin/signup", opt);
return data; return data;
@@ -34,6 +39,7 @@ export const useUpdateServerConfig = () => {
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(adminQueryKeys.serverConfig(), data); queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
queryClient.invalidateQueries(adminQueryKeys.serverConfig()); queryClient.invalidateQueries(adminQueryKeys.serverConfig());
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
} }
}); });
}; };
@@ -84,6 +84,10 @@ export const SignUpPage = () => {
tag: userPass.encryptedPrivateKeyTag, tag: userPass.encryptedPrivateKeyTag,
privateKey privateKey
}); });
// TODO(akhilmhdh): This is such a confusing pattern and too unreliable
// Will be refactored in next iteration to make it url based rather than local storage ones
// Part of migration to nextjs 14
localStorage.setItem("orgData.id", res.organization.id);
setStep(SignupSteps.BackupKey); setStep(SignupSteps.BackupKey);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -130,8 +134,8 @@ export const SignUpPage = () => {
> >
<div className="flex flex-col items-center space-y-2 text-center"> <div className="flex flex-col items-center space-y-2 text-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" /> <img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
<div className="text-4xl pt-4">Welcome to Infisical</div> <div className="pt-4 text-4xl">Welcome to Infisical</div>
<div className="text-bunker-300 pb-4">Create your first Super Admin Account</div> <div className="pb-4 text-bunker-300">Create your first Super Admin Account</div>
</div> </div>
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="mt-8"> <div className="mt-8">
@@ -199,7 +203,14 @@ export const SignUpPage = () => {
)} )}
/> />
</div> </div>
<Button type="submit" colorSchema="primary" variant="outline_bg" isFullWidth className="mt-4" isLoading={isSubmitting}> <Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="mt-4"
isLoading={isSubmitting}
>
Continue Continue
</Button> </Button>
</form> </form>