Compare commits
33 Commits
main
...
jill/handl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd693ea721 | ||
|
|
7d492de296 | ||
|
|
75acf919f3 | ||
|
|
c4aeeff620 | ||
|
|
44af64418a | ||
|
|
50fb695a57 | ||
|
|
e7517d7b3e | ||
|
|
db4ac8464b | ||
|
|
5523b3fd67 | ||
|
|
44ef890925 | ||
|
|
d7959a3396 | ||
|
|
ba2e69a32e | ||
|
|
e5946f890f | ||
|
|
6afd0621a7 | ||
|
|
72c110fb96 | ||
|
|
b590204659 | ||
|
|
6490b7af0e | ||
|
|
676313036e | ||
|
|
43c1f24739 | ||
|
|
3c643fe809 | ||
|
|
41f600a118 | ||
|
|
a03b151beb | ||
|
|
9ef8ce29fa | ||
|
|
cc33b584d1 | ||
|
|
21385b0c31 | ||
|
|
2a0e01171e | ||
|
|
8f91e40957 | ||
|
|
d4fc305bfa | ||
|
|
cb3e4f29eb | ||
|
|
f4ee2a9d76 | ||
|
|
59b7671409 | ||
|
|
d7da1c3ae2 | ||
|
|
ffffc2db51 |
167
.github/workflows/e2e-tests.yml
vendored
167
.github/workflows/e2e-tests.yml
vendored
@@ -21,7 +21,10 @@ on:
|
||||
OP_SERVICE_ACCOUNT_TOKEN:
|
||||
required: true
|
||||
VAULT:
|
||||
description: "1Password vault name or UUID"
|
||||
description: "1Password vault name"
|
||||
required: true
|
||||
VAULT_ID:
|
||||
description: "1Password vault UUID"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
@@ -56,9 +59,27 @@ jobs:
|
||||
- name: Generate .env.tpl
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p tests
|
||||
echo "FILE_SECRET=op://${{ secrets.VAULT }}/test-secret/password" > tests/.env.tpl
|
||||
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT }}/test-secret/test-section/password" >> tests/.env.tpl
|
||||
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT }}/multiline-secret/notesPlain" >> tests/.env.tpl
|
||||
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT }}/file-secret/test.txt" >> tests/.env.tpl
|
||||
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT }}/file-secret/file section/test.txt" >> tests/.env.tpl
|
||||
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT }}/double-section-secret/test-section/password" >> tests/.env.tpl
|
||||
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
|
||||
|
||||
- name: Generate .vaultId_env.tpl
|
||||
shell: bash
|
||||
run: |
|
||||
echo "FILE_SECRET=op://${{ secrets.VAULT_ID }}/test-secret/password" > tests/.vaultId_env.tpl
|
||||
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT_ID }}/test-secret/test-section/password" >> tests/.vaultId_env.tpl
|
||||
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain" >> tests/.vaultId_env.tpl
|
||||
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT_ID }}/file-secret/test.txt" >> tests/.vaultId_env.tpl
|
||||
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt" >> tests/.vaultId_env.tpl
|
||||
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password" >> tests/.vaultId_env.tpl
|
||||
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key" >> tests/.vaultId_env.tpl
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.vaultId_env.tpl
|
||||
|
||||
- name: Configure Service account
|
||||
uses: ./configure
|
||||
@@ -75,6 +96,11 @@ jobs:
|
||||
SECRET: op://${{ secrets.VAULT }}/test-secret/password
|
||||
SECRET_IN_SECTION: op://${{ secrets.VAULT }}/test-secret/test-section/password
|
||||
MULTILINE_SECRET: op://${{ secrets.VAULT }}/multiline-secret/notesPlain
|
||||
SECRET_WITH_FILE: op://${{ secrets.VAULT }}/file-secret/test.txt
|
||||
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT }}/file-secret/file section/test.txt
|
||||
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT }}/double-section-secret/test-section/password
|
||||
SSH_PRIVATE_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key
|
||||
SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh
|
||||
OP_ENV_FILE: ./tests/.env.tpl
|
||||
|
||||
- name: Assert test secret values [step output]
|
||||
@@ -87,6 +113,11 @@ jobs:
|
||||
FILE_SECRET: ${{ steps.load_secrets.outputs.FILE_SECRET }}
|
||||
FILE_SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.FILE_SECRET_IN_SECTION }}
|
||||
FILE_MULTILINE_SECRET: ${{ steps.load_secrets.outputs.FILE_MULTILINE_SECRET }}
|
||||
SECRET_WITH_FILE: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE }}
|
||||
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE_IN_SECTION }}
|
||||
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets.outputs.DOUBLE_SECTION_SECRET }}
|
||||
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
|
||||
- name: Assert test secret values [exported env]
|
||||
@@ -94,6 +125,19 @@ jobs:
|
||||
shell: bash
|
||||
run: ./tests/assert-env-set.sh
|
||||
|
||||
- name: Assert SSH keys [step output]
|
||||
if: ${{ !matrix.export-env }}
|
||||
shell: bash
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
|
||||
run: ./tests/assert-ssh-keys.sh
|
||||
|
||||
- name: Assert SSH keys [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
shell: bash
|
||||
run: ./tests/assert-ssh-keys.sh
|
||||
|
||||
- name: Remove secrets [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
uses: ./
|
||||
@@ -111,7 +155,6 @@ jobs:
|
||||
uses: ./
|
||||
env:
|
||||
BAD_REF: "op://x"
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
with:
|
||||
export-env: true
|
||||
|
||||
@@ -121,6 +164,40 @@ jobs:
|
||||
env:
|
||||
STEP_OUTCOME: ${{ steps.load_invalid.outcome }}
|
||||
|
||||
- name: Load secrets by vault ID
|
||||
id: load_secrets_by_vault_id
|
||||
uses: ./
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
export-env: ${{ matrix.export-env }}
|
||||
env:
|
||||
SECRET: op://${{ secrets.VAULT_ID }}/test-secret/password
|
||||
SECRET_IN_SECTION: op://${{ secrets.VAULT_ID }}/test-secret/test-section/password
|
||||
MULTILINE_SECRET: op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain
|
||||
SECRET_WITH_FILE: op://${{ secrets.VAULT_ID }}/file-secret/test.txt
|
||||
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt
|
||||
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password
|
||||
SSH_PRIVATE_KEY: op://${{ secrets.VAULT_ID }}/test-ssh-key/private key
|
||||
SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh
|
||||
OP_ENV_FILE: ./tests/.vaultId_env.tpl
|
||||
|
||||
- name: Assert test secret values [vault by ID]
|
||||
if: ${{ !matrix.export-env }}
|
||||
shell: bash
|
||||
env:
|
||||
SECRET: ${{ steps.load_secrets_by_vault_id.outputs.SECRET }}
|
||||
SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_IN_SECTION }}
|
||||
MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.MULTILINE_SECRET }}
|
||||
FILE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET }}
|
||||
FILE_SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET_IN_SECTION }}
|
||||
FILE_MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_MULTILINE_SECRET }}
|
||||
SECRET_WITH_FILE: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE }}
|
||||
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE_IN_SECTION }}
|
||||
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.DOUBLE_SECTION_SECRET }}
|
||||
SSH_PRIVATE_KEY: ${{ steps.load_secrets_by_vault_id.outputs.SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets_by_vault_id.outputs.SSH_PRIVATE_KEY_OPENSSH }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
|
||||
test-connect:
|
||||
name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }})
|
||||
runs-on: ubuntu-latest
|
||||
@@ -155,13 +232,29 @@ jobs:
|
||||
echo "FILE_SECRET=op://${{ secrets.VAULT }}/test-secret/password" > tests/.env.tpl
|
||||
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT }}/test-secret/test-section/password" >> tests/.env.tpl
|
||||
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT }}/multiline-secret/notesPlain" >> tests/.env.tpl
|
||||
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT }}/file-secret/test.txt" >> tests/.env.tpl
|
||||
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT }}/file-secret/file section/test.txt" >> tests/.env.tpl
|
||||
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT }}/double-section-secret/test-section/password" >> tests/.env.tpl
|
||||
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl
|
||||
|
||||
- name: Generate .vaultId_env.tpl
|
||||
run: |
|
||||
echo "FILE_SECRET=op://${{ secrets.VAULT_ID }}/test-secret/password" > tests/.vaultId_env.tpl
|
||||
echo "FILE_SECRET_IN_SECTION=op://${{ secrets.VAULT_ID }}/test-secret/test-section/password" >> tests/.vaultId_env.tpl
|
||||
echo "FILE_MULTILINE_SECRET=op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain" >> tests/.vaultId_env.tpl
|
||||
echo "SECRET_WITH_FILE=op://${{ secrets.VAULT_ID }}/file-secret/test.txt" >> tests/.vaultId_env.tpl
|
||||
echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt" >> tests/.vaultId_env.tpl
|
||||
echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password" >> tests/.vaultId_env.tpl
|
||||
echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key" >> tests/.vaultId_env.tpl
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.vaultId_env.tpl
|
||||
|
||||
- name: Launch 1Password Connect instance
|
||||
env:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
run: |
|
||||
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
|
||||
docker compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
|
||||
docker compose -f tests/fixtures/docker-compose.yml up -d && sleep 30
|
||||
|
||||
- name: Configure 1Password Connect
|
||||
uses: ./configure
|
||||
@@ -179,6 +272,11 @@ jobs:
|
||||
SECRET: op://${{ secrets.VAULT }}/test-secret/password
|
||||
SECRET_IN_SECTION: op://${{ secrets.VAULT }}/test-secret/test-section/password
|
||||
MULTILINE_SECRET: op://${{ secrets.VAULT }}/multiline-secret/notesPlain
|
||||
SECRET_WITH_FILE: op://${{ secrets.VAULT }}/file-secret/test.txt
|
||||
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT }}/file-secret/file section/test.txt
|
||||
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT }}/double-section-secret/test-section/password
|
||||
SSH_PRIVATE_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key
|
||||
SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh
|
||||
OP_ENV_FILE: ./tests/.env.tpl
|
||||
|
||||
- name: Assert test secret values [step output]
|
||||
@@ -190,12 +288,30 @@ jobs:
|
||||
FILE_SECRET: ${{ steps.load_secrets.outputs.FILE_SECRET }}
|
||||
FILE_SECRET_IN_SECTION: ${{ steps.load_secrets.outputs.FILE_SECRET_IN_SECTION }}
|
||||
FILE_MULTILINE_SECRET: ${{ steps.load_secrets.outputs.FILE_MULTILINE_SECRET }}
|
||||
SECRET_WITH_FILE: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE }}
|
||||
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE_IN_SECTION }}
|
||||
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets.outputs.DOUBLE_SECTION_SECRET }}
|
||||
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
|
||||
- name: Assert test secret values [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
|
||||
- name: Assert SSH keys [step output]
|
||||
if: ${{ !matrix.export-env }}
|
||||
shell: bash
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }}
|
||||
SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }}
|
||||
run: ./tests/assert-ssh-keys.sh
|
||||
|
||||
- name: Assert SSH keys [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
shell: bash
|
||||
run: ./tests/assert-ssh-keys.sh
|
||||
|
||||
- name: Remove secrets [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
uses: ./
|
||||
@@ -205,3 +321,48 @@ jobs:
|
||||
- name: Assert removed secrets [exported env]
|
||||
if: ${{ matrix.export-env }}
|
||||
run: ./tests/assert-env-unset.sh
|
||||
|
||||
- name: Load secrets (invalid ref - expect failure)
|
||||
id: load_invalid
|
||||
continue-on-error: true
|
||||
uses: ./
|
||||
env:
|
||||
BAD_REF: "op://x"
|
||||
with:
|
||||
export-env: true
|
||||
|
||||
- name: Assert invalid ref failed
|
||||
shell: bash
|
||||
run: ./tests/assert-invalid-ref-failed.sh
|
||||
env:
|
||||
STEP_OUTCOME: ${{ steps.load_invalid.outcome }}
|
||||
|
||||
- name: Load secrets by vault ID
|
||||
id: load_secrets_by_vault_id
|
||||
uses: ./
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
export-env: ${{ matrix.export-env }}
|
||||
env:
|
||||
SECRET: op://${{ secrets.VAULT_ID }}/test-secret/password
|
||||
SECRET_IN_SECTION: op://${{ secrets.VAULT_ID }}/test-secret/test-section/password
|
||||
MULTILINE_SECRET: op://${{ secrets.VAULT_ID }}/multiline-secret/notesPlain
|
||||
SECRET_WITH_FILE: op://${{ secrets.VAULT_ID }}/file-secret/test.txt
|
||||
SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt
|
||||
DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password
|
||||
OP_ENV_FILE: ./tests/.vaultId_env.tpl
|
||||
|
||||
- name: Assert test secret values [vault by ID]
|
||||
if: ${{ !matrix.export-env }}
|
||||
shell: bash
|
||||
env:
|
||||
SECRET: ${{ steps.load_secrets_by_vault_id.outputs.SECRET }}
|
||||
SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_IN_SECTION }}
|
||||
MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.MULTILINE_SECRET }}
|
||||
FILE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET }}
|
||||
FILE_SECRET_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.FILE_SECRET_IN_SECTION }}
|
||||
FILE_MULTILINE_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.FILE_MULTILINE_SECRET }}
|
||||
SECRET_WITH_FILE: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE }}
|
||||
SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE_IN_SECTION }}
|
||||
DOUBLE_SECTION_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.DOUBLE_SECTION_SECRET }}
|
||||
run: ./tests/assert-env-set.sh
|
||||
|
||||
1
.github/workflows/test-e2e.yml
vendored
1
.github/workflows/test-e2e.yml
vendored
@@ -91,6 +91,7 @@ jobs:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
VAULT: ${{ secrets.VAULT }}
|
||||
VAULT_ID: ${{ secrets.VAULT_ID }}
|
||||
|
||||
# Post comment on fork PRs after /ok-to-test
|
||||
comment-pr:
|
||||
|
||||
@@ -17,7 +17,8 @@ This document explains how to run e2e tests locally using `act`.
|
||||
| Secret | Description |
|
||||
| -------------------------- | --------------------- |
|
||||
| `OP_SERVICE_ACCOUNT_TOKEN` | Service Account token |
|
||||
| `VAULT` | Vault name or UUID |
|
||||
| `VAULT` | Vault name |
|
||||
| `VAULT_ID` | Vault UUID |
|
||||
|
||||
## Building Before Testing
|
||||
|
||||
|
||||
289
package-lock.json
generated
289
package-lock.json
generated
@@ -9,18 +9,21 @@
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@1password/connect": "^1.4.2",
|
||||
"@1password/op-js": "^0.1.11",
|
||||
"@1password/sdk": "^0.4.0",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/tool-cache": "^2.0.2",
|
||||
"dotenv": "^17.2.2"
|
||||
"dotenv": "^17.2.2",
|
||||
"sshpk": "^1.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@1password/eslint-config": "^4.3.1",
|
||||
"@1password/prettier-config": "^1.2.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/sshpk": "^1.17.4",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"husky": "^9.0.11",
|
||||
"jest": "^29.7.0",
|
||||
@@ -29,6 +32,19 @@
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@1password/connect": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@1password/connect/-/connect-1.4.2.tgz",
|
||||
"integrity": "sha512-CxcDQIr76nloWwGWRrmz/U7DuU65WKrN/yarq45LrC3L6b/pC7bZyskvougadG32fRwBieLJX143lTI8T1bAtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"debug": "^4.4.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@1password/eslint-config": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@1password/eslint-config/-/eslint-config-4.3.1.tgz",
|
||||
@@ -1288,6 +1304,15 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1412,6 +1437,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sshpk": {
|
||||
"version": "1.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.17.4.tgz",
|
||||
"integrity": "sha512-5gI/7eJn6wmkuIuFY8JZJ1g5b30H9K5U5vKrvOuYu+hoZLb2xcVEgxhYZ2Vhbs0w/ACyzyfkJq0hQtBfSCugjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/asn1": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
@@ -1966,6 +2001,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||
@@ -1980,6 +2031,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@@ -2006,6 +2063,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -2149,6 +2217,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -2272,7 +2348,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -2517,6 +2592,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
@@ -2595,6 +2682,17 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
|
||||
@@ -2650,10 +2748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -2736,6 +2833,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
@@ -2799,7 +2905,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@@ -2810,6 +2915,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
|
||||
"dependencies": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
@@ -2943,7 +3057,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2953,7 +3066,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2990,7 +3102,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@@ -3000,15 +3111,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
|
||||
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
|
||||
"dev": true,
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.1"
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3841,6 +3952,26 @@
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@@ -3851,6 +3982,22 @@
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -3877,7 +4024,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -3950,7 +4096,6 @@
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
|
||||
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@@ -4012,6 +4157,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -4107,7 +4260,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4193,7 +4345,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4206,7 +4357,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -4222,7 +4372,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -5568,6 +5717,11 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
|
||||
},
|
||||
"node_modules/jsdoc-type-pratt-parser": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz",
|
||||
@@ -5953,6 +6107,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -6151,7 +6311,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
|
||||
"integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -6188,6 +6347,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
@@ -6238,7 +6418,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
@@ -6724,6 +6903,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -7038,6 +7223,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
@@ -7237,6 +7427,15 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/slugify": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
|
||||
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -7290,6 +7489,30 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
||||
"dependencies": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@@ -7758,6 +7981,11 @@
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -7968,6 +8196,19 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||
|
||||
@@ -42,16 +42,19 @@
|
||||
"dependencies": {
|
||||
"@1password/op-js": "^0.1.11",
|
||||
"@1password/sdk": "^0.4.0",
|
||||
"@1password/connect": "^1.4.2",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/tool-cache": "^2.0.2",
|
||||
"dotenv": "^17.2.2"
|
||||
"dotenv": "^17.2.2",
|
||||
"sshpk": "^1.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@1password/eslint-config": "^4.3.1",
|
||||
"@1password/prettier-config": "^1.2.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/sshpk": "^1.17.4",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"husky": "^9.0.11",
|
||||
"jest": "^29.7.0",
|
||||
|
||||
25
src/index.ts
25
src/index.ts
@@ -1,9 +1,7 @@
|
||||
import dotenv from "dotenv";
|
||||
import * as core from "@actions/core";
|
||||
import { validateCli } from "@1password/op-js";
|
||||
import { installCliOnGithubActionRunner } from "./op-cli-installer";
|
||||
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
|
||||
import { envFilePath, envConnectHost, envConnectToken } from "./constants";
|
||||
import { envFilePath } from "./constants";
|
||||
|
||||
const loadSecretsAction = async () => {
|
||||
try {
|
||||
@@ -26,13 +24,6 @@ const loadSecretsAction = async () => {
|
||||
dotenv.config({ path: file });
|
||||
}
|
||||
|
||||
const isConnect =
|
||||
process.env[envConnectHost] && process.env[envConnectToken];
|
||||
// If Connect is used, download and install the CLI
|
||||
if (isConnect) {
|
||||
await installCLI();
|
||||
}
|
||||
|
||||
// Load secrets
|
||||
await loadSecrets(shouldExportEnv);
|
||||
} catch (error) {
|
||||
@@ -43,22 +34,10 @@ const loadSecretsAction = async () => {
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else {
|
||||
String(error);
|
||||
message = String(error);
|
||||
}
|
||||
core.setFailed(message);
|
||||
}
|
||||
};
|
||||
|
||||
// This function's name is an exception from the naming convention
|
||||
// since we refer to the 1Password CLI here.
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const installCLI = async (): Promise<void> => {
|
||||
// validateCli checks if there's an existing 1Password CLI installed on the runner.
|
||||
// If there's no CLI installed, then validateCli will throw an error, which we will use
|
||||
// as an indicator that we need to execute the installation script.
|
||||
await validateCli().catch(async () => {
|
||||
await installCliOnGithubActionRunner();
|
||||
});
|
||||
};
|
||||
|
||||
void loadSecretsAction();
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import { read, setClientInfo } from "@1password/op-js";
|
||||
import { read } from "@1password/op-js";
|
||||
import { createClient, Secrets } from "@1password/sdk";
|
||||
import { OnePasswordConnect, FullItem } from "@1password/connect";
|
||||
import {
|
||||
extractSecret,
|
||||
loadSecrets,
|
||||
unsetPrevious,
|
||||
validateAuth,
|
||||
findMatchingFieldAndFile,
|
||||
findSectionIdsByQuery,
|
||||
parseOpRef,
|
||||
getEnvVarNamesWithSecretRefs,
|
||||
} from "./utils";
|
||||
import {
|
||||
authErr,
|
||||
@@ -30,6 +35,7 @@ jest.mock("@1password/sdk", () => ({
|
||||
validateSecretReference: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock("@1password/connect");
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -153,38 +159,225 @@ describe("extractSecret", () => {
|
||||
|
||||
describe("loadSecrets when using Connect", () => {
|
||||
beforeEach(() => {
|
||||
process.env[envConnectHost] = "https://localhost:8000";
|
||||
process.env[envConnectToken] = "token";
|
||||
process.env[envConnectHost] = "https://connect.example";
|
||||
process.env[envConnectToken] = "test-token";
|
||||
process.env[envServiceAccountToken] = "";
|
||||
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
if (
|
||||
typeof process.env[key] === "string" &&
|
||||
process.env[key]?.startsWith("op://")
|
||||
) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
process.env.MY_SECRET = "op://vault/item/field";
|
||||
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: jest.fn().mockResolvedValue({ id: "vault-id-123" }),
|
||||
getItem: jest.fn().mockResolvedValue({
|
||||
fields: [
|
||||
{ label: "field", value: "resolved-via-connect", section: undefined },
|
||||
],
|
||||
sections: [],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the client info and gets the executed output", async () => {
|
||||
it("resolves ref via Connect SDK and exports secret", async () => {
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(setClientInfo).toHaveBeenCalledWith({
|
||||
name: "1Password GitHub Action",
|
||||
id: "GHA",
|
||||
});
|
||||
expect(exec.getExecOutput).toHaveBeenCalledWith('sh -c "op env ls"');
|
||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||
"OP_MANAGED_VARIABLES",
|
||||
"MOCK_SECRET",
|
||||
"MY_SECRET",
|
||||
"resolved-via-connect",
|
||||
);
|
||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||
envManagedVariables,
|
||||
"MY_SECRET",
|
||||
);
|
||||
});
|
||||
|
||||
it("return early if no env vars with secrets found", async () => {
|
||||
(exec.getExecOutput as jest.Mock).mockReturnValueOnce({ stdout: "" });
|
||||
delete process.env.MY_SECRET;
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(exec.getExecOutput).toHaveBeenCalledWith('sh -c "op env ls"');
|
||||
expect(core.exportVariable).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets step output when shouldExportEnv is false", async () => {
|
||||
await loadSecrets(false);
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith(
|
||||
"MY_SECRET",
|
||||
"resolved-via-connect",
|
||||
);
|
||||
expect(core.exportVariable).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("masks resolved secret with setSecret", async () => {
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(core.setSecret).toHaveBeenCalledWith("resolved-via-connect");
|
||||
});
|
||||
|
||||
it("calls getVault with vault segment from ref", async () => {
|
||||
process.env.MY_SECRET = "op://my-vault-name/my-item/field";
|
||||
const mockGetVault = jest.fn().mockResolvedValue({ id: "vault-uuid" });
|
||||
const mockGetItem = jest.fn().mockResolvedValue({
|
||||
fields: [{ label: "field", value: "secret-value", section: undefined }],
|
||||
sections: [],
|
||||
});
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: mockGetItem,
|
||||
});
|
||||
|
||||
await loadSecrets(false);
|
||||
|
||||
expect(mockGetVault).toHaveBeenCalledWith("my-vault-name");
|
||||
});
|
||||
|
||||
it("throws when getVault returns vault without id", async () => {
|
||||
const mockGetVault = jest.fn().mockResolvedValue({});
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: jest.fn(),
|
||||
});
|
||||
|
||||
await expect(loadSecrets(true)).rejects.toThrow(
|
||||
/Could not find valid vault "vault" for ref "op:\/\/vault\/item\/field"/,
|
||||
);
|
||||
expect(mockGetVault).toHaveBeenCalledWith("vault");
|
||||
});
|
||||
|
||||
it("resolves vault by name and uses returned id for getItem", async () => {
|
||||
process.env.MY_SECRET = "op://My Vault/My Item/field";
|
||||
const mockGetVault = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: "uuid-for-my-vault" });
|
||||
const mockGetItem = jest.fn().mockResolvedValue({
|
||||
fields: [
|
||||
{
|
||||
label: "field",
|
||||
value: "secret-from-named-vault",
|
||||
section: undefined,
|
||||
},
|
||||
],
|
||||
sections: [],
|
||||
});
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: mockGetItem,
|
||||
});
|
||||
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(mockGetVault).toHaveBeenCalledWith("My Vault");
|
||||
expect(mockGetItem).toHaveBeenCalledWith("uuid-for-my-vault", "My Item");
|
||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||
"MY_SECRET",
|
||||
"secret-from-named-vault",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls getItem with vault id from getVault, not ref vault segment", async () => {
|
||||
const mockGetVault = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: "resolved-vault-id" });
|
||||
const mockGetItem = jest.fn().mockResolvedValue({
|
||||
fields: [
|
||||
{ label: "field", value: "resolved-via-connect", section: undefined },
|
||||
],
|
||||
sections: [],
|
||||
});
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: mockGetItem,
|
||||
});
|
||||
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(mockGetVault).toHaveBeenCalledWith("vault");
|
||||
expect(mockGetItem).toHaveBeenCalledWith("resolved-vault-id", "item");
|
||||
});
|
||||
|
||||
it("rejects when getItem fails", async () => {
|
||||
const mockGetVault = jest.fn().mockResolvedValue({ id: "vault-id-123" });
|
||||
const mockGetItem = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("Item not found"));
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: mockGetItem,
|
||||
});
|
||||
|
||||
await expect(loadSecrets(true)).rejects.toThrow("Item not found");
|
||||
});
|
||||
|
||||
it("resolves refs in different vaults using each vault id", async () => {
|
||||
delete process.env.MY_SECRET;
|
||||
process.env.SECRET_A = "op://vault-a/item1/field1";
|
||||
process.env.SECRET_B = "op://vault-b/item2/field2";
|
||||
const mockGetVault = jest
|
||||
.fn()
|
||||
.mockImplementation(async (vaultName: string) =>
|
||||
Promise.resolve({
|
||||
id: vaultName === "vault-a" ? "id-a" : "id-b",
|
||||
}),
|
||||
);
|
||||
const mockGetItem = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
fields: [{ label: "field1", value: "value-a", section: undefined }],
|
||||
sections: [],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
fields: [{ label: "field2", value: "value-b", section: undefined }],
|
||||
sections: [],
|
||||
});
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: mockGetItem,
|
||||
});
|
||||
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(mockGetVault).toHaveBeenCalledWith("vault-a");
|
||||
expect(mockGetVault).toHaveBeenCalledWith("vault-b");
|
||||
expect(mockGetItem).toHaveBeenNthCalledWith(1, "id-a", "item1");
|
||||
expect(mockGetItem).toHaveBeenNthCalledWith(2, "id-b", "item2");
|
||||
expect(core.exportVariable).toHaveBeenCalledWith("SECRET_A", "value-a");
|
||||
expect(core.exportVariable).toHaveBeenCalledWith("SECRET_B", "value-b");
|
||||
});
|
||||
|
||||
it("throws on invalid ref before calling Connect", async () => {
|
||||
delete process.env.MY_SECRET;
|
||||
process.env.BAD_REF = "op://x";
|
||||
const mockGetVault = jest.fn();
|
||||
const mockGetItem = jest.fn();
|
||||
(OnePasswordConnect as jest.Mock).mockReturnValue({
|
||||
getVault: mockGetVault,
|
||||
getItem: mockGetItem,
|
||||
});
|
||||
|
||||
await expect(loadSecrets(true)).rejects.toThrow(/invalid|reference/i);
|
||||
expect(mockGetVault).not.toHaveBeenCalled();
|
||||
expect(mockGetItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("core.exportVariable", () => {
|
||||
it("is called when shouldExportEnv is true", async () => {
|
||||
await loadSecrets(true);
|
||||
|
||||
expect(core.exportVariable).toHaveBeenCalledTimes(1);
|
||||
expect(core.exportVariable).toHaveBeenCalledTimes(2);
|
||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||
"MY_SECRET",
|
||||
"resolved-via-connect",
|
||||
);
|
||||
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||
envManagedVariables,
|
||||
"MY_SECRET",
|
||||
);
|
||||
});
|
||||
|
||||
it("is not called when shouldExportEnv is false", async () => {
|
||||
@@ -404,3 +597,360 @@ describe("unsetPrevious", () => {
|
||||
expect(core.exportVariable).toHaveBeenCalledWith("TEST_SECRET", "");
|
||||
});
|
||||
});
|
||||
|
||||
describe("findMatchingFieldAndFile", () => {
|
||||
interface TestField {
|
||||
id?: string;
|
||||
label?: string;
|
||||
value?: string | null;
|
||||
section?: { id: string } | null | undefined;
|
||||
}
|
||||
interface TestFile {
|
||||
id?: string;
|
||||
name?: string;
|
||||
section?: { id: string } | null | undefined;
|
||||
}
|
||||
|
||||
const item = (opts: { fields?: TestField[]; files?: TestFile[] }): FullItem =>
|
||||
({
|
||||
fields: opts.fields ?? [],
|
||||
files: opts.files ?? [],
|
||||
sections: [],
|
||||
}) as unknown as FullItem;
|
||||
|
||||
const find = (
|
||||
opts: { fields?: TestField[]; files?: TestFile[] },
|
||||
sectionIds: string[] = [],
|
||||
) => findMatchingFieldAndFile(item(opts), "password", sectionIds);
|
||||
|
||||
describe("when section filter is used (sectionIds.length > 0)", () => {
|
||||
it.each<{
|
||||
name: string;
|
||||
itemOpts: { fields?: TestField[]; files?: TestFile[] };
|
||||
expected: { fieldValue?: string; fileId?: string };
|
||||
}>([
|
||||
{
|
||||
name: "returns field value when one field matches query and is in ref sections",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{
|
||||
id: "f1",
|
||||
label: "password",
|
||||
value: "secret123",
|
||||
section: { id: "section-1" },
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: { fieldValue: "secret123" },
|
||||
},
|
||||
{
|
||||
name: "returns file id when one file matches query and is in ref sections",
|
||||
itemOpts: {
|
||||
files: [
|
||||
{
|
||||
id: "file-uuid",
|
||||
name: "password",
|
||||
section: { id: "section-1" },
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: { fileId: "file-uuid" },
|
||||
},
|
||||
{
|
||||
name: "returns empty object when no field or file matches",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{ label: "other", value: "x", section: { id: "section-1" } },
|
||||
],
|
||||
files: [],
|
||||
},
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
name: "returns field value when field matches by id",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{
|
||||
id: "password",
|
||||
label: "Password Label",
|
||||
value: "secret-by-id",
|
||||
section: { id: "section-1" },
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: { fieldValue: "secret-by-id" },
|
||||
},
|
||||
])("$name", ({ itemOpts, expected }) => {
|
||||
expect(find(itemOpts, ["section-1"])).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each<{
|
||||
name: string;
|
||||
itemOpts: { fields?: TestField[]; files?: TestFile[] };
|
||||
error: RegExp;
|
||||
}>([
|
||||
{
|
||||
name: "throws when multiple fields match",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{ label: "password", value: "a", section: { id: "section-1" } },
|
||||
{ label: "password", value: "b", section: { id: "section-1" } },
|
||||
],
|
||||
},
|
||||
error: /Multiple matches/,
|
||||
},
|
||||
{
|
||||
name: "throws when multiple files match",
|
||||
itemOpts: {
|
||||
files: [
|
||||
{ id: "id1", name: "password", section: { id: "section-1" } },
|
||||
{ id: "id2", name: "password", section: { id: "section-1" } },
|
||||
],
|
||||
},
|
||||
error: /Multiple matches/,
|
||||
},
|
||||
{
|
||||
name: "throws when both a field and a file match",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{ label: "password", value: "v", section: { id: "section-1" } },
|
||||
],
|
||||
files: [
|
||||
{ id: "fid", name: "password", section: { id: "section-1" } },
|
||||
],
|
||||
},
|
||||
error: /Both a field and a file match/,
|
||||
},
|
||||
{
|
||||
name: "throws when field has no value",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{ label: "password", value: null, section: { id: "section-1" } },
|
||||
],
|
||||
},
|
||||
error: /has no value/,
|
||||
},
|
||||
])("$name", ({ itemOpts, error }) => {
|
||||
expect(() => find(itemOpts, ["section-1"])).toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when no section filter (sectionIds.length === 0)", () => {
|
||||
const sectionIds: string[] = [];
|
||||
|
||||
it.each<{
|
||||
name: string;
|
||||
itemOpts: { fields?: TestField[]; files?: TestFile[] };
|
||||
expected: { fieldValue?: string; fileId?: string };
|
||||
}>([
|
||||
{
|
||||
name: "returns field value when one field has no section and matches query",
|
||||
itemOpts: {
|
||||
fields: [{ label: "password", value: "secret", section: undefined }],
|
||||
},
|
||||
expected: { fieldValue: "secret" },
|
||||
},
|
||||
{
|
||||
name: "returns file id when one file has no section and matches query",
|
||||
itemOpts: {
|
||||
files: [{ id: "file-id", name: "password", section: undefined }],
|
||||
},
|
||||
expected: { fileId: "file-id" },
|
||||
},
|
||||
{
|
||||
name: "returns field value from fallback (any section) when no field with no section matches",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{ label: "other", value: "x", section: undefined },
|
||||
{
|
||||
label: "password",
|
||||
value: "from-any-section",
|
||||
section: { id: "sec" },
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: { fieldValue: "from-any-section" },
|
||||
},
|
||||
{
|
||||
name: "returns file id from fallback (any section) when no file with no section matches",
|
||||
itemOpts: {
|
||||
files: [
|
||||
{ id: "other", name: "x", section: undefined },
|
||||
{ id: "file-any", name: "password", section: { id: "sec" } },
|
||||
],
|
||||
},
|
||||
expected: { fileId: "file-any" },
|
||||
},
|
||||
{
|
||||
name: "returns empty object when no match",
|
||||
itemOpts: {
|
||||
fields: [{ label: "other", value: "x", section: undefined }],
|
||||
files: [],
|
||||
},
|
||||
expected: {},
|
||||
},
|
||||
])("$name", ({ itemOpts, expected }) => {
|
||||
expect(find(itemOpts, sectionIds)).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each<{
|
||||
name: string;
|
||||
itemOpts: { fields?: TestField[]; files?: TestFile[] };
|
||||
error: RegExp;
|
||||
}>([
|
||||
{
|
||||
name: "throws when multiple fields with no section match",
|
||||
itemOpts: {
|
||||
fields: [
|
||||
{ label: "password", value: "a", section: undefined },
|
||||
{ label: "password", value: "b", section: undefined },
|
||||
],
|
||||
},
|
||||
error: /Multiple matches/,
|
||||
},
|
||||
{
|
||||
name: "throws when multiple files with no section match",
|
||||
itemOpts: {
|
||||
files: [
|
||||
{ id: "1", name: "password", section: undefined },
|
||||
{ id: "2", name: "password", section: undefined },
|
||||
],
|
||||
},
|
||||
error: /Multiple matches/,
|
||||
},
|
||||
{
|
||||
name: "throws when both field and file match",
|
||||
itemOpts: {
|
||||
fields: [{ label: "password", value: "value", section: undefined }],
|
||||
files: [{ id: "fid", name: "password", section: undefined }],
|
||||
},
|
||||
error: /Both a field and a file match/,
|
||||
},
|
||||
])("$name", ({ itemOpts, error }) => {
|
||||
expect(() => find(itemOpts, sectionIds)).toThrow(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findSectionIdsByQuery", () => {
|
||||
it("throws when sections is empty", () => {
|
||||
expect(() => findSectionIdsByQuery([], "section-1")).toThrow(
|
||||
/Item has no sections; cannot resolve section "section-1"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when sections is null/undefined", () => {
|
||||
expect(() =>
|
||||
findSectionIdsByQuery(undefined as unknown as FullItem["sections"], "x"),
|
||||
).toThrow(/Item has no sections; cannot resolve section "x"/);
|
||||
});
|
||||
|
||||
it("throws when section query matches no section", () => {
|
||||
const sections = [{ id: "sec-1", label: "Other" }];
|
||||
expect(() =>
|
||||
findSectionIdsByQuery(sections as FullItem["sections"], "nonexistent"),
|
||||
).toThrow(/No section matching "nonexistent" found in specified item/);
|
||||
});
|
||||
|
||||
it("returns section id when section matches by label", () => {
|
||||
const sections = [{ id: "sec-1", label: "My Section" }];
|
||||
expect(
|
||||
findSectionIdsByQuery(sections as FullItem["sections"], "My Section"),
|
||||
).toEqual(["sec-1"]);
|
||||
});
|
||||
|
||||
it("throws when section query matches no section", () => {
|
||||
const sections = [{ id: "sec-1", label: "Other" }];
|
||||
expect(() =>
|
||||
findSectionIdsByQuery(sections as FullItem["sections"], "nonexistent"),
|
||||
).toThrow(/No section matching "nonexistent" found in specified item/);
|
||||
});
|
||||
|
||||
it("returns multiple ids when multiple sections match", () => {
|
||||
const sections = [
|
||||
{ id: "sec-1", label: "A" },
|
||||
{ id: "sec-2", label: "A" },
|
||||
];
|
||||
expect(
|
||||
findSectionIdsByQuery(sections as FullItem["sections"], "A"),
|
||||
).toEqual(["sec-1", "sec-2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseOpRef", () => {
|
||||
it("parses 3-segment ref (vault/item/field)", () => {
|
||||
expect(parseOpRef("op://vault/item/field")).toEqual({
|
||||
vault: "vault",
|
||||
item: "item",
|
||||
field: "field",
|
||||
section: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses 4-segment ref (vault/item/section/field)", () => {
|
||||
expect(parseOpRef("op://vault/item/MySection/password")).toEqual({
|
||||
vault: "vault",
|
||||
item: "item",
|
||||
section: "MySection",
|
||||
field: "password",
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes URI-encoded segments", () => {
|
||||
expect(parseOpRef("op://my%20vault/my%20item/field")).toEqual({
|
||||
vault: "my vault",
|
||||
item: "my item",
|
||||
field: "field",
|
||||
section: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when ref does not start with op://", () => {
|
||||
expect(() => parseOpRef("invalid-ref")).toThrow(
|
||||
/Invalid op reference: invalid-ref/,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when segment count is invalid", () => {
|
||||
expect(() => parseOpRef("op://vault/item")).toThrow(
|
||||
/use op:\/\/<vault>\/<item>\/<field>/,
|
||||
);
|
||||
expect(() => parseOpRef("op://a/b/c/d/e")).toThrow(
|
||||
/use op:\/\/<vault>\/<item>\/<field>/,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when vault or item or field is empty", () => {
|
||||
expect(() => parseOpRef("op:///item/field")).toThrow(/vault is required/);
|
||||
expect(() => parseOpRef("op://vault//field")).toThrow(/item is required/);
|
||||
expect(() => parseOpRef("op://vault/item/")).toThrow(/field is required/);
|
||||
});
|
||||
|
||||
it("throws when 4-segment ref has empty section", () => {
|
||||
expect(() => parseOpRef("op://vault/item//field")).toThrow(
|
||||
/section is required when using 4 path segments/,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when last segment is empty (trailing slash)", () => {
|
||||
expect(() => parseOpRef("op://vault/item/field/")).toThrow(
|
||||
/field is required/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEnvVarNamesWithSecretRefs", () => {
|
||||
it("returns only env var names whose value is a string starting with op://", () => {
|
||||
process.env.OP_REF = "op://vault/item/field";
|
||||
process.env.NOT_OP_REF = "https://example.com";
|
||||
process.env.EMPTY_REF = "";
|
||||
process.env.OP_REF_OTHER = "op://other/vault/item/secret";
|
||||
|
||||
const result = getEnvVarNamesWithSecretRefs();
|
||||
|
||||
expect(result).toContain("OP_REF");
|
||||
expect(result).toContain("OP_REF_OTHER");
|
||||
expect(result).not.toContain("NOT_OP_REF");
|
||||
expect(result).not.toContain("EMPTY_REF");
|
||||
});
|
||||
});
|
||||
|
||||
415
src/utils.ts
415
src/utils.ts
@@ -1,7 +1,8 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import { read, setClientInfo, semverToInt } from "@1password/op-js";
|
||||
import sshpk from "sshpk";
|
||||
import { read } from "@1password/op-js";
|
||||
import { createClient, Secrets } from "@1password/sdk";
|
||||
import { OnePasswordConnect, FullItem, OPConnect } from "@1password/connect";
|
||||
import { version } from "../package.json";
|
||||
import {
|
||||
authErr,
|
||||
@@ -11,26 +12,287 @@ import {
|
||||
envManagedVariables,
|
||||
} from "./constants";
|
||||
|
||||
export const validateAuth = (): void => {
|
||||
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
|
||||
const isServiceAccount = process.env[envServiceAccountToken];
|
||||
// #region Op ref parsing
|
||||
interface ParsedOpRef {
|
||||
vault: string;
|
||||
item: string;
|
||||
section: string | undefined;
|
||||
field: string;
|
||||
|
||||
if (isConnect && isServiceAccount) {
|
||||
core.warning(
|
||||
"WARNING: Both service account and Connect credentials are provided. Connect credentials will take priority.",
|
||||
queryParams: string | undefined;
|
||||
}
|
||||
|
||||
export const parseOpRef = (ref: string): ParsedOpRef => {
|
||||
// Safety check: refs are validated by validateSecretRefs before this runs
|
||||
// this guards against parseOpRef being called directly with invalid input
|
||||
if (!ref.startsWith("op://")) {
|
||||
throw new Error(`Invalid op reference: ${ref}`);
|
||||
}
|
||||
|
||||
const segments = ref
|
||||
.slice("op://".length)
|
||||
.split("/")
|
||||
.map((s) => decodeURIComponent(s));
|
||||
|
||||
if (segments.length < 3 || segments.length > 4) {
|
||||
throw new Error(
|
||||
`Invalid op reference: use op://<vault>/<item>/<field> or op://<vault>/<item>/<section>/<field>. Got: ${ref}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isConnect && !isServiceAccount) {
|
||||
throw new Error(authErr);
|
||||
const vault = segments[0] ?? "";
|
||||
if (!vault) {
|
||||
throw new Error(`Invalid op reference: vault is required`);
|
||||
}
|
||||
|
||||
const authType = isConnect ? "Connect" : "Service account";
|
||||
const item = segments[1] ?? "";
|
||||
if (!item) {
|
||||
throw new Error(`Invalid op reference: item is required`);
|
||||
}
|
||||
|
||||
core.info(`Authenticated with ${authType}.`);
|
||||
// Last segment is the field; it may include a query string (e.g. "private key?ssh-format=openssh")
|
||||
const lastSegment = segments[segments.length - 1] ?? "";
|
||||
const [fieldPart, queryPart] = lastSegment.split("?");
|
||||
const field = fieldPart ?? "";
|
||||
if (!field) {
|
||||
throw new Error(`Invalid op reference: field is required`);
|
||||
}
|
||||
const queryParams = queryPart ?? undefined;
|
||||
|
||||
// Second to last segment is the section if it exists
|
||||
let section: string | undefined;
|
||||
if (segments.length === 4) {
|
||||
section = segments[2];
|
||||
if (!section) {
|
||||
throw new Error(
|
||||
`Invalid op reference: section is required when using 4 path segments`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vault,
|
||||
item,
|
||||
field,
|
||||
section,
|
||||
queryParams,
|
||||
};
|
||||
};
|
||||
// #endregion
|
||||
|
||||
// #region Connect item resolution
|
||||
const getSecretFromConnectItem = async (
|
||||
client: OPConnect,
|
||||
item: FullItem,
|
||||
parsed: ParsedOpRef,
|
||||
): Promise<string> => {
|
||||
const sectionIds = parsed.section
|
||||
? findSectionIdsByQuery(item.sections, parsed.section)
|
||||
: [];
|
||||
const { fieldValue, fileId } = findMatchingFieldAndFile(
|
||||
item,
|
||||
parsed.field,
|
||||
sectionIds,
|
||||
);
|
||||
|
||||
if (fieldValue !== undefined) {
|
||||
return fieldValue;
|
||||
}
|
||||
|
||||
if (fileId) {
|
||||
return getFileContentWithRetry(client, parsed.vault, parsed.item, fileId);
|
||||
}
|
||||
|
||||
if (parsed.section) {
|
||||
throw new Error(
|
||||
`could not find field or file ${parsed.field} in section ${parsed.section} on item ${parsed.item} in vault ${parsed.vault}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`could not find field or file ${parsed.field} on item ${parsed.item} in vault ${parsed.vault}`,
|
||||
);
|
||||
};
|
||||
|
||||
const getEnvVarNamesWithSecretRefs = (): string[] =>
|
||||
const getFileContentWithRetry = async (
|
||||
client: OPConnect,
|
||||
vaultId: string,
|
||||
itemId: string,
|
||||
fileId: string,
|
||||
): Promise<string> => {
|
||||
const maxAttempts = 3;
|
||||
const retryDelayMs = 2000;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await client.getFileContent(vaultId, itemId, fileId);
|
||||
} catch (err) {
|
||||
// Retry on 503 errors as this can happen on multiple secret fetches
|
||||
const is503 =
|
||||
err !== null &&
|
||||
typeof err === "object" &&
|
||||
(err as Record<string, unknown>).statusCode === 503;
|
||||
if (is503 && attempt < maxAttempts) {
|
||||
await new Promise((r) => setTimeout(r, retryDelayMs));
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const findSectionIdsByQuery = (
|
||||
sections: FullItem["sections"],
|
||||
sectionQuery: string | undefined,
|
||||
): string[] => {
|
||||
// If no sections were returned with the item throw an error
|
||||
if (!sections || sections.length === 0) {
|
||||
throw new Error(
|
||||
`Item has no sections; cannot resolve section "${sectionQuery}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const ids = sections
|
||||
.filter((s) => s.id === sectionQuery || s.label === sectionQuery)
|
||||
.flatMap((s) => (s.id ? [s.id] : []));
|
||||
|
||||
// If no sections were found with the given query throw an error
|
||||
if (ids.length === 0) {
|
||||
throw new Error(
|
||||
`No section matching "${sectionQuery}" found in specified item`,
|
||||
);
|
||||
}
|
||||
|
||||
return ids;
|
||||
};
|
||||
|
||||
export const findMatchingFieldAndFile = (
|
||||
item: FullItem,
|
||||
fieldOrFileQuery: string,
|
||||
sectionIds: string[],
|
||||
): { fieldValue?: string; fileId?: string } => {
|
||||
// Get the fields/files from the item and check if the ref has a section filter
|
||||
const fields = item.fields ?? [];
|
||||
const files = item.files ?? [];
|
||||
const sectionFilter = sectionIds.length > 0;
|
||||
|
||||
const fieldMatchesQuery = (f: (typeof fields)[0]) =>
|
||||
f.id === fieldOrFileQuery || f.label === fieldOrFileQuery;
|
||||
const fileMatchesQuery = (f: (typeof files)[0]) =>
|
||||
f.id === fieldOrFileQuery || f.name === fieldOrFileQuery;
|
||||
|
||||
let matchedField: (typeof fields)[0] | undefined;
|
||||
let matchedFile: (typeof files)[0] | undefined;
|
||||
|
||||
if (sectionFilter) {
|
||||
// If the ref has a section filter only accept matches inside the referenced sections
|
||||
const matchingFields = fields.filter((f) => {
|
||||
const sectionId = f.section?.id;
|
||||
const inRefSections =
|
||||
sectionId !== null &&
|
||||
sectionId !== undefined &&
|
||||
sectionIds.includes(sectionId);
|
||||
return fieldMatchesQuery(f) && inRefSections;
|
||||
});
|
||||
matchedField = findSingleMatch(matchingFields);
|
||||
|
||||
const matchingFiles = files.filter((f) => {
|
||||
const sectionId = f.section?.id;
|
||||
const inRefSections =
|
||||
sectionId !== null &&
|
||||
sectionId !== undefined &&
|
||||
sectionIds.includes(sectionId);
|
||||
return fileMatchesQuery(f) && inRefSections;
|
||||
});
|
||||
matchedFile = findSingleMatch(matchingFiles);
|
||||
} else {
|
||||
// If the ref has no section filter search for matches with no section
|
||||
const matchingFields = fields.filter((f) => {
|
||||
const hasNoSection =
|
||||
f.section?.id === null || f.section?.id === undefined;
|
||||
return fieldMatchesQuery(f) && hasNoSection;
|
||||
});
|
||||
matchedField = findSingleMatch(matchingFields);
|
||||
|
||||
// If no matches were found with no section, search for matches in any section
|
||||
if (!matchedField) {
|
||||
const matchingFieldsInAnySection = fields.filter(fieldMatchesQuery);
|
||||
matchedField = findSingleMatch(matchingFieldsInAnySection);
|
||||
}
|
||||
|
||||
const matchingFiles = files.filter((f) => {
|
||||
const hasNoSection =
|
||||
f.section?.id === null || f.section?.id === undefined;
|
||||
return fileMatchesQuery(f) && hasNoSection;
|
||||
});
|
||||
matchedFile = findSingleMatch(matchingFiles);
|
||||
|
||||
if (!matchedFile) {
|
||||
const matchingFilesInAnySection = files.filter(fileMatchesQuery);
|
||||
matchedFile = findSingleMatch(matchingFilesInAnySection);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedField && matchedFile) {
|
||||
throw new Error(
|
||||
`Both a field and a file match "${fieldOrFileQuery}". Rename one or use the ID in your op:// reference.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (matchedField) {
|
||||
if (matchedField.value === undefined || matchedField.value === null) {
|
||||
throw new Error(
|
||||
`field ${fieldOrFileQuery} has no value in specified item`,
|
||||
);
|
||||
}
|
||||
return { fieldValue: matchedField.value };
|
||||
}
|
||||
|
||||
if (matchedFile?.id) {
|
||||
return { fileId: matchedFile.id };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const findSingleMatch = <T>(matches: T[]): T | undefined => {
|
||||
if (matches.length > 1) {
|
||||
throw new Error(
|
||||
"Multiple matches found. Rename one or use an ID in your op:// reference.",
|
||||
);
|
||||
}
|
||||
return matches[0];
|
||||
};
|
||||
|
||||
const createConnectClient = (host: string, token: string): OPConnect => {
|
||||
try {
|
||||
return OnePasswordConnect({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
serverURL: host,
|
||||
token,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Connect authentication failed: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const toOpenSSH = (value: string, _queryParams: string | undefined): string => {
|
||||
try {
|
||||
const key = sshpk.parsePrivateKey(value, "auto");
|
||||
return key.toString("openssh");
|
||||
} catch {
|
||||
core.warning(
|
||||
`Failed to parse private key to OpenSSH, returning original value`,
|
||||
);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
// #endregion
|
||||
|
||||
// #region Shared helpers and auth
|
||||
export const getEnvVarNamesWithSecretRefs = (): string[] =>
|
||||
Object.keys(process.env).filter(
|
||||
(key) =>
|
||||
typeof process.env[key] === "string" &&
|
||||
@@ -80,6 +342,25 @@ const setResolvedSecret = (
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAuth = (): void => {
|
||||
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
|
||||
const isServiceAccount = process.env[envServiceAccountToken];
|
||||
|
||||
if (isConnect && isServiceAccount) {
|
||||
core.warning(
|
||||
"WARNING: Both service account and Connect credentials are provided. Connect credentials will take priority.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!isConnect && !isServiceAccount) {
|
||||
throw new Error(authErr);
|
||||
}
|
||||
|
||||
const authType = isConnect ? "Connect" : "Service account";
|
||||
|
||||
core.info(`Authenticated with ${authType}.`);
|
||||
};
|
||||
|
||||
export const extractSecret = (
|
||||
envName: string,
|
||||
shouldExportEnv: boolean,
|
||||
@@ -97,29 +378,93 @@ export const extractSecret = (
|
||||
setResolvedSecret(envName, secretValue, shouldExportEnv);
|
||||
};
|
||||
|
||||
// Connect loads secrets via the 1Password CLI
|
||||
export const unsetPrevious = (): void => {
|
||||
if (process.env[envManagedVariables]) {
|
||||
core.info("Unsetting previous values ...");
|
||||
const managedEnvs = process.env[envManagedVariables].split(",");
|
||||
for (const envName of managedEnvs) {
|
||||
core.info(`Unsetting ${envName}`);
|
||||
core.exportVariable(envName, "");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchVaultId = async (
|
||||
client: OPConnect,
|
||||
vaultQuery: string,
|
||||
ref: string,
|
||||
vaultIdCache: Map<string, string>,
|
||||
): Promise<string> => {
|
||||
// Check if the vault ID is already cached to avoid unnecessary API calls
|
||||
const cached = vaultIdCache.get(vaultQuery);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const vault = await client.getVault(vaultQuery);
|
||||
if (!vault.id) {
|
||||
throw new Error(
|
||||
`Could not find valid vault "${vaultQuery}" for ref "${ref}"`,
|
||||
);
|
||||
}
|
||||
|
||||
vaultIdCache.set(vaultQuery, vault.id);
|
||||
return vault.id;
|
||||
};
|
||||
// #endregion
|
||||
|
||||
// #region Load secrets
|
||||
// Connect loads secrets via the Connect JS SDK
|
||||
const loadSecretsViaConnect = async (
|
||||
shouldExportEnv: boolean,
|
||||
): Promise<void> => {
|
||||
setClientInfo({
|
||||
name: "1Password GitHub Action",
|
||||
id: "GHA",
|
||||
build: semverToInt(version),
|
||||
});
|
||||
|
||||
// Load secrets from environment variables using 1Password CLI.
|
||||
// Iterate over them to find 1Password references, extract the secret values,
|
||||
// and make them available in the next steps either as step outputs or as environment variables.
|
||||
const res = await exec.getExecOutput(`sh -c "op env ls"`);
|
||||
|
||||
if (res.stdout === "") {
|
||||
const envs = getEnvVarNamesWithSecretRefs();
|
||||
if (envs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const envs = res.stdout.replace(/\n+$/g, "").split(/\r?\n/);
|
||||
for (const envName of envs) {
|
||||
extractSecret(envName, shouldExportEnv);
|
||||
validateSecretRefs(envs);
|
||||
|
||||
const host = process.env[envConnectHost];
|
||||
const token = process.env[envConnectToken];
|
||||
if (!host || !token) {
|
||||
throw new Error(authErr);
|
||||
}
|
||||
|
||||
const client = createConnectClient(host, token);
|
||||
const vaultIdCache = new Map<string, string>();
|
||||
|
||||
for (const envName of envs) {
|
||||
const ref = process.env[envName];
|
||||
if (!ref) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the op ref and get the item from the Connect SDK
|
||||
const parsed = parseOpRef(ref);
|
||||
|
||||
const vaultId = await fetchVaultId(
|
||||
client,
|
||||
parsed.vault,
|
||||
ref,
|
||||
vaultIdCache,
|
||||
);
|
||||
const item = await client.getItem(vaultId, parsed.item);
|
||||
|
||||
// Get the secret value from the item as Connect returns a full item object
|
||||
const secretValue = await getSecretFromConnectItem(client, item, parsed);
|
||||
let valueToSet = secretValue;
|
||||
if (parsed.queryParams?.includes("ssh-format=openssh")) {
|
||||
valueToSet = toOpenSSH(secretValue, parsed.queryParams);
|
||||
}
|
||||
setResolvedSecret(envName, valueToSet, shouldExportEnv);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Failed to load ref "${ref}": ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldExportEnv) {
|
||||
core.exportVariable(envManagedVariables, envs.join());
|
||||
}
|
||||
@@ -181,14 +526,4 @@ export const loadSecrets = async (shouldExportEnv: boolean): Promise<void> => {
|
||||
|
||||
await loadSecretsViaServiceAccount(shouldExportEnv);
|
||||
};
|
||||
|
||||
export const unsetPrevious = (): void => {
|
||||
if (process.env[envManagedVariables]) {
|
||||
core.info("Unsetting previous values ...");
|
||||
const managedEnvs = process.env[envManagedVariables].split(",");
|
||||
for (const envName of managedEnvs) {
|
||||
core.info(`Unsetting ${envName}`);
|
||||
core.exportVariable(envName, "");
|
||||
}
|
||||
}
|
||||
};
|
||||
// #endregion
|
||||
|
||||
@@ -10,6 +10,9 @@ assert_env_equals() {
|
||||
}
|
||||
|
||||
readonly SECRET="RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||
readonly FILE_SECRET_CONTENT="This is a test"
|
||||
readonly DOUBLE_SECTION_SECRET_CONTENT="test-password"
|
||||
|
||||
MULTILINE_SECRET="$(cat << EOF
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLApXaGls
|
||||
@@ -35,3 +38,8 @@ assert_env_equals "FILE_SECRET_IN_SECTION" "${SECRET}"
|
||||
|
||||
assert_env_equals "MULTILINE_SECRET" "${MULTILINE_SECRET}"
|
||||
assert_env_equals "FILE_MULTILINE_SECRET" "${MULTILINE_SECRET}"
|
||||
|
||||
assert_env_equals "SECRET_WITH_FILE" "${FILE_SECRET_CONTENT}"
|
||||
assert_env_equals "SECRET_WITH_FILE_IN_SECTION" "${FILE_SECRET_CONTENT}"
|
||||
|
||||
assert_env_equals "DOUBLE_SECTION_SECRET" "${DOUBLE_SECTION_SECRET_CONTENT}"
|
||||
|
||||
@@ -17,3 +17,10 @@ assert_env_unset "FILE_SECRET_IN_SECTION"
|
||||
|
||||
assert_env_unset "MULTILINE_SECRET"
|
||||
assert_env_unset "FILE_MULTILINE_SECRET"
|
||||
|
||||
assert_env_unset "SECRET_WITH_FILE"
|
||||
assert_env_unset "SECRET_WITH_FILE_IN_SECTION"
|
||||
assert_env_unset "DOUBLE_SECTION_SECRET"
|
||||
|
||||
assert_env_unset "SSH_PRIVATE_KEY"
|
||||
assert_env_unset "SSH_PRIVATE_KEY_OPENSSH"
|
||||
|
||||
34
tests/assert-ssh-keys.sh
Executable file
34
tests/assert-ssh-keys.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# SSH_PRIVATE_KEY: any private key format
|
||||
v="$(printenv SSH_PRIVATE_KEY)"
|
||||
if [ -z "$v" ]; then
|
||||
echo "SSH_PRIVATE_KEY is not set"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$v" | head -1 | grep -qE -- '^-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then
|
||||
echo "SSH_PRIVATE_KEY does not start with a private key header"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$v" | tail -1 | grep -qE -- '-----END (RSA |EC |OPENSSH )?PRIVATE KEY-----$'; then
|
||||
echo "SSH_PRIVATE_KEY does not end with a private key footer"
|
||||
exit 1
|
||||
fi
|
||||
echo "SSH_PRIVATE_KEY has valid key format"
|
||||
|
||||
# SSH_PRIVATE_KEY_OPENSSH: OpenSSH format only
|
||||
v="$(printenv SSH_PRIVATE_KEY_OPENSSH)"
|
||||
if [ -z "$v" ]; then
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH is not set"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$v" | head -1 | grep -q -- '-----BEGIN OPENSSH PRIVATE KEY-----'; then
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH is not in OpenSSH format"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$v" | tail -1 | grep -q -- '-----END OPENSSH PRIVATE KEY-----$'; then
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH does not end with OpenSSH private key footer"
|
||||
exit 1
|
||||
fi
|
||||
echo "SSH_PRIVATE_KEY_OPENSSH has valid OpenSSH key format"
|
||||
Reference in New Issue
Block a user