Compare commits

...

27 Commits

Author SHA1 Message Date
05f07b25ac fix: cli npm release windows and symlink bugs 2024-11-14 06:13:14 +04:00
60fb195706 Merge pull request #2726 from Infisical/scott/paste-secrets
Feat: Paste Secrets for Upload
2024-11-12 17:57:13 -08:00
c8109b4e84 improvement: add example paste value formats 2024-11-12 16:46:35 -08:00
1f2b0443cc improvement: address requested changes 2024-11-12 16:11:47 -08:00
dd1cabf9f6 Merge pull request #2727 from Infisical/daniel/fix-npm-cli-symlink
fix: npm cli symlink
2024-11-12 22:47:01 +04:00
8b781b925a fix: npm cli symlink 2024-11-12 22:45:37 +04:00
ddcf5b576b improvement: improve field error message 2024-11-12 10:25:23 -08:00
7138b392f2 Feature: add ability to paste .env, .yml or .json secrets for upload and also fix upload when keys conflict but are not on current page 2024-11-12 10:21:07 -08:00
bfce1021fb Merge pull request #1076 from G3root/infisical-npm
feat: infisical cli for npm
2024-11-12 21:48:47 +04:00
93c0313b28 docs: added NPM install option 2024-11-12 21:48:04 +04:00
8cfc217519 Update README.md 2024-11-12 21:38:34 +04:00
d272c6217a Merge pull request #2722 from Infisical/scott/secret-refrence-fixes
Fix: Secret Reference Multiple References and Special Character Stripping
2024-11-12 22:49:18 +05:30
2fe2ddd9fc Update package.json 2024-11-12 21:17:53 +04:00
e330ddd5ee fix: remove dry run 2024-11-12 20:56:18 +04:00
7aba9c1a50 Update index.cjs 2024-11-12 20:54:55 +04:00
4cd8e0fa67 fix: workflow fixes 2024-11-12 20:47:10 +04:00
ea3d164ead Update release_build_infisical_cli.yml 2024-11-12 20:40:45 +04:00
df468e4865 Update release_build_infisical_cli.yml 2024-11-12 20:39:16 +04:00
66e96018c4 Update release_build_infisical_cli.yml 2024-11-12 20:37:28 +04:00
3b02eedca6 feat: npm CLI 2024-11-12 20:36:09 +04:00
a55fe2b788 chore: add git ignore 2024-11-12 17:40:46 +04:00
5d7a267f1d chore: add package.json 2024-11-12 17:40:37 +04:00
b16ab6f763 feat: add script 2024-11-12 17:40:37 +04:00
334a728259 chore: remove console log 2024-11-11 14:06:12 -08:00
4a3143e689 fix: correct unique secret check to account for env and path 2024-11-11 14:04:36 -08:00
14810de054 fix: correct secret reference value replacement to support special characters 2024-11-11 13:46:39 -08:00
8cfcbaa12c fix: correct secret reference validation check to permit referencing the same secret multiple times and improve error message 2024-11-11 13:17:25 -08:00
23 changed files with 888 additions and 70 deletions

View File

@ -10,8 +10,7 @@ on:
permissions:
contents: write
# packages: write
# issues: write
jobs:
cli-integration-tests:
name: Run tests before deployment
@ -26,6 +25,63 @@ jobs:
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
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:
runs-on: ubuntu-20.04
needs: [cli-integration-tests]

2
.gitignore vendored
View File

@ -71,3 +71,5 @@ frontend-build
cli/infisical-merge
cli/test/infisical-merge
/backend/binary
/npm/bin

View File

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

View File

@ -361,6 +361,10 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
}
}
if (filters?.keys) {
void bd.whereIn(`${TableName.SecretV2}.key`, filters.keys);
}
})
.where((bd) => {
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });

View File

@ -518,7 +518,10 @@ export const expandSecretReferencesFactory = ({
}
if (referencedSecretValue) {
expandedValue = expandedValue.replaceAll(interpolationSyntax, referencedSecretValue);
expandedValue = expandedValue.replaceAll(
interpolationSyntax,
() => referencedSecretValue // prevents special characters from triggering replacement patterns
);
}
}
}

View File

