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
25 changed files with 889 additions and 217 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

@ -1,145 +0,0 @@
---
title: GitLab
description: "Learn how to authenticate GitLab pipelines with Infisical using OpenID Connect (OIDC)."
---
**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect.
## Diagram
The following sequence diagram illustrates the OIDC Auth workflow for authenticating GitLab pipelines with Infisical.
```mermaid
sequenceDiagram
participant Client as GitLab Pipeline
participant Idp as GitLab Identity Provider
participant Infis as Infisical
Client->>Idp: Step 1: Request identity token
Idp-->>Client: Return JWT with verifiable claims
Note over Client,Infis: Step 2: Login Operation
Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login
Note over Infis,Idp: Step 3: Query verification
Infis->>Idp: Request JWT public key using OIDC Discovery
Idp-->>Infis: Return public key
Note over Infis: Step 4: JWT validation
Infis->>Client: Return short-lived access token
Note over Client,Infis: Step 5: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The GitLab pipeline requests an identity token from GitLab's identity provider.
2. The fetched identity token is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint.
3. Infisical fetches the public key that was used to sign the identity token from GitLab's identity provider using OIDC Discovery.
4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria.
5. If all is well, Infisical returns a short-lived access token that the GitLab pipeline can use to make authenticated requests to the Infisical API.
<Note>
Infisical needs network-level access to GitLab's identity provider endpoints.
</Note>
## Guide
In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png)
<Warning>Restrict access by configuring the Subject, Audiences, and Claims fields</Warning>
Here's some more guidance on each field:
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT. For GitLab SaaS (GitLab.com), this should be set to `https://gitlab.com`. For self-hosted GitLab instances, use the domain of your GitLab instance.
- Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider. This should also be set to the domain of the Gitlab instance.
- CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints. For GitLab.com, this can be left blank.
- Subject: The expected principal that is the subject of the JWT. For GitLab pipelines, this should be set to a string that uniquely identifies the pipeline and its context, in the format `project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}` (e.g., `project_path:example-group/example-project:ref_type:branch:ref:main`).
- Claims: Additional information or attributes that should be present in the JWT for it to be valid. You can refer to GitLab's [documentation](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload) for the list of supported claims.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
<Tip>For more details on the appropriate values for the OIDC fields, refer to GitLab's [documentation](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload). </Tip>
<Info>The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible.</Info>
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
As demonstration, we will be using the Infisical CLI to fetch Infisical secrets and utilize them within a GitLab pipeline.
To access Infisical secrets as the identity, you need to use an identity token from GitLab which matches the OIDC configuration defined for the machine identity.
This can be done by defining the `id_tokens` property. The resulting token would then be used to login with OIDC like the following: `infisical login --method=oidc-auth --oidc-jwt=$GITLAB_TOKEN`
Below is a complete example of how a GitLab pipeline can be configured to work with secrets from Infisical using the Infisical CLI with OIDC Auth:
```yaml
image: ubuntu
stages:
- build
build-job:
stage: build
id_tokens:
INFISICAL_ID_TOKEN:
aud: infisical-aud-test
script:
- apt update && apt install -y curl
- curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
- apt-get update && apt-get install -y infisical
- export INFISICAL_TOKEN=$(infisical login --method=oidc-auth --machine-identity-id=4e807a78-1b1c-4bd6-9609-ef2b0cf4fd54 --oidc-jwt=$INFISICAL_ID_TOKEN --silent --plain)
- infisical run --projectId=1d0443c1-cd43-4b3a-91a3-9d5f81254a89 --env=dev -- npm run build
```
The `id_tokens` keyword is used to request an ID token for the job. In this example, an ID token named `INFISICAL_ID_TOKEN` is requested with the audience (`aud`) claim set to "infisical-aud-test". This ID token will be used to authenticate with Infisical.
<Note>
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation.
</Note>
</Step>
</Steps>

View File

@ -226,8 +226,7 @@
"pages": [
"documentation/platform/identities/oidc-auth/general",
"documentation/platform/identities/oidc-auth/github",
"documentation/platform/identities/oidc-auth/circleci",
"documentation/platform/identities/oidc-auth/gitlab"
"documentation/platform/identities/oidc-auth/circleci"
]
},
"documentation/platform/mfa",

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();