From 033ed632d92ec82e0fe96e6282880c199f023f8c Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 22 Dec 2025 13:21:04 +1100 Subject: [PATCH 1/6] ci: add protectjs for CI secrets management Replace 1Password CLI with @cipherstash/protect (protectjs) for encrypting and decrypting CI secrets using ZeroKMS. - Add scripts/ directory with TypeScript encrypt/decrypt tooling - Add encrypted secrets file (.github/secrets.env.encrypted) - Update all workflow files to use protectjs decryption - Add scripts/node_modules/ to .gitignore Requires GitHub secrets: CS_VAULT_CLIENT_KEY, CS_VAULT_CLIENT_ACCESS_KEY --- .github/secrets.env.encrypted | 170 ++++ .github/workflows/benchmark.yml | 33 +- .github/workflows/release-aws-marketplace.yml | 18 +- .github/workflows/release.yml | 45 +- .github/workflows/test.yml | 37 +- .gitignore | 6 + scripts/decrypt-secrets.ts | 55 ++ scripts/encrypt-secrets.ts | 46 ++ scripts/package-lock.json | 758 ++++++++++++++++++ scripts/package.json | 19 + scripts/tsconfig.json | 13 + 11 files changed, 1165 insertions(+), 35 deletions(-) create mode 100644 .github/secrets.env.encrypted create mode 100644 scripts/decrypt-secrets.ts create mode 100644 scripts/encrypt-secrets.ts create mode 100644 scripts/package-lock.json create mode 100644 scripts/package.json create mode 100644 scripts/tsconfig.json diff --git a/.github/secrets.env.encrypted b/.github/secrets.env.encrypted new file mode 100644 index 00000000..fd7712fa --- /dev/null +++ b/.github/secrets.env.encrypted @@ -0,0 +1,170 @@ +{ + "CS_CLIENT_ACCESS_KEY": { + "k": "ct", + "c": "mBbLSVoU8`Do{H^4JdEH-DQ=;S0?!jXuj1GO<~nS77f6pGpQlROf9T!OWofsjl^#8A)M`97s6*4d0;YqiP5Q}aMyTySjc4-4673@C8B(NM?d)d7XuPf8JI)0vuk!)IKt`S^%ik$#2^`^r9+Nxb_`1Qo*(n?oS^3v|KZyn5qEtOAqksWikYQ$VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_CLIENT_ID": { + "k": "ct", + "c": "mBbLxa?ahrJW29+2tZu4UG$>FHmFEjiG510=YVqATlXu#AZ-+z)vGg7>@rO#&?Nyn#PyM+c#ggbfg(pF{O54Y;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_CLIENT_KEY": { + "k": "ct", + "c": "mBbL8G4ax6h3T~Q?tDDf%8k~=0b-Ynl7J`f94zJ0Z;L9{dMuI!X+A>=g&*voOs>;NFtYQ<31*#vAI^rYUd(y>*0hkz{!b}eF^i`hbwxLEL_MYql(SEdej(3Y6`O$)10zct`!1(oaD2`A94cnf+8)Z+7SNee62O1DW2PGR<(>^3lWbgXMzq6XlbvZrg@l=cyD~(CNWUo((hawMLjb>x=PE#2{IEcyVS|$jr{D@^!2|R>_;wluEZeSgL3m3dI-49kfKlVJ2oJ>=6Nj?KZ~>Z$R|4I8P{6Im?F", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_DEFAULT_KEYSET_ID": { + "k": "ct", + "c": "mBbJss<&W}*cPMX<|8MC6Jz1THhfz?pMTD^N5K+iXDom=eL~KfZiZvZ?BX~^Cf)U7IxtS494>ot;NV9~V5`T>A085uF9AWsAR|;F|L`fBbduc_c0%;e8FE}^9C+I-6&+JSH%G}TPNjBXY;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_TENANT_KEYSET_ID_1": { + "k": "ct", + "c": "mBbLxyC6xC+{EW;;5A`|u`KPxHUzKScK`B&^Jy}8?+{AvGOt<>3Ysz%{Twa+L!gXjsy63HTwS%&1712T=;=KzS_v)L5cf62AYUC~xJMlYv^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_TENANT_KEYSET_ID_2": { + "k": "ct", + "c": "mBbMJ`*UJyfuK2WYIx}JeqCO~Ha*-O?bCvj0H4BWR!I{IP+g&84Z`0c?^?NS3pgN;u~y8WK){H*3!ky3h!n?FzrMcGlAh+oAZuER+rp|(n*Dxg^gF{wpz@sto!>QMQm6+&?wJE6GNpE5Y;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_TENANT_KEYSET_ID_3": { + "k": "ct", + "c": "mBbLNuQ68uYA@n#p0N1{dBu*zHdE`M%fbp1-_?W4`AEXO*zc-@=O9>^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_TENANT_KEYSET_NAME_1": { + "k": "ct", + "c": "mBbJ+K&~zqj~hA_s_brbV0D|s7Xp~0205UCF^xN;Ns7e=m`RWayps}t#2`?P9j09-zvbqOuavY5bnQx4PA1>4T;p_tY()ytR_vvAVQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_TENANT_KEYSET_NAME_2": { + "k": "ct", + "c": "mBbL}WAbeTps@Mb2F2-O!$sZ17t=Cd3lu>!8?)(EBu0T<4=Esv8cRfl#2{-@)J9now;&lMqA$iiHUU)}y)=fkK>Ciy^P!vSvNxr6VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "CS_TENANT_KEYSET_NAME_3": { + "k": "ct", + "c": "mBbJwjyTKdTw8=TdyJ9#EDfl{7w{KlGGfJHd6wn21L&_@lHCm207NnPl&O{i{{7b!RG7K=s5RjVeiDiAPtDArSLa5ZE?t)<`)rh0?Hop1JJl`6(R?rFLO#b!Eg5G+h19c5guRv^Y;FRyoUu", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "DOCKER_HUB_PASSWORD": { + "k": "ct", + "c": "mBbK*YbGjRbIEUJ;l7IpaEQl3416=!tEKyAXSugKRW6Htia|^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + }, + "MULTITUDES_ACCESS_TOKEN": { + "k": "ct", + "c": "mBbKHz&;-To())kJ?nL;PvuU<0e1LJPzQ$#7Ug-%n=uXD(aTAjP4{L_z#E(X;IhH9QC4Ym4b`KXe?Y)HqcJ>qx1<#@>U9Bta5E7qM~Hl4%fGFNuIo^b_ng2^0#=te%aZNxV53-uVKMgDk;~FQGNZ(=~w=mWqVKzEI57qi`6Q)&Rdk68I(64_I^sI#30&bs!?e8=@s6hiH|SuM<^&qZPXfHPmcrvuaUD*Be11*VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 + } +} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 14f995fe..cd98d8b4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -24,24 +24,39 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-test + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: scripts/package-lock.json + + - name: Install decrypt dependencies + working-directory: scripts + run: npm ci + + - name: Decrypt secrets + env: + CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} + CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + run: cd scripts && npm run decrypt + - run: | mise run postgres:up --extra-args "--detach --wait" + - name: Run benchmark working-directory: tests/benchmark env: - CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} - CS_DEFAULT_KEYSET_ID: ${{ secrets.CS_DEFAULT_KEYSET_ID }} - CS_CLIENT_ID: ${{ secrets.CS_CLIENT_ID }} - CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} - CS_WORKSPACE_CRN: ${{ secrets.CS_WORKSPACE_CRN }} RUST_BACKTRACE: "1" run: mise run benchmark:continuous + # Download previous benchmark result from cache (if exists) - name: Download previous benchmark data uses: actions/cache@v4 with: path: ./cache key: ${{ runner.os }}-benchmark + # Run `github-action-benchmark` action - name: Store benchmark result uses: benchmark-action/github-action-benchmark@v1 @@ -56,10 +71,4 @@ jobs: comment-on-alert: true summary-always: true auto-push: true - benchmark-data-dir-path: docs - - - uses: ./.github/actions/send-slack-notification - with: - channel: engineering - webhook_url: ${{ secrets.SLACK_NOTIFICATION_WEBHOOK_URL }} - + benchmark-data-dir-path: docs \ No newline at end of file diff --git a/.github/workflows/release-aws-marketplace.yml b/.github/workflows/release-aws-marketplace.yml index 396e99e5..e716a043 100644 --- a/.github/workflows/release-aws-marketplace.yml +++ b/.github/workflows/release-aws-marketplace.yml @@ -82,6 +82,22 @@ jobs: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: scripts/package-lock.json + + - name: Install decrypt dependencies + working-directory: scripts + run: npm ci + + - name: Decrypt secrets + env: + CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} + CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + run: cd scripts && npm run decrypt + - uses: jdx/mise-action@v2 with: version: 2025.1.6 # [default: latest] mise version to install @@ -111,6 +127,6 @@ jobs: --fail-with-body \ --url "https://api.developer.multitudes.co/deployments" \ --header "Content-Type: application/json" \ - --header "Authorization: ${{ secrets.MULTITUDES_ACCESS_TOKEN }}" \ + --header "Authorization: ${{ env.MULTITUDES_ACCESS_TOKEN }}" \ --data '{"commitSha": "${{ github.sha }}", "environmentName":"marketplace"}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60bf72ca..7d3188d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,23 @@ jobs: runs-on: ${{matrix.build.os}} steps: - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: scripts/package-lock.json + + - name: Install decrypt dependencies + working-directory: scripts + run: npm ci + + - name: Decrypt secrets + env: + CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} + CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + run: cd scripts && npm run decrypt + - name: Setup Rust cache uses: Swatinem/rust-cache@v2 if: github.event_name == 'pull_request' # only cache in pull requests @@ -55,8 +72,8 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PERSONAL_ACCESS_TOKEN }} + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ env.DOCKER_HUB_PASSWORD }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -92,6 +109,24 @@ jobs: needs: - build steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: scripts/package-lock.json + + - name: Install decrypt dependencies + working-directory: scripts + run: npm ci + + - name: Decrypt secrets + env: + CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} + CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + run: cd scripts && npm run decrypt + - name: Download digests uses: actions/download-artifact@v4 with: @@ -102,8 +137,8 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PERSONAL_ACCESS_TOKEN }} + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ env.DOCKER_HUB_PASSWORD }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -135,5 +170,5 @@ jobs: --fail-with-body \ --url "https://api.developer.multitudes.co/deployments" \ --header "Content-Type: application/json" \ - --header "Authorization: ${{ secrets.MULTITUDES_ACCESS_TOKEN }}" \ + --header "Authorization: ${{ env.MULTITUDES_ACCESS_TOKEN }}" \ --data '{"commitSha": "${{ github.sha }}", "environmentName":"dockerhub"}' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f714b57e..6c23a048 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,28 +18,31 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-test + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: scripts/package-lock.json + + - name: Install decrypt dependencies + working-directory: scripts + run: npm ci + + - name: Decrypt secrets + env: + CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} + CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + run: cd scripts && npm run decrypt + - run: | mise run postgres:up --extra-args "--detach --wait" - - env: + + - name: Run tests + env: # REMEMBER TO ADD ENVIRONMENT VARIABLES TO tests/docker-compose.yml # The tests/docker-compose.yml config passes the ENV vars into the container - CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} - CS_DEFAULT_KEYSET_ID: ${{ secrets.CS_DEFAULT_KEYSET_ID }} - CS_TENANT_KEYSET_ID_1: ${{ secrets.CS_TENANT_KEYSET_ID_1 }} - CS_TENANT_KEYSET_ID_2: ${{ secrets.CS_TENANT_KEYSET_ID_2 }} - CS_TENANT_KEYSET_ID_3: ${{ secrets.CS_TENANT_KEYSET_ID_3 }} - CS_TENANT_KEYSET_NAME_1: ${{ secrets.CS_TENANT_KEYSET_NAME_1 }} - CS_TENANT_KEYSET_NAME_2: ${{ secrets.CS_TENANT_KEYSET_NAME_2 }} - CS_TENANT_KEYSET_NAME_3: ${{ secrets.CS_TENANT_KEYSET_NAME_3 }} - CS_CLIENT_ID: ${{ secrets.CS_CLIENT_ID }} - CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} - CS_WORKSPACE_CRN: ${{ secrets.CS_WORKSPACE_CRN }} RUST_BACKTRACE: "1" run: | mise run --output prefix test - - uses: ./.github/actions/send-slack-notification - with: - channel: engineering - webhook_url: ${{ secrets.SLACK_NOTIFICATION_WEBHOOK_URL }} - diff --git a/.gitignore b/.gitignore index 3f7448bf..4317065e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,12 @@ rust-toolchain.toml # credentials for local dev .env.proxy.docker +# CI secrets plaintext (never commit actual values) +.github/secrets.env.plaintext + +# Node modules (scripts directory) +scripts/node_modules/ + ## benchmark result data tests/benchmark/results/*.csv tests/benchmark/benchmark-*.png diff --git a/scripts/decrypt-secrets.ts b/scripts/decrypt-secrets.ts new file mode 100644 index 00000000..7d5bad33 --- /dev/null +++ b/scripts/decrypt-secrets.ts @@ -0,0 +1,55 @@ +import { protect, csTable, csColumn, Encrypted } from "@cipherstash/protect"; +import * as fs from "fs"; +import * as path from "path"; + +const schema = csTable("ci_secrets", { + value: csColumn("value"), +}); + +type EncryptedSecrets = Record; + +async function main(): Promise { + // Find .env.encrypted relative to repo root (one level up from scripts/) + const repoRoot = path.resolve(import.meta.dirname, ".."); + const encryptedPath = path.join(repoRoot, ".github", "secrets.env.encrypted"); + + if (!fs.existsSync(encryptedPath)) { + console.error(`Error: ${encryptedPath} not found`); + process.exit(1); + } + + const client = await protect({ schemas: [schema] }); + const encrypted: EncryptedSecrets = JSON.parse( + fs.readFileSync(encryptedPath, "utf-8") + ); + + const githubEnvPath = process.env.GITHUB_ENV; + const isCI = !!githubEnvPath; + + for (const [key, payload] of Object.entries(encrypted)) { + const result = await client.decrypt(payload); + if (result.failure) { + console.error(`Failed to decrypt ${key}: ${result.failure.message}`); + process.exit(1); + } + const value = String(result.data); + + if (isCI) { + // GitHub Actions: use heredoc syntax for multiline values + const delimiter = `EOF_${key}_${Date.now()}`; + fs.appendFileSync(githubEnvPath, `${key}<<${delimiter}\n${value}\n${delimiter}\n`); + } else { + // Local: simple KEY=value output (for testing) + console.log(`${key}=${value}`); + } + } + + if (isCI) { + console.error(`Decrypted ${Object.keys(encrypted).length} secrets to $GITHUB_ENV`); + } +} + +main().catch((err) => { + console.error("Decryption failed:", err); + process.exit(1); +}); diff --git a/scripts/encrypt-secrets.ts b/scripts/encrypt-secrets.ts new file mode 100644 index 00000000..a7be246f --- /dev/null +++ b/scripts/encrypt-secrets.ts @@ -0,0 +1,46 @@ +import { protect, csTable, csColumn } from "@cipherstash/protect"; +import * as fs from "fs"; +import * as path from "path"; +import * as dotenv from "dotenv"; + +const schema = csTable("ci_secrets", { + value: csColumn("value"), +}); + +async function main(): Promise { + const repoRoot = path.resolve(import.meta.dirname, ".."); + const plaintextPath = path.join(repoRoot, ".github", "secrets.env.plaintext"); + const encryptedPath = path.join(repoRoot, ".github", "secrets.env.encrypted"); + + if (!fs.existsSync(plaintextPath)) { + console.error(`Error: ${plaintextPath} not found`); + console.error("Create this file with your plaintext secrets (KEY=value format)"); + process.exit(1); + } + + const env = dotenv.parse(fs.readFileSync(plaintextPath)); + const client = await protect({ schemas: [schema] }); + + const encrypted: Record = {}; + + for (const [key, value] of Object.entries(env)) { + const result = await client.encrypt(value, { + table: schema, + column: schema.value, + }); + if (result.failure) { + console.error(`Failed to encrypt ${key}: ${result.failure.message}`); + process.exit(1); + } + encrypted[key] = result.data; + console.error(`Encrypted: ${key}`); + } + + fs.writeFileSync(encryptedPath, JSON.stringify(encrypted, null, 2) + "\n"); + console.error(`\nWrote ${Object.keys(encrypted).length} secrets to ${encryptedPath}`); +} + +main().catch((err) => { + console.error("Encryption failed:", err); + process.exit(1); +}); diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 00000000..afc74c84 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,758 @@ +{ + "name": "ci-secrets", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ci-secrets", + "version": "1.0.0", + "dependencies": { + "@cipherstash/protect": "^10.2.0", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@byteslice/result": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@byteslice/result/-/result-0.2.2.tgz", + "integrity": "sha512-dYAFK4SYj57r5WXB5ASrEztoTifMztFm8VTXDZLEgXTQ6lf39CtJPYCUrbugOeUoleqUr0vj7fqolVTShJFv/g==" + }, + "node_modules/@cipherstash/protect": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@cipherstash/protect/-/protect-10.2.0.tgz", + "integrity": "sha512-zYLQ9WBZiAz30yZS8SMGEpr9WlpSrI/37lXTIQYCndPrOyQIEkjX1OM4+YuQ4N17R4eTYPceY9UCfbyoB8PKvg==", + "license": "MIT", + "dependencies": { + "@byteslice/result": "^0.2.0", + "@cipherstash/protect-ffi": "0.18.1", + "@cipherstash/schema": "2.0.1", + "zod": "^3.24.2" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.24.0" + } + }, + "node_modules/@cipherstash/protect-ffi": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi/-/protect-ffi-0.18.1.tgz", + "integrity": "sha512-rDYIRIWo7EvJ+ytGEwzeb0D/4j3tQ8w+5Kpz089IEhxIVxOzavsR+f5HyMXohAKYkH+19RpD+2X+v1P8rwmiUA==", + "license": "ISC", + "dependencies": { + "@neon-rs/load": "^0.1.82" + }, + "optionalDependencies": { + "@cipherstash/protect-ffi-darwin-arm64": "0.18.1", + "@cipherstash/protect-ffi-darwin-x64": "0.18.1", + "@cipherstash/protect-ffi-linux-arm64-gnu": "0.18.1", + "@cipherstash/protect-ffi-linux-x64-gnu": "0.18.1", + "@cipherstash/protect-ffi-linux-x64-musl": "0.18.1", + "@cipherstash/protect-ffi-win32-x64-msvc": "0.18.1" + } + }, + "node_modules/@cipherstash/protect-ffi-darwin-arm64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi-darwin-arm64/-/protect-ffi-darwin-arm64-0.18.1.tgz", + "integrity": "sha512-kTkshWuGG07X9m4Ug0mPXIglfZopVFCJy55/B2yy9Z+hhNP95d4Zjq6AXlRYAHsnxgcp0SnynC8LaVTcXsEx8w==", + "cpu": [ + "arm64" + ], + "license": "ISC", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cipherstash/protect-ffi-darwin-x64": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi-darwin-x64/-/protect-ffi-darwin-x64-0.18.1.tgz", + "integrity": "sha512-VdoTOIF5hmS7sQUR/w6FQKAj7aJOhOD2lo2wlV3hxG0PNd2pWiB1HvE9wvrGY2VAPqBU97Cp+S8IFkf5ckqQ2A==", + "cpu": [ + "x64" + ], + "license": "ISC", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cipherstash/protect-ffi-linux-arm64-gnu": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi-linux-arm64-gnu/-/protect-ffi-linux-arm64-gnu-0.18.1.tgz", + "integrity": "sha512-DcGIgoTkRDaBqqXs3X4fB7XCOMtleSLd+kMAStS1uEq6agVHp2P/9Y+ag30MQ7Ypm0xTJBZVwl8ClDXDSklQcw==", + "cpu": [ + "arm64" + ], + "license": "ISC", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cipherstash/protect-ffi-linux-x64-gnu": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi-linux-x64-gnu/-/protect-ffi-linux-x64-gnu-0.18.1.tgz", + "integrity": "sha512-I5Cg7vlkVX2qL3hKYiYEx1C1lTQ+uGt2YrAxaqSs09gpvpa6xC2/2QMpV9UvKnJ9uLlxVly8jycnzp7TyvS9pA==", + "cpu": [ + "x64" + ], + "license": "ISC", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cipherstash/protect-ffi-linux-x64-musl": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi-linux-x64-musl/-/protect-ffi-linux-x64-musl-0.18.1.tgz", + "integrity": "sha512-Ol9dYVNXIaoYrhpsJv90PX5pHg1mmWKqnpnhh2ofA+NNZqEAdolo4BeTsQc10sCE6WogcM8FFJ9OcOrk79N8lA==", + "cpu": [ + "x64" + ], + "license": "ISC", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cipherstash/protect-ffi-win32-x64-msvc": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@cipherstash/protect-ffi-win32-x64-msvc/-/protect-ffi-win32-x64-msvc-0.18.1.tgz", + "integrity": "sha512-j9mqXQDHu3vdLJx93g2lAuKDeOh+XuC1FBvsrr2FTjgzBqrZAHH60QFPXjvFT1/U1oWjV8nAgTZQ4cUMJSDa9Q==", + "cpu": [ + "x64" + ], + "license": "ISC", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@cipherstash/schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cipherstash/schema/-/schema-2.0.1.tgz", + "integrity": "sha512-L7NQZFQd5fgkqrBUOp8pHYn6OgM/lwoes9QMquOV6V92XxaRZQO2P5YckmPETNoLxBI/rPXdR5T2KqEwOao4rA==", + "license": "MIT", + "dependencies": { + "zod": "^3.24.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@neon-rs/load": { + "version": "0.1.82", + "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.1.82.tgz", + "integrity": "sha512-H4Gu2o5kPp+JOEhRrOQCnJnf7X6sv9FBLttM/wSbb4efsgFWeHzfU/ItZ01E5qqEk+U6QGdeVO7lxXIAtYHr5A==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 00000000..bf023b1f --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "ci-secrets", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "encrypt": "tsx encrypt-secrets.ts", + "decrypt": "tsx decrypt-secrets.ts" + }, + "dependencies": { + "@cipherstash/protect": "^10.2.0", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 00000000..0507bef5 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["*.ts"] +} From f5315799bfddb8a716ae71566b053e9900df931f Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 22 Dec 2025 13:22:44 +1100 Subject: [PATCH 2/6] chore: add CS_WORKSPACE_CRN --- .github/secrets.env.encrypted | 68 +++---------------- .github/workflows/benchmark.yml | 2 + .github/workflows/release-aws-marketplace.yml | 2 + .github/workflows/release.yml | 2 + .github/workflows/test.yml | 2 + 5 files changed, 18 insertions(+), 58 deletions(-) diff --git a/.github/secrets.env.encrypted b/.github/secrets.env.encrypted index fd7712fa..7637e4d7 100644 --- a/.github/secrets.env.encrypted +++ b/.github/secrets.env.encrypted @@ -1,55 +1,7 @@ { - "CS_CLIENT_ACCESS_KEY": { - "k": "ct", - "c": "mBbLSVoU8`Do{H^4JdEH-DQ=;S0?!jXuj1GO<~nS77f6pGpQlROf9T!OWofsjl^#8A)M`97s6*4d0;YqiP5Q}aMyTySjc4-4673@C8B(NM?d)d7XuPf8JI)0vuk!)IKt`S^%ik$#2^`^r9+Nxb_`1Qo*(n?oS^3v|KZyn5qEtOAqksWikYQ$VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_CLIENT_ID": { - "k": "ct", - "c": "mBbLxa?ahrJW29+2tZu4UG$>FHmFEjiG510=YVqATlXu#AZ-+z)vGg7>@rO#&?Nyn#PyM+c#ggbfg(pF{O54Y;|SC5Hwu<&vtJ>^t3onC{{VkhX", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_CLIENT_KEY": { - "k": "ct", - "c": "mBbL8G4ax6h3T~Q?tDDf%8k~=0b-Ynl7J`f94zJ0Z;L9{dMuI!X+A>=g&*voOs>;NFtYQ<31*#vAI^rYUd(y>*0hkz{!b}eF^i`hbwxLEL_MYql(SEdej(3Y6`O$)10zct`!1(oaD2`A94cnf+8)Z+7SNee62O1DW2PGR<(>^3lWbgXMzq6XlbvZrg@l=cyD~(CNWUo((hawMLjb>x=PE#2{IEcyVS|$jr{D@^!2|R>_;wluEZeSgL3m3dI-49kfKlVJ2oJ>=6Nj?KZ~>Z$R|4I8P{6Im?F", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, "CS_DEFAULT_KEYSET_ID": { "k": "ct", - "c": "mBbJss<&W}*cPMX<|8MC6Jz1THhfz?pMTD^N5K+iXDom=eL~KfZiZvZ?BX~^Cf)U7IxtS494>ot;NV9~V5`T>A085uF9AWsAR|;F|L`fBbduc_c0%;e8FE}^9C+I-6&+JSH%G}TPNjBXY;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "c": "mBbM0?Nl*Qu~k~!?IMwm(qwtWHu>HeEmn8T4a(l7Q2ep~@2y2p^Y1vQS{!GRU-)lG{ysSLcEnRYnH3F}iR6+(smAqKtl@XWAfLV&!nG*uPP}T~-r&G*#J3lNz%2t5ZP7a`*ZI=R*`;=2Y;|SC5Hwu<&vtJ>^t3onC{{VkhX", "ob": null, "bf": null, "hm": null, @@ -61,7 +13,7 @@ }, "CS_TENANT_KEYSET_ID_1": { "k": "ct", - "c": "mBbLxyC6xC+{EW;;5A`|u`KPxHUzKScK`B&^Jy}8?+{AvGOt<>3Ysz%{Twa+L!gXjsy63HTwS%&1712T=;=KzS_v)L5cf62AYUC~xJMlYv^t3onC{{VkhX", + "c": "mBbKa^tk|l%%@u_arbG%UnJ|qHjRA(OZ~O~aEFq|Uc*O6?2pFqI4tsmify9c35DEDw5q)}F>^yg^t3onC{{VkhX", "ob": null, "bf": null, "hm": null, @@ -73,7 +25,7 @@ }, "CS_TENANT_KEYSET_ID_2": { "k": "ct", - "c": "mBbMJ`*UJyfuK2WYIx}JeqCO~Ha*-O?bCvj0H4BWR!I{IP+g&84Z`0c?^?NS3pgN;u~y8WK){H*3!ky3h!n?FzrMcGlAh+oAZuER+rp|(n*Dxg^gF{wpz@sto!>QMQm6+&?wJE6GNpE5Y;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "c": "mBbLD*8EXQH6XEOK;w-geHlW;Hcz#WkN6K1cJ)L8Il`dD=s-(DNfCehdrxUhA=?^ltY2~a>ltl>eOb}jO|b-_#B9kfh2@RJAn@8tcumPN3B^t3onC{{VkhX", "ob": null, "bf": null, "hm": null, @@ -85,7 +37,7 @@ }, "CS_TENANT_KEYSET_ID_3": { "k": "ct", - "c": "mBbLNuQ68uYA@n#p0N1{dBu*zHdE`M%fbp1-_?W4`AEXO*zc-@=O9>^t3onC{{VkhX", + "c": "mBbM86sosp5K0i7^KEo0`t7I0HXCW)>~VFO&LzfWcEs~u?jfrxx~{6@WR)}XfQBFSj3FDJ!DpHH>*W?X3)L;xfUNGCTmGWNAZ4;VmI-6*9I7}bl;);r{9C+95IEyT(n9|(mdx|EyQOwvY;|SC5Hwu<&vtJ>^t3onC{{VkhX", "ob": null, "bf": null, "hm": null, @@ -97,7 +49,7 @@ }, "CS_TENANT_KEYSET_NAME_1": { "k": "ct", - "c": "mBbJ+K&~zqj~hA_s_brbV0D|s7Xp~0205UCF^xN;Ns7e=m`RWayps}t#2`?P9j09-zvbqOuavY5bnQx4PA1>4T;p_tY()ytR_vvAVQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "c": "mBbJ+(r74pg5s5`J;<6!;3&n!7qFLrdkua!vBgow7y#K5HGW~ayhSXH#2`0o!W21kgS_p^5HtcI%n39_NeYh-0@8ppT$?~FLY}2|VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", "ob": null, "bf": null, "hm": null, @@ -109,7 +61,7 @@ }, "CS_TENANT_KEYSET_NAME_2": { "k": "ct", - "c": "mBbL}WAbeTps@Mb2F2-O!$sZ17t=Cd3lu>!8?)(EBu0T<4=Esv8cRfl#2{-@)J9now;&lMqA$iiHUU)}y)=fkK>Ciy^P!vSvNxr6VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "c": "mBbJTqjoEaJ-CTMWn{Xk(gsY#7Yu1{4D?y85h;F%$M0wwF4J$$oh{29#31%gYxd^m5@QCp&&_m5v;-{C(+w&IFi(EQy{IYeUzVkIVQh6}#1J%G{m*u9K=iaYPbgM7%ZC", "ob": null, "bf": null, "hm": null, @@ -121,7 +73,7 @@ }, "CS_TENANT_KEYSET_NAME_3": { "k": "ct", - "c": "mBbJwjyTKdTw8=TdyJ9#EDfl{7w{KlCp+R1!^~d5yIG}nVQh6}#1J%G{m*u9K=iaYPbgM7%ZC", "ob": null, "bf": null, "hm": null, @@ -133,7 +85,7 @@ }, "DOCKER_HUB_USERNAME": { "k": "ct", - "c": "mBbJx*gnX#oukxKVee&yAkZtsA>GGfJHd6wn21L&_@lHCm207NnPl&O{i{{7b!RG7K=s5RjVeiDiAPtDArSLa5ZE?t)<`)rh0?Hop1JJl`6(R?rFLO#b!Eg5G+h19c5guRv^Y;FRyoUu", + "c": "mBbKFBRV%>F|ABa9GSR+5dsjzA*Xjz65=6Opeh)SCg=TpK2bNREA5BaLw4;oPYF{#`Q=G|+rFLO#b!Eg5G+h19c5guRv^Y;FRyoUu", "ob": null, "bf": null, "hm": null, @@ -145,7 +97,7 @@ }, "DOCKER_HUB_PASSWORD": { "k": "ct", - "c": "mBbK*YbGjRbIEUJ;l7IpaEQl3416=!tEKyAXSugKRW6Htia|^t3onC{{VkhX", + "c": "mBbJN?&s^Jst!Y@DOM$^8w{|-IvwJgJ5RK$$!h6!&)@=iTbz!tLFvp-JQKR$nd9i-bGm`I!bl^WPa_Ts#fX;jGdKtp4t<5hAl7r7XvT##hIQ^t3onC{{VkhX", "ob": null, "bf": null, "hm": null, @@ -157,7 +109,7 @@ }, "MULTITUDES_ACCESS_TOKEN": { "k": "ct", - "c": "mBbKHz&;-To())kJ?nL;PvuU<0e1LJPzQ$#7Ug-%n=uXD(aTAjP4{L_z#E(X;IhH9QC4Ym4b`KXe?Y)HqcJ>qx1<#@>U9Bta5E7qM~Hl4%fGFNuIo^b_ng2^0#=te%aZNxV53-uVKMgDk;~FQGNZ(=~w=mWqVKzEI57qi`6Q)&Rdk68I(64_I^sI#30&bs!?e8=@s6hiH|SuM<^&qZPXfHPmcrvuaUD*Be11*VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", + "c": "mBbMB3Qx9={MGQF%x>r6239u30d{5koTb&8J&>*7xLp;n!_q}|Cfu7z_Cuo(uYCH8tFxS+e`Ts_$GHOCTv$x_Y?($V~#f%qaO@le7zqJBKfo1xwNMHSCryogwW?T1M+WNNH2`QP&coXm7*vL?{v38>j6h$SMgGgM$T)hH^}97=J~?Uh}vGN@t~2yUi?CWi{Q!tSt;CI8@L6^hV!`!9ACFmr4>^GnWbN`ul!nBc2026(i37{c%{c&nyI?pB^VvAVXFewEKHuF(70R#2_j}fWJu4MW4a+wcnx5NF*Yls8(sRP$Yl*3m{Yb4$h@^VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", "ob": null, "bf": null, "hm": null, diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index cd98d8b4..11678173 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -39,6 +39,8 @@ jobs: env: CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + CS_WORKSPACE_CRN: ${{ secrets.CS_VAULT_WORKSPACE_CRN }} + CS_CLIENT_ID: ${{ secrets.CS_VAULT_CLIENT_ID }} run: cd scripts && npm run decrypt - run: | diff --git a/.github/workflows/release-aws-marketplace.yml b/.github/workflows/release-aws-marketplace.yml index e716a043..58c20432 100644 --- a/.github/workflows/release-aws-marketplace.yml +++ b/.github/workflows/release-aws-marketplace.yml @@ -96,6 +96,8 @@ jobs: env: CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + CS_WORKSPACE_CRN: ${{ secrets.CS_VAULT_WORKSPACE_CRN }} + CS_CLIENT_ID: ${{ secrets.CS_VAULT_CLIENT_ID }} run: cd scripts && npm run decrypt - uses: jdx/mise-action@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d3188d3..0907ffaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,6 +125,8 @@ jobs: env: CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + CS_WORKSPACE_CRN: ${{ secrets.CS_VAULT_WORKSPACE_CRN }} + CS_CLIENT_ID: ${{ secrets.CS_VAULT_CLIENT_ID }} run: cd scripts && npm run decrypt - name: Download digests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c23a048..87b57b7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,8 @@ jobs: env: CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} + CS_WORKSPACE_CRN: ${{ secrets.CS_VAULT_WORKSPACE_CRN }} + CS_CLIENT_ID: ${{ secrets.CS_VAULT_CLIENT_ID }} run: cd scripts && npm run decrypt - run: | From eb41525e0d7c7e933d6b8b936cadc9185f5db257 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 22 Dec 2025 14:54:04 +1100 Subject: [PATCH 3/6] refactor(ci): simplify to whole-file encryption Replace per-value encryption with single-blob encryption: - Encrypt entire secrets file as one payload - Decrypt once, then parse with dotenv - Simpler, faster, smaller encrypted file --- .github/secrets.env.encrypted | 130 +++------------------------------- scripts/decrypt-secrets.ts | 26 +++---- scripts/encrypt-secrets.ts | 29 +++----- 3 files changed, 32 insertions(+), 153 deletions(-) diff --git a/.github/secrets.env.encrypted b/.github/secrets.env.encrypted index 7637e4d7..ee041ed5 100644 --- a/.github/secrets.env.encrypted +++ b/.github/secrets.env.encrypted @@ -1,122 +1,12 @@ { - "CS_DEFAULT_KEYSET_ID": { - "k": "ct", - "c": "mBbM0?Nl*Qu~k~!?IMwm(qwtWHu>HeEmn8T4a(l7Q2ep~@2y2p^Y1vQS{!GRU-)lG{ysSLcEnRYnH3F}iR6+(smAqKtl@XWAfLV&!nG*uPP}T~-r&G*#J3lNz%2t5ZP7a`*ZI=R*`;=2Y;|SC5Hwu<&vtJ>^t3onC{{VkhX", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_TENANT_KEYSET_ID_1": { - "k": "ct", - "c": "mBbKa^tk|l%%@u_arbG%UnJ|qHjRA(OZ~O~aEFq|Uc*O6?2pFqI4tsmify9c35DEDw5q)}F>^yg^t3onC{{VkhX", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_TENANT_KEYSET_ID_2": { - "k": "ct", - "c": "mBbLD*8EXQH6XEOK;w-geHlW;Hcz#WkN6K1cJ)L8Il`dD=s-(DNfCehdrxUhA=?^ltY2~a>ltl>eOb}jO|b-_#B9kfh2@RJAn@8tcumPN3B^t3onC{{VkhX", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_TENANT_KEYSET_ID_3": { - "k": "ct", - "c": "mBbM86sosp5K0i7^KEo0`t7I0HXCW)>~VFO&LzfWcEs~u?jfrxx~{6@WR)}XfQBFSj3FDJ!DpHH>*W?X3)L;xfUNGCTmGWNAZ4;VmI-6*9I7}bl;);r{9C+95IEyT(n9|(mdx|EyQOwvY;|SC5Hwu<&vtJ>^t3onC{{VkhX", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_TENANT_KEYSET_NAME_1": { - "k": "ct", - "c": "mBbJ+(r74pg5s5`J;<6!;3&n!7qFLrdkua!vBgow7y#K5HGW~ayhSXH#2`0o!W21kgS_p^5HtcI%n39_NeYh-0@8ppT$?~FLY}2|VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_TENANT_KEYSET_NAME_2": { - "k": "ct", - "c": "mBbJTqjoEaJ-CTMWn{Xk(gsY#7Yu1{4D?y85h;F%$M0wwF4J$$oh{29#31%gYxd^m5@QCp&&_m5v;-{C(+w&IFi(EQy{IYeUzVkIVQh6}#1J%G{m*u9K=iaYPbgM7%ZC", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "CS_TENANT_KEYSET_NAME_3": { - "k": "ct", - "c": "mBbJa8JfC8cf;O(@6B(CT+-;o7g*nt^_G8wfwl^!USRDMv2)t`&x-2W#31i!G%X)KSwL1P-_O|!FD?N`{m?8>Cp+R1!^~d5yIG}nVQh6}#1J%G{m*u9K=iaYPbgM7%ZC", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "DOCKER_HUB_USERNAME": { - "k": "ct", - "c": "mBbKFBRV%>F|ABa9GSR+5dsjzA*Xjz65=6Opeh)SCg=TpK2bNREA5BaLw4;oPYF{#`Q=G|+rFLO#b!Eg5G+h19c5guRv^Y;FRyoUu", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "DOCKER_HUB_PASSWORD": { - "k": "ct", - "c": "mBbJN?&s^Jst!Y@DOM$^8w{|-IvwJgJ5RK$$!h6!&)@=iTbz!tLFvp-JQKR$nd9i-bGm`I!bl^WPa_Ts#fX;jGdKtp4t<5hAl7r7XvT##hIQ^t3onC{{VkhX", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - }, - "MULTITUDES_ACCESS_TOKEN": { - "k": "ct", - "c": "mBbMB3Qx9={MGQF%x>r6239u30d{5koTb&8J&>*7xLp;n!_q}|Cfu7z_Cuo(uYCH8tFxS+e`Ts_$GHOCTv$x_Y?($V~#f%qaO@le7zqJBKfo1xwNMHSCryogwW?T1M+WNNH2`QP&coXm7*vL?{v38>j6h$SMgGgM$T)hH^}97=J~?Uh}vGN@t~2yUi?CWi{Q!tSt;CI8@L6^hV!`!9ACFmr4>^GnWbN`ul!nBc2026(i37{c%{c&nyI?pB^VvAVXFewEKHuF(70R#2_j}fWJu4MW4a+wcnx5NF*Yls8(sRP$Yl*3m{Yb4$h@^VQh6}#1J%G{m*u9K=iaYPbgM7%ZC", - "ob": null, - "bf": null, - "hm": null, - "i": { - "t": "ci_secrets", - "c": "value" - }, - "v": 2 - } + "k": "ct", + "c": "mBbKAoNc(qCyj1dv~cffmKx^812qC-mMd8i2N)OP5M3E)uk=Yn{4kUIe-v{dQ+cJ=D+$HeAONGotT3y<*<#>k_o$T{KS;Nz!;7fwmbuVYJ_hzcdybtS2&z(C1CSv%;uS__hclT8wUKkQW3GqR70E!W>MB+5zq@1F>rqBLmw(G!fK0Lhc7>=eiUA9BSOa>5{3AIvBpWg4&hw&OlC(uk&H%$h%>tZ`QG)sMah4!fB+ry_YC0lHb>b^T*n!!%YI&4~rZTc%6~TRETmlc<^FH!HhiK0>=bI6HV(V!&$m5-T=E$ziHSfEPSP5a1=eskcjrWx5@HRq@zmMkBB7dq*Wy+Ts-4kCWHu&gN>rJ!Piwt=;u%;IAawIKXxxX1Z)nSV+|T_KB1}u>Y|Dkp}i32WPAW3$R|vRky(Vl;<$Vb4&CLI&JdW{MD=R$sT)#1g}Z5>+7*A;V3dZ}9Qy+Ghg{70uIo@$=pweSy;(DJHMlYjvP_T70n`BCq_hP9`n;pph=Z4dD4%kfn)~z&M}}cbmJy8Sk!Tz)W8$Ju9T0v44m$14hXjTM$B6O^ohdxKeUV(nvnmeni;#)m<6Veou9#DPak%mzbqA;tZ5Y2S2AOS@Lq97|$J$thFlY~`vY=6s=>>`pDg71Q_XSX{x~#5Ep;;-;)iId*3@SDNf|u)g&bf(P*Yjol+9l*gG%6bHj}T2f1IX0DXPAkYy0y^tZcM*Wcw_PHZQNC|ec!FFR-8TOPB@`E*k>ZNvJY;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "ob": null, + "bf": null, + "hm": null, + "i": { + "t": "ci_secrets", + "c": "value" + }, + "v": 2 } diff --git a/scripts/decrypt-secrets.ts b/scripts/decrypt-secrets.ts index 7d5bad33..6f3fb288 100644 --- a/scripts/decrypt-secrets.ts +++ b/scripts/decrypt-secrets.ts @@ -1,15 +1,13 @@ import { protect, csTable, csColumn, Encrypted } from "@cipherstash/protect"; import * as fs from "fs"; import * as path from "path"; +import * as dotenv from "dotenv"; const schema = csTable("ci_secrets", { value: csColumn("value"), }); -type EncryptedSecrets = Record; - async function main(): Promise { - // Find .env.encrypted relative to repo root (one level up from scripts/) const repoRoot = path.resolve(import.meta.dirname, ".."); const encryptedPath = path.join(repoRoot, ".github", "secrets.env.encrypted"); @@ -19,33 +17,31 @@ async function main(): Promise { } const client = await protect({ schemas: [schema] }); - const encrypted: EncryptedSecrets = JSON.parse( + const encrypted: Encrypted = JSON.parse( fs.readFileSync(encryptedPath, "utf-8") ); + const result = await client.decrypt(encrypted); + if (result.failure) { + console.error(`Failed to decrypt: ${result.failure.message}`); + process.exit(1); + } + + const env = dotenv.parse(String(result.data)); const githubEnvPath = process.env.GITHUB_ENV; const isCI = !!githubEnvPath; - for (const [key, payload] of Object.entries(encrypted)) { - const result = await client.decrypt(payload); - if (result.failure) { - console.error(`Failed to decrypt ${key}: ${result.failure.message}`); - process.exit(1); - } - const value = String(result.data); - + for (const [key, value] of Object.entries(env)) { if (isCI) { - // GitHub Actions: use heredoc syntax for multiline values const delimiter = `EOF_${key}_${Date.now()}`; fs.appendFileSync(githubEnvPath, `${key}<<${delimiter}\n${value}\n${delimiter}\n`); } else { - // Local: simple KEY=value output (for testing) console.log(`${key}=${value}`); } } if (isCI) { - console.error(`Decrypted ${Object.keys(encrypted).length} secrets to $GITHUB_ENV`); + console.error(`Decrypted ${Object.keys(env).length} secrets to $GITHUB_ENV`); } } diff --git a/scripts/encrypt-secrets.ts b/scripts/encrypt-secrets.ts index a7be246f..51d0efba 100644 --- a/scripts/encrypt-secrets.ts +++ b/scripts/encrypt-secrets.ts @@ -1,7 +1,6 @@ import { protect, csTable, csColumn } from "@cipherstash/protect"; import * as fs from "fs"; import * as path from "path"; -import * as dotenv from "dotenv"; const schema = csTable("ci_secrets", { value: csColumn("value"), @@ -14,30 +13,24 @@ async function main(): Promise { if (!fs.existsSync(plaintextPath)) { console.error(`Error: ${plaintextPath} not found`); - console.error("Create this file with your plaintext secrets (KEY=value format)"); process.exit(1); } - const env = dotenv.parse(fs.readFileSync(plaintextPath)); + const fileContent = fs.readFileSync(plaintextPath, "utf-8"); const client = await protect({ schemas: [schema] }); - const encrypted: Record = {}; - - for (const [key, value] of Object.entries(env)) { - const result = await client.encrypt(value, { - table: schema, - column: schema.value, - }); - if (result.failure) { - console.error(`Failed to encrypt ${key}: ${result.failure.message}`); - process.exit(1); - } - encrypted[key] = result.data; - console.error(`Encrypted: ${key}`); + const result = await client.encrypt(fileContent, { + table: schema, + column: schema.value, + }); + + if (result.failure) { + console.error(`Failed to encrypt: ${result.failure.message}`); + process.exit(1); } - fs.writeFileSync(encryptedPath, JSON.stringify(encrypted, null, 2) + "\n"); - console.error(`\nWrote ${Object.keys(encrypted).length} secrets to ${encryptedPath}`); + fs.writeFileSync(encryptedPath, JSON.stringify(result.data, null, 2) + "\n"); + console.error(`Encrypted secrets file to ${encryptedPath}`); } main().catch((err) => { From 1d1ac59d64596a57cf38835d3bf77eba744367d8 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 22 Dec 2025 15:12:01 +1100 Subject: [PATCH 4/6] fix(ci): forward bootstrap secrets to $GITHUB_ENV The decrypt step receives bootstrap secrets (CS_CLIENT_ID, etc.) as env vars but they weren't being written to $GITHUB_ENV for subsequent steps. Now forwards all 4 bootstrap secrets alongside decrypted secrets. --- scripts/decrypt-secrets.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/decrypt-secrets.ts b/scripts/decrypt-secrets.ts index 6f3fb288..1ed7d493 100644 --- a/scripts/decrypt-secrets.ts +++ b/scripts/decrypt-secrets.ts @@ -31,17 +31,37 @@ async function main(): Promise { const githubEnvPath = process.env.GITHUB_ENV; const isCI = !!githubEnvPath; - for (const [key, value] of Object.entries(env)) { + // Bootstrap secrets (passed in as env vars, need to forward to $GITHUB_ENV) + const bootstrapSecrets = [ + "CS_CLIENT_ID", + "CS_CLIENT_KEY", + "CS_CLIENT_ACCESS_KEY", + "CS_WORKSPACE_CRN", + ]; + + // Combine bootstrap secrets with decrypted secrets + const allSecrets: Record = { ...env }; + for (const key of bootstrapSecrets) { + const value = process.env[key]; + if (value) { + allSecrets[key] = value; + } + } + + for (const [key, value] of Object.entries(allSecrets)) { if (isCI) { const delimiter = `EOF_${key}_${Date.now()}`; fs.appendFileSync(githubEnvPath, `${key}<<${delimiter}\n${value}\n${delimiter}\n`); } else { - console.log(`${key}=${value}`); + // Only output non-bootstrap secrets locally (bootstrap are already in env) + if (!bootstrapSecrets.includes(key)) { + console.log(`${key}=${value}`); + } } } if (isCI) { - console.error(`Decrypted ${Object.keys(env).length} secrets to $GITHUB_ENV`); + console.error(`Wrote ${Object.keys(allSecrets).length} secrets to $GITHUB_ENV`); } } From 46c3f4bbc289bd22ef779e2cd402ddd32b816647 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 23 Dec 2025 10:32:55 +1100 Subject: [PATCH 5/6] feat(ci): add dual-mode secrets encryption (file/vars) Support both whole-file encryption (--file, default) and individual variable encryption (--vars) for CI secrets. - encrypt-secrets.ts: add --file/--vars CLI flags - decrypt-secrets.ts: auto-detect format from encrypted file structure - Backwards compatible: existing file-mode encryption continues to work --- .github/secrets.env.encrypted | 2 +- scripts/decrypt-secrets.ts | 36 +++++++++++++++++----- scripts/encrypt-secrets.ts | 56 ++++++++++++++++++++++++++++------- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/.github/secrets.env.encrypted b/.github/secrets.env.encrypted index ee041ed5..9599302c 100644 --- a/.github/secrets.env.encrypted +++ b/.github/secrets.env.encrypted @@ -1,6 +1,6 @@ { "k": "ct", - "c": "mBbKAoNc(qCyj1dv~cffmKx^812qC-mMd8i2N)OP5M3E)uk=Yn{4kUIe-v{dQ+cJ=D+$HeAONGotT3y<*<#>k_o$T{KS;Nz!;7fwmbuVYJ_hzcdybtS2&z(C1CSv%;uS__hclT8wUKkQW3GqR70E!W>MB+5zq@1F>rqBLmw(G!fK0Lhc7>=eiUA9BSOa>5{3AIvBpWg4&hw&OlC(uk&H%$h%>tZ`QG)sMah4!fB+ry_YC0lHb>b^T*n!!%YI&4~rZTc%6~TRETmlc<^FH!HhiK0>=bI6HV(V!&$m5-T=E$ziHSfEPSP5a1=eskcjrWx5@HRq@zmMkBB7dq*Wy+Ts-4kCWHu&gN>rJ!Piwt=;u%;IAawIKXxxX1Z)nSV+|T_KB1}u>Y|Dkp}i32WPAW3$R|vRky(Vl;<$Vb4&CLI&JdW{MD=R$sT)#1g}Z5>+7*A;V3dZ}9Qy+Ghg{70uIo@$=pweSy;(DJHMlYjvP_T70n`BCq_hP9`n;pph=Z4dD4%kfn)~z&M}}cbmJy8Sk!Tz)W8$Ju9T0v44m$14hXjTM$B6O^ohdxKeUV(nvnmeni;#)m<6Veou9#DPak%mzbqA;tZ5Y2S2AOS@Lq97|$J$thFlY~`vY=6s=>>`pDg71Q_XSX{x~#5Ep;;-;)iId*3@SDNf|u)g&bf(P*Yjol+9l*gG%6bHj}T2f1IX0DXPAkYy0y^tZcM*Wcw_PHZQNC|ec!FFR-8TOPB@`E*k>ZNvJY;|SC5Hwu<&vtJ>^t3onC{{VkhX", + "c": "mBbKV0G4)-8UdG=fVK^Mgf0{+={6c)WxWPnZ(Cp1Iq=%3}nK4a)CGBx1bjZ=qv8EHV9gp30vQ`13OJxFK&vBuwpyEt$JE{*iks{9CMl@IGpcvQ8gA7RNsgU>j6~vXw+Ir0WKXxrDu|PrlDk@Z@tYC11HTq*7Ufq{V_GxAc=rh+gkkDsiM%LjPyK-fvut?5Fwf*i8>YcD-HELZe^)cc$8aXF;l#4*srYmmdO$ydq-KxbwECzD%{l$#IQh90%*!8$=`lK`!aea=QOICK(5JGRkL1<_eHdY9z1_J8<51L=N29rCD=uup}QKsFs2>$S)|D9?w4r;l>ovY*rGAxmlR14{QFjmRFQP!4d#Q6k6p=#uO(tNpb(^yl7XC+h4xCXFq(gy*@DmrEK>}gF7fQx{^)m{aV4gA&KH|wO)rikb|jF_K@5|f+NpJ*%)`>xuKl=uSaMF4p!PA;EzE4?hdtPnd+5Y4Dk|UC7@wAs%G|M~f`LP3K*__-N~wag5|>u|aHGqB{mnyWcVLsKU&RUA*PLl#znp61^`!uKxq8~0fpsZi%tn;yFzj>-vHH*Zndd0d(|vjSN7XFO6f$aN3~2jD)GOi}6EHaE^o(dM0T6=5c!rBY7_9zJzpt74(wmd&;*o}70+0^HUc9)3GP7-s0il063@(-@;mcA;%pp%{`q+J}@ZQ>Y7i|Que0zv2o~nuk#%2pe^E0UN;J&qv)igfU!(!JHBVN`&s8VknlfK+YI`d6Ex(qIR)))}{j{8ZssNpWrUUpL%CGhk)6bjn`YMP@j&!4b)o5EHb&x(EwT5Re@jO4IK&ZSSQ50Gn`=uvGmBQ_N|c=l+SXHc{Mwe^hT_DK)zCcCW;$(YGqnzo9>AZz%W3Dp9wW9mjKd*NrGw;>G4_yU_>0^X1Ol6KHcL#1|MY;|SC5Hwu<&vtJ>^t3onC{{VkhX", "ob": null, "bf": null, "hm": null, diff --git a/scripts/decrypt-secrets.ts b/scripts/decrypt-secrets.ts index 1ed7d493..e1911c6d 100644 --- a/scripts/decrypt-secrets.ts +++ b/scripts/decrypt-secrets.ts @@ -7,6 +7,10 @@ const schema = csTable("ci_secrets", { value: csColumn("value"), }); +function isFileMode(data: unknown): data is Encrypted { + return data !== null && typeof data === "object" && "k" in data && "c" in data; +} + async function main(): Promise { const repoRoot = path.resolve(import.meta.dirname, ".."); const encryptedPath = path.join(repoRoot, ".github", "secrets.env.encrypted"); @@ -17,17 +21,35 @@ async function main(): Promise { } const client = await protect({ schemas: [schema] }); - const encrypted: Encrypted = JSON.parse( + const encrypted: unknown = JSON.parse( fs.readFileSync(encryptedPath, "utf-8") ); - const result = await client.decrypt(encrypted); - if (result.failure) { - console.error(`Failed to decrypt: ${result.failure.message}`); - process.exit(1); - } + let env: Record; - const env = dotenv.parse(String(result.data)); + if (isFileMode(encrypted)) { + // File mode: decrypt single blob, parse as .env + const result = await client.decrypt(encrypted); + if (result.failure) { + console.error(`Failed to decrypt: ${result.failure.message}`); + process.exit(1); + } + env = dotenv.parse(String(result.data)); + console.error("Detected file mode encryption"); + } else { + // Vars mode: decrypt each variable individually + env = {}; + const encryptedVars = encrypted as Record; + for (const [key, payload] of Object.entries(encryptedVars)) { + const result = await client.decrypt(payload); + if (result.failure) { + console.error(`Failed to decrypt ${key}: ${result.failure.message}`); + process.exit(1); + } + env[key] = String(result.data); + } + console.error(`Detected vars mode encryption (${Object.keys(env).length} variables)`); + } const githubEnvPath = process.env.GITHUB_ENV; const isCI = !!githubEnvPath; diff --git a/scripts/encrypt-secrets.ts b/scripts/encrypt-secrets.ts index 51d0efba..22a4510f 100644 --- a/scripts/encrypt-secrets.ts +++ b/scripts/encrypt-secrets.ts @@ -1,12 +1,23 @@ import { protect, csTable, csColumn } from "@cipherstash/protect"; import * as fs from "fs"; import * as path from "path"; +import * as dotenv from "dotenv"; + +type Mode = "file" | "vars"; + +function parseArgs(): Mode { + const args = process.argv.slice(2); + if (args.includes("--vars")) return "vars"; + if (args.includes("--file")) return "file"; + return "file"; // default +} const schema = csTable("ci_secrets", { value: csColumn("value"), }); async function main(): Promise { + const mode = parseArgs(); const repoRoot = path.resolve(import.meta.dirname, ".."); const plaintextPath = path.join(repoRoot, ".github", "secrets.env.plaintext"); const encryptedPath = path.join(repoRoot, ".github", "secrets.env.encrypted"); @@ -19,18 +30,43 @@ async function main(): Promise { const fileContent = fs.readFileSync(plaintextPath, "utf-8"); const client = await protect({ schemas: [schema] }); - const result = await client.encrypt(fileContent, { - table: schema, - column: schema.value, - }); + if (mode === "file") { + // File mode: encrypt entire file content as single blob + const result = await client.encrypt(fileContent, { + table: schema, + column: schema.value, + }); - if (result.failure) { - console.error(`Failed to encrypt: ${result.failure.message}`); - process.exit(1); - } + if (result.failure) { + console.error(`Failed to encrypt: ${result.failure.message}`); + process.exit(1); + } - fs.writeFileSync(encryptedPath, JSON.stringify(result.data, null, 2) + "\n"); - console.error(`Encrypted secrets file to ${encryptedPath}`); + fs.writeFileSync(encryptedPath, JSON.stringify(result.data, null, 2) + "\n"); + console.error(`Encrypted secrets file to ${encryptedPath} (file mode)`); + } else { + // Vars mode: encrypt each variable individually + const env = dotenv.parse(fileContent); + const encrypted: Record = {}; + + for (const [key, value] of Object.entries(env)) { + const result = await client.encrypt(value, { + table: schema, + column: schema.value, + }); + + if (result.failure) { + console.error(`Failed to encrypt ${key}: ${result.failure.message}`); + process.exit(1); + } + + encrypted[key] = result.data; + console.error(`Encrypted: ${key}`); + } + + fs.writeFileSync(encryptedPath, JSON.stringify(encrypted, null, 2) + "\n"); + console.error(`Encrypted ${Object.keys(encrypted).length} secrets to ${encryptedPath} (vars mode)`); + } } main().catch((err) => { From 28e8cb884c7afec3a52be6a017299e4cc70b5390 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 5 Jan 2026 09:41:16 +1100 Subject: [PATCH 6/6] ci: use protectgh action for secret decryption --- .github/workflows/test.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87b57b7b..95844785 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,23 +19,15 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-test - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: scripts/package-lock.json - - - name: Install decrypt dependencies - working-directory: scripts - run: npm ci - - name: Decrypt secrets + uses: cipherstash/protectgh@main + with: + secrets-file: .github/secrets.env.encrypted env: + CS_CLIENT_ID: ${{ secrets.CS_VAULT_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_VAULT_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_VAULT_CLIENT_ACCESS_KEY }} CS_WORKSPACE_CRN: ${{ secrets.CS_VAULT_WORKSPACE_CRN }} - CS_CLIENT_ID: ${{ secrets.CS_VAULT_CLIENT_ID }} - run: cd scripts && npm run decrypt - run: | mise run postgres:up --extra-args "--detach --wait"