@ -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({
message: `Referenced secret not found. Found only ${diff(
message: `Referenced secret(s) not found: ${diff(
references.map((el) => el.secretKey),
referredSecrets.map((el) => el.key)
).join(",")}`

View File

@ -33,6 +33,7 @@ export type TGetSecretsDTO = {
offset?: number;
limit?: number;
search?: string;
keys?: string[];
} & TProjectPermission;
export type TGetASecretDTO = {
@ -294,6 +295,7 @@ export type TFindSecretsByFolderIdsFilter = {
search?: string;
tagSlugs?: string[];
includeTagsInSearch?: boolean;
keys?: string[];
};
export type TGetSecretsRawByFolderMappingsDTO = {

View File

@ -185,6 +185,7 @@ export type TGetSecretsRawDTO = {
offset?: number;
limit?: number;
search?: string;
keys?: string[];
} & TProjectPermission;
export type TGetASecretRawDTO = {

View File

@ -9,7 +9,7 @@ You can use it across various environments, whether it's local development, CI/C
## Installation
<Tabs>
<Tab title="MacOS">
<Tab title="MacOS">
Use [brew](https://brew.sh/) package manager
```bash
@ -21,9 +21,8 @@ You can use it across various environments, whether it's local development, CI/C
```bash
brew update && brew upgrade infisical
```
</Tab>
<Tab title="Windows">
</Tab>
<Tab title="Windows">
Use [Scoop](https://scoop.sh/) package manager
```bash
@ -40,7 +39,20 @@ You can use it across various environments, whether it's local development, CI/C
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">
Install prerequisite
```bash

View File

@ -11,7 +11,7 @@ import { SecretType } from "@app/hooks/api/types";
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import { createNotification } from "../notifications";
import { parseDotEnv } from "../utilities/parseDotEnv";
import { parseDotEnv } from "../utilities/parseSecrets";
import guidGenerator from "../utilities/randomId";
interface DropZoneProps {

View File

@ -6,7 +6,7 @@ const LINE =
* @param {ArrayBuffer} src - source buffer
* @returns {String} text - text of buffer
*/
export function parseDotEnv(src: ArrayBuffer) {
export function parseDotEnv(src: ArrayBuffer | string) {
const object: {
[key: string]: { value: string; comments: string[] };
} = {};
@ -65,3 +65,15 @@ export function parseDotEnv(src: ArrayBuffer) {
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;
};

View File

@ -83,6 +83,7 @@ export type FormControlProps = {
className?: string;
icon?: ReactNode;
tooltipText?: ReactElement | string;
tooltipClassName?: string;
};
export const FormControl = ({
@ -96,7 +97,8 @@ export const FormControl = ({
isError,
icon,
className,
tooltipText
tooltipText,
tooltipClassName
}: FormControlProps): JSX.Element => {
return (
<div className={twMerge("mb-4", className)}>
@ -108,6 +110,7 @@ export const FormControl = ({
id={id}
icon={icon}
tooltipText={tooltipText}
tooltipClassName={tooltipClassName}
/>
) : (
label

View File

@ -5,6 +5,7 @@ import axios from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import {
DashboardProjectSecretsByKeys,
DashboardProjectSecretsDetails,
DashboardProjectSecretsDetailsResponse,
DashboardProjectSecretsOverview,
@ -12,6 +13,7 @@ import {
DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO
@ -101,6 +103,23 @@ export const fetchProjectSecretsDetails = async ({
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 = (
{
projectId,

View File

@ -29,6 +29,10 @@ export type DashboardProjectSecretsDetailsResponse = {
totalCount: number;
};
export type DashboardProjectSecretsByKeys = {
secrets: SecretV3Raw[];
};
export type DashboardProjectSecretsOverview = Omit<
DashboardProjectSecretsOverviewResponse,
"secrets"
@ -89,3 +93,10 @@ export type TGetDashboardProjectSecretsQuickSearchDTO = {
search: string;
environments: string[];
};
export type TGetDashboardProjectSecretsByKeys = {
projectId: string;
secretPath: string;
environment: string;
keys: string[];
};

View File

@ -552,7 +552,6 @@ const SecretMainPageContent = () => {
</ModalContent>
</Modal>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}

View File

@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import {
faClone,
faFileImport,
faKey,
faSearch,
faSquareCheck,
@ -151,6 +152,7 @@ export const CopySecretsFromBoard = ({
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
onClick={() => onToggle(true)}
isDisabled={!isAllowed}
variant="star"

View File

@ -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=&quot;example-service&quot;</p>
<p>APP_VERSION=&quot;1.2.3&quot;</p>
<p>NODE_ENV=&quot;production&quot;</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>
);
};

View File

@ -1,7 +1,7 @@
import { ChangeEvent, DragEvent } from "react";
import { useTranslation } from "react-i18next";
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 { useQueryClient } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge";
@ -9,30 +9,22 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
// 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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
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 { 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 { CopySecretsFromBoard } from "./CopySecretsFromBoard";
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;
};
import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
@ -43,7 +35,6 @@ type Props = {
workspaceId: string;
environment: string;
secretPath: string;
secrets?: SecretV3RawSanitized[];
isProtectedBranch?: boolean;
};
@ -53,7 +44,6 @@ export const SecretDropzone = ({
workspaceId,
environment,
secretPath,
secrets = [],
isProtectedBranch = false
}: Props): JSX.Element => {
const { t } = useTranslation();
@ -62,7 +52,8 @@ export const SecretDropzone = ({
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"importSecEnv",
"overlapKeyWarning"
"confirmUpload",
"pasteSecEnv"
] as const);
const queryClient = useQueryClient();
const { openPopUp } = usePopUpAction();
@ -86,20 +77,10 @@ export const SecretDropzone = ({
}
};
const handleParsedEnv = (env: TParsedEnv) => {
const secretsGroupedByKey = secrets?.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.key]: true }),
{}
);
const overlappedSecrets = Object.keys(env)
.filter((secKey) => secretsGroupedByKey?.[secKey])
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
const handleParsedEnv = async (env: TParsedEnv) => {
const envSecretKeys = Object.keys(env);
const nonOverlappedSecrets = Object.keys(env)
.filter((secKey) => !secretsGroupedByKey?.[secKey])
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
if (!Object.keys(overlappedSecrets).length && !Object.keys(nonOverlappedSecrets).length) {
if (!envSecretKeys.length) {
createNotification({
type: "error",
text: "Failed to find secrets"
@ -107,10 +88,42 @@ export const SecretDropzone = ({
return;
}
handlePopUpOpen("overlapKeyWarning", {
update: overlappedSecrets,
create: nonOverlappedSecrets
});
try {
setIsLoading.on();
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) => {
@ -160,7 +173,7 @@ export const SecretDropzone = ({
};
const handleSaveSecrets = async () => {
const { update, create } = popUp?.overlapKeyWarning?.data as TSecOverwriteOpt;
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
try {
if (Object.keys(create || {}).length) {
await createSecretBatch({
@ -195,7 +208,7 @@ export const SecretDropzone = ({
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
);
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
handlePopUpClose("overlapKeyWarning");
handlePopUpClose("confirmUpload");
createNotification({
type: "success",
text: isProtectedBranch
@ -211,10 +224,16 @@ export const SecretDropzone = ({
}
};
const isUploadedDuplicateSecretsEmpty = !Object.keys(
(popUp.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {}
const createSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
).length;
const updateSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {}
).length;
const isNonConflictingUpload = !updateSecretCount;
return (
<div>
<div
@ -278,7 +297,15 @@ export const SecretDropzone = ({
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
<div className="w-1/5 border-t border-mineshaft-700" />
</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
isOpen={popUp.importSecEnv.isOpen}
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
@ -301,11 +328,12 @@ export const SecretDropzone = ({
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
variant="star"
isDisabled={!isAllowed}
>
Add a new secret
Add a New Secret
</Button>
)}
</ProjectPermissionCan>
@ -315,25 +343,25 @@ export const SecretDropzone = ({
)}
</div>
<Modal
isOpen={popUp?.overlapKeyWarning?.isOpen}
onOpenChange={(open) => handlePopUpToggle("overlapKeyWarning", open)}
isOpen={popUp?.confirmUpload?.isOpen}
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
>
<ModalContent
title={isUploadedDuplicateSecretsEmpty ? "Confirmation" : "Duplicate Secrets!!"}
title="Confirm Secret Upload"
footerContent={[
<Button
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema={isUploadedDuplicateSecretsEmpty ? "primary" : "danger"}
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
key="overwrite-btn"
onClick={handleSaveSecrets}
>
{isUploadedDuplicateSecretsEmpty ? "Upload" : "Overwrite"}
{isNonConflictingUpload ? "Upload" : "Overwrite"}
</Button>,
<Button
key="keep-old-btn"
className="mr-4"
onClick={() => handlePopUpClose("overlapKeyWarning")}
className="ml-4"
onClick={() => handlePopUpClose("confirmUpload")}
variant="outline_bg"
isDisabled={isSubmitting}
>
@ -341,17 +369,27 @@ export const SecretDropzone = ({
</Button>
]}
>
{isUploadedDuplicateSecretsEmpty ? (
<div>Upload secrets from this file</div>
{isNonConflictingUpload ? (
<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>Your file contains following duplicate secrets</div>
<div className="text-sm text-gray-400">
{Object.keys((popUp?.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {})
<div className="flex flex-col text-gray-300">
<div>Your project already contains the following {updateSecretCount} secrets:</div>
<div className="mt-2 text-sm text-gray-400">
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
?.map((key) => key)
.join(", ")}
</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>
)}
</ModalContent>

9
npm/.eslintrc.json Normal file
View File

@ -0,0 +1,9 @@
{
"env": {
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": "latest"
}
}

71
npm/README.md Normal file
View 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
View 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
View 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
View 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();