mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
27 Commits
doc/add-gi
...
daniel/npm
Author | SHA1 | Date | |
---|---|---|---|
05f07b25ac | |||
60fb195706 | |||
c8109b4e84 | |||
1f2b0443cc | |||
dd1cabf9f6 | |||
8b781b925a | |||
ddcf5b576b | |||
7138b392f2 | |||
bfce1021fb | |||
93c0313b28 | |||
8cfc217519 | |||
d272c6217a | |||
2fe2ddd9fc | |||
e330ddd5ee | |||
7aba9c1a50 | |||
4cd8e0fa67 | |||
ea3d164ead | |||
df468e4865 | |||
66e96018c4 | |||
3b02eedca6 | |||
a55fe2b788 | |||
5d7a267f1d | |||
b16ab6f763 | |||
334a728259 | |||
4a3143e689 | |||
14810de054 | |||
8cfcbaa12c |
@ -10,8 +10,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
# packages: write
|
|
||||||
# issues: write
|
|
||||||
jobs:
|
jobs:
|
||||||
cli-integration-tests:
|
cli-integration-tests:
|
||||||
name: Run tests before deployment
|
name: Run tests before deployment
|
||||||
@ -26,6 +25,63 @@ jobs:
|
|||||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||||
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||||
|
|
||||||
|
npm-release:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
env:
|
||||||
|
working-directory: ./npm
|
||||||
|
needs:
|
||||||
|
- cli-integration-tests
|
||||||
|
- goreleaser
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
run: |
|
||||||
|
VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
|
||||||
|
echo "Version extracted: $VERSION"
|
||||||
|
echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Print version
|
||||||
|
run: echo ${{ env.CLI_VERSION }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: ./npm/package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
|
- name: Set NPM version
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
|
||||||
|
|
||||||
|
- name: Setup NPM
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: |
|
||||||
|
echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
|
||||||
|
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
|
||||||
|
|
||||||
|
echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
|
||||||
|
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Pack NPM
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm pack
|
||||||
|
|
||||||
|
- name: Publish NPM
|
||||||
|
working-directory: ${{ env.working-directory }}
|
||||||
|
run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [cli-integration-tests]
|
needs: [cli-integration-tests]
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -71,3 +71,5 @@ frontend-build
|
|||||||
cli/infisical-merge
|
cli/infisical-merge
|
||||||
cli/test/infisical-merge
|
cli/test/infisical-merge
|
||||||
/backend/binary
|
/backend/binary
|
||||||
|
|
||||||
|
/npm/bin
|
||||||
|
@ -840,4 +840,91 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/secrets-by-keys",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
keys: z.string().trim().transform(decodeURIComponent)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
color: true
|
||||||
|
})
|
||||||
|
.extend({ name: z.string() })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { secretPath, projectId, environment } = req.query;
|
||||||
|
|
||||||
|
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
|
||||||
|
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
|
||||||
|
|
||||||
|
const { secrets } = await server.services.secret.getSecretsRaw({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environment,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
keys
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
numberOfSecrets: secrets.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretPulled,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -361,6 +361,10 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
|
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters?.keys) {
|
||||||
|
void bd.whereIn(`${TableName.SecretV2}.key`, filters.keys);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
|
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
|
||||||
|
@ -518,7 +518,10 @@ export const expandSecretReferencesFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (referencedSecretValue) {
|
if (referencedSecretValue) {
|
||||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referencedSecretValue);
|
expandedValue = expandedValue.replaceAll(
|
||||||
|
interpolationSyntax,
|
||||||
|
() => referencedSecretValue // prevents special characters from triggering replacement patterns
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,9 +150,13 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (referredSecrets.length !== references.length)
|
if (
|
||||||
|
referredSecrets.length !==
|
||||||
|
new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`))
|
||||||
|
.size // only count unique references
|
||||||
|
)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Referenced secret not found. Found only ${diff(
|
message: `Referenced secret(s) not found: ${diff(
|
||||||
references.map((el) => el.secretKey),
|
references.map((el) => el.secretKey),
|
||||||
referredSecrets.map((el) => el.key)
|
referredSecrets.map((el) => el.key)
|
||||||
).join(",")}`
|
).join(",")}`
|
||||||
|
@ -33,6 +33,7 @@ export type TGetSecretsDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
keys?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretDTO = {
|
export type TGetASecretDTO = {
|
||||||
@ -294,6 +295,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
|||||||
search?: string;
|
search?: string;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
includeTagsInSearch?: boolean;
|
includeTagsInSearch?: boolean;
|
||||||
|
keys?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretsRawByFolderMappingsDTO = {
|
export type TGetSecretsRawByFolderMappingsDTO = {
|
||||||
|
@ -185,6 +185,7 @@ export type TGetSecretsRawDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
keys?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretRawDTO = {
|
export type TGetASecretRawDTO = {
|
||||||
|
@ -9,7 +9,7 @@ You can use it across various environments, whether it's local development, CI/C
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="MacOS">
|
<Tab title="MacOS">
|
||||||
Use [brew](https://brew.sh/) package manager
|
Use [brew](https://brew.sh/) package manager
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -21,9 +21,8 @@ You can use it across various environments, whether it's local development, CI/C
|
|||||||
```bash
|
```bash
|
||||||
brew update && brew upgrade infisical
|
brew update && brew upgrade infisical
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
</Tab>
|
<Tab title="Windows">
|
||||||
<Tab title="Windows">
|
|
||||||
Use [Scoop](https://scoop.sh/) package manager
|
Use [Scoop](https://scoop.sh/) package manager
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -40,7 +39,20 @@ You can use it across various environments, whether it's local development, CI/C
|
|||||||
scoop update infisical
|
scoop update infisical
|
||||||
```
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab title="NPM">
|
||||||
|
Use [NPM](https://www.npmjs.com/) package manager
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @infisical/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm update -g @infisical/cli
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
<Tab title="Alpine">
|
<Tab title="Alpine">
|
||||||
Install prerequisite
|
Install prerequisite
|
||||||
```bash
|
```bash
|
||||||
|
@ -11,7 +11,7 @@ import { SecretType } from "@app/hooks/api/types";
|
|||||||
import Button from "../basic/buttons/Button";
|
import Button from "../basic/buttons/Button";
|
||||||
import Error from "../basic/Error";
|
import Error from "../basic/Error";
|
||||||
import { createNotification } from "../notifications";
|
import { createNotification } from "../notifications";
|
||||||
import { parseDotEnv } from "../utilities/parseDotEnv";
|
import { parseDotEnv } from "../utilities/parseSecrets";
|
||||||
import guidGenerator from "../utilities/randomId";
|
import guidGenerator from "../utilities/randomId";
|
||||||
|
|
||||||
interface DropZoneProps {
|
interface DropZoneProps {
|
||||||
|
@ -6,7 +6,7 @@ const LINE =
|
|||||||
* @param {ArrayBuffer} src - source buffer
|
* @param {ArrayBuffer} src - source buffer
|
||||||
* @returns {String} text - text of buffer
|
* @returns {String} text - text of buffer
|
||||||
*/
|
*/
|
||||||
export function parseDotEnv(src: ArrayBuffer) {
|
export function parseDotEnv(src: ArrayBuffer | string) {
|
||||||
const object: {
|
const object: {
|
||||||
[key: string]: { value: string; comments: string[] };
|
[key: string]: { value: string; comments: string[] };
|
||||||
} = {};
|
} = {};
|
||||||
@ -65,3 +65,15 @@ export function parseDotEnv(src: ArrayBuffer) {
|
|||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const parseJson = (src: ArrayBuffer | string) => {
|
||||||
|
const file = src.toString();
|
||||||
|
const formatedData: Record<string, string> = JSON.parse(file);
|
||||||
|
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||||
|
Object.keys(formatedData).forEach((key) => {
|
||||||
|
if (typeof formatedData[key] === "string") {
|
||||||
|
env[key] = { value: formatedData[key], comments: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return env;
|
||||||
|
};
|
@ -83,6 +83,7 @@ export type FormControlProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
tooltipText?: ReactElement | string;
|
tooltipText?: ReactElement | string;
|
||||||
|
tooltipClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormControl = ({
|
export const FormControl = ({
|
||||||
@ -96,7 +97,8 @@ export const FormControl = ({
|
|||||||
isError,
|
isError,
|
||||||
icon,
|
icon,
|
||||||
className,
|
className,
|
||||||
tooltipText
|
tooltipText,
|
||||||
|
tooltipClassName
|
||||||
}: FormControlProps): JSX.Element => {
|
}: FormControlProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={twMerge("mb-4", className)}>
|
<div className={twMerge("mb-4", className)}>
|
||||||
@ -108,6 +110,7 @@ export const FormControl = ({
|
|||||||
id={id}
|
id={id}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
tooltipText={tooltipText}
|
tooltipText={tooltipText}
|
||||||
|
tooltipClassName={tooltipClassName}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
label
|
label
|
||||||
|
@ -5,6 +5,7 @@ import axios from "axios";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import {
|
import {
|
||||||
|
DashboardProjectSecretsByKeys,
|
||||||
DashboardProjectSecretsDetails,
|
DashboardProjectSecretsDetails,
|
||||||
DashboardProjectSecretsDetailsResponse,
|
DashboardProjectSecretsDetailsResponse,
|
||||||
DashboardProjectSecretsOverview,
|
DashboardProjectSecretsOverview,
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
DashboardSecretsOrderBy,
|
DashboardSecretsOrderBy,
|
||||||
TDashboardProjectSecretsQuickSearch,
|
TDashboardProjectSecretsQuickSearch,
|
||||||
TDashboardProjectSecretsQuickSearchResponse,
|
TDashboardProjectSecretsQuickSearchResponse,
|
||||||
|
TGetDashboardProjectSecretsByKeys,
|
||||||
TGetDashboardProjectSecretsDetailsDTO,
|
TGetDashboardProjectSecretsDetailsDTO,
|
||||||
TGetDashboardProjectSecretsOverviewDTO,
|
TGetDashboardProjectSecretsOverviewDTO,
|
||||||
TGetDashboardProjectSecretsQuickSearchDTO
|
TGetDashboardProjectSecretsQuickSearchDTO
|
||||||
@ -101,6 +103,23 @@ export const fetchProjectSecretsDetails = async ({
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchDashboardProjectSecretsByKeys = async ({
|
||||||
|
keys,
|
||||||
|
...params
|
||||||
|
}: TGetDashboardProjectSecretsByKeys) => {
|
||||||
|
const { data } = await apiRequest.get<DashboardProjectSecretsByKeys>(
|
||||||
|
"/api/v1/dashboard/secrets-by-keys",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
keys: encodeURIComponent(keys.join(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetProjectSecretsOverview = (
|
export const useGetProjectSecretsOverview = (
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -29,6 +29,10 @@ export type DashboardProjectSecretsDetailsResponse = {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DashboardProjectSecretsByKeys = {
|
||||||
|
secrets: SecretV3Raw[];
|
||||||
|
};
|
||||||
|
|
||||||
export type DashboardProjectSecretsOverview = Omit<
|
export type DashboardProjectSecretsOverview = Omit<
|
||||||
DashboardProjectSecretsOverviewResponse,
|
DashboardProjectSecretsOverviewResponse,
|
||||||
"secrets"
|
"secrets"
|
||||||
@ -89,3 +93,10 @@ export type TGetDashboardProjectSecretsQuickSearchDTO = {
|
|||||||
search: string;
|
search: string;
|
||||||
environments: string[];
|
environments: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetDashboardProjectSecretsByKeys = {
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
keys: string[];
|
||||||
|
};
|
||||||
|
@ -552,7 +552,6 @@ const SecretMainPageContent = () => {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<SecretDropzone
|
<SecretDropzone
|
||||||
secrets={secrets}
|
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
|
@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
faClone,
|
faClone,
|
||||||
|
faFileImport,
|
||||||
faKey,
|
faKey,
|
||||||
faSearch,
|
faSearch,
|
||||||
faSquareCheck,
|
faSquareCheck,
|
||||||
@ -151,6 +152,7 @@ export const CopySecretsFromBoard = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||||
onClick={() => onToggle(true)}
|
onClick={() => onToggle(true)}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
variant="star"
|
variant="star"
|
||||||
|
@ -0,0 +1,165 @@
|
|||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
|
import { faInfoCircle, faPaste } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalTrigger,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
isSmaller?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
value: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const PasteEnvForm = ({ onParsedEnv }: Pick<Props, "onParsedEnv">) => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { isDirty, errors },
|
||||||
|
setError,
|
||||||
|
setFocus
|
||||||
|
} = useForm<TForm>({ defaultValues: { value: "" }, resolver: zodResolver(formSchema) });
|
||||||
|
|
||||||
|
const onSubmit = ({ value }: TForm) => {
|
||||||
|
let env: Record<string, { value: string; comments: string[] }>;
|
||||||
|
try {
|
||||||
|
env = parseJson(value);
|
||||||
|
} catch (e) {
|
||||||
|
// not json, parse as env
|
||||||
|
env = parseDotEnv(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(env).length) {
|
||||||
|
setError("value", {
|
||||||
|
message: "No secrets found. Please make sure the provided format is valid."
|
||||||
|
});
|
||||||
|
setFocus("value");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onParsedEnv(env);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<FormControl
|
||||||
|
label="Secret Values"
|
||||||
|
isError={Boolean(errors.value)}
|
||||||
|
errorText={errors.value?.message}
|
||||||
|
icon={<FontAwesomeIcon size="sm" className="text-mineshaft-400" icon={faInfoCircle} />}
|
||||||
|
tooltipClassName="max-w-lg px-2 whitespace-pre-line"
|
||||||
|
tooltipText={
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p>Example Formats:</p>
|
||||||
|
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||||
|
{/* eslint-disable-next-line react/jsx-no-comment-textnodes */}
|
||||||
|
<p className="text-mineshaft-400">// .json</p>
|
||||||
|
{JSON.stringify(
|
||||||
|
{
|
||||||
|
APP_NAME: "example-service",
|
||||||
|
APP_VERSION: "1.2.3",
|
||||||
|
NODE_ENV: "production"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||||
|
<p className="text-mineshaft-400"># .env</p>
|
||||||
|
<p>APP_NAME="example-service"</p>
|
||||||
|
<p>APP_VERSION="1.2.3"</p>
|
||||||
|
<p>NODE_ENV="production"</p>
|
||||||
|
</pre>
|
||||||
|
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
|
||||||
|
<p className="text-mineshaft-400"># .yml</p>
|
||||||
|
<p>APP_NAME: example-service</p>
|
||||||
|
<p>APP_VERSION: 1.2.3</p>
|
||||||
|
<p>NODE_ENV: production</p>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...register("value")}
|
||||||
|
placeholder="Paste secrets in .json, .yml or .env format..."
|
||||||
|
className="h-[60vh] !resize-none"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button isDisabled={!isDirty} type="submit">
|
||||||
|
Import Secrets
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasteSecretEnvModal = ({
|
||||||
|
isSmaller,
|
||||||
|
isOpen,
|
||||||
|
onParsedEnv,
|
||||||
|
onToggle,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPaste} />}
|
||||||
|
onClick={() => onToggle(true)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="star"
|
||||||
|
size={isSmaller ? "xs" : "sm"}
|
||||||
|
>
|
||||||
|
Paste Secrets
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-2xl"
|
||||||
|
title="Past Secret Values"
|
||||||
|
subTitle="Paste values in .env, .json or .yml format"
|
||||||
|
>
|
||||||
|
<PasteEnvForm
|
||||||
|
onParsedEnv={(value) => {
|
||||||
|
onToggle(false);
|
||||||
|
onParsedEnv(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { ChangeEvent, DragEvent } from "react";
|
import { ChangeEvent, DragEvent } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@ -9,30 +9,22 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
|
||||||
import { Button, Modal, ModalContent } from "@app/components/v2";
|
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
|
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
|
||||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
import {
|
||||||
|
dashboardKeys,
|
||||||
|
fetchDashboardProjectSecretsByKeys
|
||||||
|
} from "@app/hooks/api/dashboard/queries";
|
||||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||||
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
||||||
|
import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
|
||||||
const parseJson = (src: ArrayBuffer) => {
|
|
||||||
const file = src.toString();
|
|
||||||
const formatedData: Record<string, string> = JSON.parse(file);
|
|
||||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
|
||||||
Object.keys(formatedData).forEach((key) => {
|
|
||||||
if (typeof formatedData[key] === "string") {
|
|
||||||
env[key] = { value: formatedData[key], comments: [] };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return env;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
|
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
|
||||||
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
||||||
@ -43,7 +35,6 @@ type Props = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
secrets?: SecretV3RawSanitized[];
|
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,7 +44,6 @@ export const SecretDropzone = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
secrets = [],
|
|
||||||
isProtectedBranch = false
|
isProtectedBranch = false
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -62,7 +52,8 @@ export const SecretDropzone = ({
|
|||||||
|
|
||||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"importSecEnv",
|
"importSecEnv",
|
||||||
"overlapKeyWarning"
|
"confirmUpload",
|
||||||
|
"pasteSecEnv"
|
||||||
] as const);
|
] as const);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { openPopUp } = usePopUpAction();
|
const { openPopUp } = usePopUpAction();
|
||||||
@ -86,20 +77,10 @@ export const SecretDropzone = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleParsedEnv = (env: TParsedEnv) => {
|
const handleParsedEnv = async (env: TParsedEnv) => {
|
||||||
const secretsGroupedByKey = secrets?.reduce<Record<string, boolean>>(
|
const envSecretKeys = Object.keys(env);
|
||||||
(prev, curr) => ({ ...prev, [curr.key]: true }),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const overlappedSecrets = Object.keys(env)
|
|
||||||
.filter((secKey) => secretsGroupedByKey?.[secKey])
|
|
||||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
|
||||||
|
|
||||||
const nonOverlappedSecrets = Object.keys(env)
|
if (!envSecretKeys.length) {
|
||||||
.filter((secKey) => !secretsGroupedByKey?.[secKey])
|
|
||||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
|
||||||
|
|
||||||
if (!Object.keys(overlappedSecrets).length && !Object.keys(nonOverlappedSecrets).length) {
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: "Failed to find secrets"
|
text: "Failed to find secrets"
|
||||||
@ -107,10 +88,42 @@ export const SecretDropzone = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("overlapKeyWarning", {
|
try {
|
||||||
update: overlappedSecrets,
|
setIsLoading.on();
|
||||||
create: nonOverlappedSecrets
|
const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({
|
||||||
});
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectId: workspaceId,
|
||||||
|
keys: envSecretKeys
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretsGroupedByKey = existingSecrets.reduce<Record<string, boolean>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.secretKey]: true }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSecrets = Object.keys(env)
|
||||||
|
.filter((secKey) => secretsGroupedByKey[secKey])
|
||||||
|
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||||
|
|
||||||
|
const createSecrets = Object.keys(env)
|
||||||
|
.filter((secKey) => !secretsGroupedByKey[secKey])
|
||||||
|
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||||
|
|
||||||
|
handlePopUpOpen("confirmUpload", {
|
||||||
|
update: updateSecrets,
|
||||||
|
create: createSecrets
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to check for secret conflicts",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
handlePopUpClose("confirmUpload");
|
||||||
|
} finally {
|
||||||
|
setIsLoading.off();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseFile = (file?: File, isJson?: boolean) => {
|
const parseFile = (file?: File, isJson?: boolean) => {
|
||||||
@ -160,7 +173,7 @@ export const SecretDropzone = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveSecrets = async () => {
|
const handleSaveSecrets = async () => {
|
||||||
const { update, create } = popUp?.overlapKeyWarning?.data as TSecOverwriteOpt;
|
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
|
||||||
try {
|
try {
|
||||||
if (Object.keys(create || {}).length) {
|
if (Object.keys(create || {}).length) {
|
||||||
await createSecretBatch({
|
await createSecretBatch({
|
||||||
@ -195,7 +208,7 @@ export const SecretDropzone = ({
|
|||||||
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
||||||
handlePopUpClose("overlapKeyWarning");
|
handlePopUpClose("confirmUpload");
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: isProtectedBranch
|
text: isProtectedBranch
|
||||||
@ -211,10 +224,16 @@ export const SecretDropzone = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUploadedDuplicateSecretsEmpty = !Object.keys(
|
const createSecretCount = Object.keys(
|
||||||
(popUp.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {}
|
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
const updateSecretCount = Object.keys(
|
||||||
|
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {}
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const isNonConflictingUpload = !updateSecretCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@ -278,7 +297,15 @@ export const SecretDropzone = ({
|
|||||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center space-x-8">
|
<div className="flex flex-col items-center justify-center gap-4 lg:flex-row">
|
||||||
|
<PasteSecretEnvModal
|
||||||
|
isOpen={popUp.pasteSecEnv.isOpen}
|
||||||
|
onToggle={(isOpen) => handlePopUpToggle("pasteSecEnv", isOpen)}
|
||||||
|
onParsedEnv={handleParsedEnv}
|
||||||
|
environment={environment}
|
||||||
|
secretPath={secretPath}
|
||||||
|
isSmaller={isSmaller}
|
||||||
|
/>
|
||||||
<CopySecretsFromBoard
|
<CopySecretsFromBoard
|
||||||
isOpen={popUp.importSecEnv.isOpen}
|
isOpen={popUp.importSecEnv.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
||||||
@ -301,11 +328,12 @@ export const SecretDropzone = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
||||||
variant="star"
|
variant="star"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add a new secret
|
Add a New Secret
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
@ -315,25 +343,25 @@ export const SecretDropzone = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.overlapKeyWarning?.isOpen}
|
isOpen={popUp?.confirmUpload?.isOpen}
|
||||||
onOpenChange={(open) => handlePopUpToggle("overlapKeyWarning", open)}
|
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={isUploadedDuplicateSecretsEmpty ? "Confirmation" : "Duplicate Secrets!!"}
|
title="Confirm Secret Upload"
|
||||||
footerContent={[
|
footerContent={[
|
||||||
<Button
|
<Button
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
colorSchema={isUploadedDuplicateSecretsEmpty ? "primary" : "danger"}
|
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
|
||||||
key="overwrite-btn"
|
key="overwrite-btn"
|
||||||
onClick={handleSaveSecrets}
|
onClick={handleSaveSecrets}
|
||||||
>
|
>
|
||||||
{isUploadedDuplicateSecretsEmpty ? "Upload" : "Overwrite"}
|
{isNonConflictingUpload ? "Upload" : "Overwrite"}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key="keep-old-btn"
|
key="keep-old-btn"
|
||||||
className="mr-4"
|
className="ml-4"
|
||||||
onClick={() => handlePopUpClose("overlapKeyWarning")}
|
onClick={() => handlePopUpClose("confirmUpload")}
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
@ -341,17 +369,27 @@ export const SecretDropzone = ({
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isUploadedDuplicateSecretsEmpty ? (
|
{isNonConflictingUpload ? (
|
||||||
<div>Upload secrets from this file</div>
|
<div>
|
||||||
|
Are you sure you want to import {createSecretCount} secret
|
||||||
|
{createSecretCount > 1 ? "s" : ""} to this environment?
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col space-y-2 text-gray-300">
|
<div className="flex flex-col text-gray-300">
|
||||||
<div>Your file contains following duplicate secrets</div>
|
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
||||||
<div className="text-sm text-gray-400">
|
<div className="mt-2 text-sm text-gray-400">
|
||||||
{Object.keys((popUp?.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {})
|
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
||||||
?.map((key) => key)
|
?.map((key) => key)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
<div>Are you sure you want to overwrite these secrets and create other ones?</div>
|
<div className="mt-6">
|
||||||
|
Are you sure you want to overwrite these secrets
|
||||||
|
{createSecretCount > 0
|
||||||
|
? ` and import ${createSecretCount} new
|
||||||
|
one${createSecretCount > 1 ? "s" : ""}`
|
||||||
|
: ""}
|
||||||
|
?
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
9
npm/.eslintrc.json
Normal file
9
npm/.eslintrc.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest"
|
||||||
|
}
|
||||||
|
}
|
71
npm/README.md
Normal file
71
npm/README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<h1 align="center">Infisical</h1>
|
||||||
|
<p align="center">
|
||||||
|
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4 align="center">
|
||||||
|
<a href="https://infisical.com/slack">Slack</a> |
|
||||||
|
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||||
|
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||||
|
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
||||||
|
<a href="https://www.infisical.com">Website</a> |
|
||||||
|
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
|
||||||
|
<h4 align="center">
|
||||||
|
<a href="https://github.com/Infisical/infisical/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
|
||||||
|
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Infisical/infisical/issues">
|
||||||
|
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||||
|
</a>
|
||||||
|
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||||
|
<img src="https://img.shields.io/badge/Downloads-6.95M-orange" alt="Cloudsmith downloads" />
|
||||||
|
</a>
|
||||||
|
<a href="https://infisical.com/slack">
|
||||||
|
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/infisical">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/infisical?label=Follow" alt="Infisical Twitter" />
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI.
|
||||||
|
|
||||||
|
We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
|
||||||
|
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
The Infisical CLI NPM package serves as a new installation method in addition to our [existing installation methods](https://infisical.com/docs/cli/overview).
|
||||||
|
|
||||||
|
After installing the CLI with the command below, you'll be able to use the infisical CLI across your machine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @infisical/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Full example:
|
||||||
|
```bash
|
||||||
|
# Install the Infisical CLI
|
||||||
|
$ npm install -g @infisical/cli
|
||||||
|
|
||||||
|
# Authenticate with the Infisical CLI
|
||||||
|
$ infisical login
|
||||||
|
|
||||||
|
# Initialize your Infisical CLI
|
||||||
|
$ infisical init
|
||||||
|
|
||||||
|
# List your secrets with Infisical CLI
|
||||||
|
$ infisical secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
Our full CLI documentation can be found [here](https://infisical.com/docs/cli/usage).
|
141
npm/package-lock.json
generated
Normal file
141
npm/package-lock.json
generated
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"name": "@infisical/cli",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@infisical/cli",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tar": "^6.2.0",
|
||||||
|
"yauzl": "^3.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"infisical": "bin/infisical"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-minipass": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-minipass/node_modules/minipass": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib/node_modules/minipass": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^2.0.0",
|
||||||
|
"fs-minipass": "^2.0.0",
|
||||||
|
"minipass": "^5.0.0",
|
||||||
|
"minizlib": "^2.1.1",
|
||||||
|
"mkdirp": "^1.0.3",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
npm/package.json
Normal file
25
npm/package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@infisical/cli",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"infisical",
|
||||||
|
"cli",
|
||||||
|
"command-line"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"infisical": "./bin/infisical"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Infisical/infisical.git"
|
||||||
|
},
|
||||||
|
"author": "Infisical Inc, <daniel@infisical.com>",
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "node src/index.cjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tar": "^6.2.0",
|
||||||
|
"yauzl": "^3.2.0"
|
||||||
|
}
|
||||||
|
}
|
152
npm/src/index.cjs
Normal file
152
npm/src/index.cjs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const stream = require("node:stream");
|
||||||
|
const tar = require("tar");
|
||||||
|
const path = require("path");
|
||||||
|
const zlib = require("zlib");
|
||||||
|
const yauzl = require("yauzl");
|
||||||
|
|
||||||
|
const packageJSON = require("../package.json");
|
||||||
|
|
||||||
|
const supportedPlatforms = ["linux", "darwin", "win32", "freebsd", "windows"];
|
||||||
|
const outputDir = "bin";
|
||||||
|
|
||||||
|
const getPlatform = () => {
|
||||||
|
let platform = process.platform;
|
||||||
|
|
||||||
|
if (platform === "win32") {
|
||||||
|
platform = "windows";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedPlatforms.includes(platform)) {
|
||||||
|
console.error("Your platform doesn't seem to be of type darwin, linux or windows");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return platform;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArchitecture = () => {
|
||||||
|
const architecture = process.arch;
|
||||||
|
let arch = "";
|
||||||
|
|
||||||
|
if (architecture === "x64" || architecture === "amd64") {
|
||||||
|
arch = "amd64";
|
||||||
|
} else if (architecture === "arm64") {
|
||||||
|
arch = "arm64";
|
||||||
|
} else if (architecture === "arm") {
|
||||||
|
// If the platform is Linux, we should find the exact ARM version, otherwise we default to armv7 which is the most common
|
||||||
|
if (process.platform === "linux" || process.platform === "freebsd") {
|
||||||
|
const output = childProcess.execSync("uname -m").toString().trim();
|
||||||
|
|
||||||
|
const armVersions = ["armv5", "armv6", "armv7"];
|
||||||
|
|
||||||
|
const armVersion = armVersions.find(version => output.startsWith(version));
|
||||||
|
|
||||||
|
if (armVersion) {
|
||||||
|
arch = armVersion;
|
||||||
|
} else {
|
||||||
|
arch = "armv7";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arch = "armv7";
|
||||||
|
}
|
||||||
|
} else if (architecture === "ia32") {
|
||||||
|
arch = "i386";
|
||||||
|
} else {
|
||||||
|
console.error("Your architecture doesn't seem to be supported. Your architecture is", architecture);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arch;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function extractZip(buffer, targetPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
|
||||||
|
zipfile.readEntry();
|
||||||
|
zipfile.on("entry", entry => {
|
||||||
|
const isExecutable = entry.fileName === "infisical" || entry.fileName === "infisical.exe";
|
||||||
|
|
||||||
|
if (/\/$/.test(entry.fileName) || !isExecutable) {
|
||||||
|
// Directory entry
|
||||||
|
zipfile.readEntry();
|
||||||
|
} else {
|
||||||
|
// File entry
|
||||||
|
zipfile.openReadStream(entry, (err, readStream) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
|
||||||
|
const outputPath = path.join(targetPath, entry.fileName.includes("infisical") ? "infisical" : entry.fileName);
|
||||||
|
const writeStream = fs.createWriteStream(outputPath);
|
||||||
|
|
||||||
|
readStream.pipe(writeStream);
|
||||||
|
writeStream.on("close", () => {
|
||||||
|
zipfile.readEntry();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
zipfile.on("end", resolve);
|
||||||
|
zipfile.on("error", reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const PLATFORM = getPlatform();
|
||||||
|
const ARCH = getArchitecture();
|
||||||
|
const NUMERIC_RELEASE_VERSION = packageJSON.version;
|
||||||
|
const LATEST_RELEASE_VERSION = `v${NUMERIC_RELEASE_VERSION}`;
|
||||||
|
const EXTENSION = PLATFORM === "windows" ? "zip" : "tar.gz";
|
||||||
|
const downloadLink = `https://github.com/Infisical/infisical/releases/download/infisical-cli/${LATEST_RELEASE_VERSION}/infisical_${NUMERIC_RELEASE_VERSION}_${PLATFORM}_${ARCH}.${EXTENSION}`;
|
||||||
|
|
||||||
|
// Ensure the output directory exists
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the latest CLI binary
|
||||||
|
try {
|
||||||
|
const response = await fetch(downloadLink, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/octet-stream"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EXTENSION === "zip") {
|
||||||
|
// For ZIP files, we need to buffer the whole thing first
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
await extractZip(Buffer.from(buffer), outputDir);
|
||||||
|
} else {
|
||||||
|
// For tar.gz files, we stream
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const outStream = stream.Readable.fromWeb(response.body)
|
||||||
|
.pipe(zlib.createGunzip())
|
||||||
|
.pipe(
|
||||||
|
tar.x({
|
||||||
|
C: path.join(outputDir),
|
||||||
|
filter: path => path === "infisical"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
outStream.on("error", reject);
|
||||||
|
outStream.on("close", resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the binary execute permissions if we're not on Windows
|
||||||
|
if (PLATFORM !== "win32") {
|
||||||
|
fs.chmodSync(path.join(outputDir, "infisical"), "755");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading or extracting Infisical CLI:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main();
|
Reference in New Issue
Block a user