diff --git a/package-lock.json b/package-lock.json index 7d61a9d..2900279 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "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", @@ -29,6 +30,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", @@ -1980,6 +1994,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 +2026,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", @@ -2272,7 +2303,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 +2547,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", @@ -2650,10 +2692,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 +2777,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 +2849,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", @@ -2943,7 +2992,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 +3001,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 +3037,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 +3046,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 +3887,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 +3917,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 +3959,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 +4031,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", @@ -4107,7 +4187,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 +4272,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 +4284,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 +4299,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" @@ -5953,6 +6029,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 +6233,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 +6269,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 +6340,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 +6825,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", @@ -7237,6 +7344,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", @@ -7968,6 +8084,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", diff --git a/package.json b/package.json index b265942..5c9e775 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "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", diff --git a/src/index.ts b/src/index.ts index 2cf0995..db533ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,13 +26,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) { diff --git a/src/utils.test.ts b/src/utils.test.ts index ca954a2..9cb2f16 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -15,6 +15,7 @@ import { envManagedVariables, envServiceAccountToken, } from "./constants"; +import { OnePasswordConnect } from "@1password/connect"; jest.mock("@actions/core"); jest.mock("@actions/exec", () => ({ @@ -29,6 +30,7 @@ jest.mock("@1password/sdk", () => ({ validateSecretReference: jest.fn(), }, })); +jest.mock("@1password/connect"); beforeEach(() => { jest.clearAllMocks(); @@ -151,25 +153,47 @@ describe("extractSecret", () => { }); describe("loadSecrets when using Connect", () => { - it("sets the client info and gets the executed output", async () => { + beforeEach(() => { + 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({ + getItem: jest.fn().mockResolvedValue({ + fields: [ + { label: "field", value: "resolved-via-connect", section: undefined }, + ], + sections: [], + }), + }); + }); + 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(); }); @@ -177,7 +201,15 @@ describe("loadSecrets when using Connect", () => { 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 () => { diff --git a/src/utils.ts b/src/utils.ts index 97922bb..e84b121 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import * as core from "@actions/core"; -import * as exec from "@actions/exec"; -import { read, setClientInfo, semverToInt } from "@1password/op-js"; +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,6 +11,242 @@ import { envManagedVariables, } from "./constants"; +// Types for parsed op ref +export interface ParsedOpRef { + vault: string; + item: string; + section: string | undefined; + field: string; +} + +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://// or op:////
/. Got: ${ref}`, + ); + } + + const vault = segments[0] ?? ""; + if (!vault) { + throw new Error(`Invalid op reference: vault is required`); + } + + const item = segments[1] ?? ""; + if (!item) { + throw new Error(`Invalid op reference: item is required`); + } + + // Last segment is always the field + const field = segments[segments.length - 1] ?? ""; + if (!field) { + throw new Error(`Invalid op reference: field is required`); + } + + // 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, + }; +} + +const getSecretFromConnectItem = async ( + client: OPConnect, + item: FullItem, + parsed: ParsedOpRef, +): Promise => { + const sectionIds = parsed.section ? findSectionIdsByQuery(item.sections, parsed.section) : []; + const { fieldValue, fileId } = findMatchingFieldAndFile(item, parsed.field, sectionIds); + + if (fieldValue !== undefined) { + return fieldValue; + } + + // If a file was found, get the content of the file + if (fileId) { + const content = await client.getFileContent( + parsed.vault, + parsed.item, + fileId, + ); + return content; + } + + 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 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( + `section ${sectionQuery} could not be found in specified item`, + ); + } + + const ids = sections + .filter((s) => s.id === sectionQuery || s.label === sectionQuery) + .map((s) => s.id!) + .filter(Boolean); + + // If no sections were found with the given query throw an error + if (ids.length === 0) { + throw new Error( + `section ${sectionQuery} could not be found in specified item`, + ); + } + + return ids; +}; + + + +const findMatchingFieldAndFile = ( + item: FullItem, + fieldOrFileQuery: string, + sectionIds: string[], +): { fieldValue?: string; fileId?: string } => { + const errMultiple = `multiple fields ${fieldOrFileQuery} that match the provided reference have been found`; + + const fields = item.fields ?? []; + const files = item.files ?? []; + const sectionFilter = sectionIds.length > 0; + + let matchedField: (typeof fields)[0] | undefined; + let matchedFile: (typeof files)[0] | undefined; + + if (sectionFilter) { + // Filter fields by section + const matchingFields = fields.filter((f) => { + const fieldIdOrLabelMatchesQuery = f.id === fieldOrFileQuery || f.label === fieldOrFileQuery; + const sectionId = f.section?.id; + const fieldSectionIsInRefSections = sectionId != null && sectionIds.includes(sectionId); + return fieldIdOrLabelMatchesQuery && fieldSectionIsInRefSections; + }); + + // If multiple fields match the query throw an error otherwise set first matching field + if (matchingFields.length > 1) { + throw new Error(errMultiple); + } + matchedField = matchingFields[0]; + + const matchingFiles = files.filter((f) => { + const fileIdOrNameMatchesQuery = f.id === fieldOrFileQuery || f.name === fieldOrFileQuery; + const sectionId = f.section?.id; + const fileSectionIsInRefSections = sectionId != null && sectionIds.includes(sectionId); + return fileIdOrNameMatchesQuery && fileSectionIsInRefSections; + }); + + // If multiple files match the query throw an error otherwise set first matching file + if (matchingFiles.length > 1) { + throw new Error(errMultiple); + } + matchedFile = matchingFiles[0]; + } else { + let matchingFields = fields.filter((f) => { + const fieldIdOrLabelMatchesQuery = f.id === fieldOrFileQuery || f.label === fieldOrFileQuery; + const fieldHasNoSection = f.section?.id == null; + return fieldIdOrLabelMatchesQuery && fieldHasNoSection; + }); + + // If multiple fields match the query throw an error otherwise set first matching field + if (matchingFields.length > 1) { + throw new Error(errMultiple); + } + matchedField = matchingFields[0]; + + // If no field was found with no section, find a field in any section + if (!matchedField) { + const matchingFieldsInAnySection = fields.filter((f) => { + const fieldIdOrLabelMatchesQuery =f.id === fieldOrFileQuery || f.label === fieldOrFileQuery; + return fieldIdOrLabelMatchesQuery; + }); + + if (matchingFieldsInAnySection.length > 1) { + throw new Error(errMultiple); + } + matchedField = matchingFieldsInAnySection[0]; + } + + let matchingFiles = files.filter((f) => { + const fileIdOrNameMatchesQuery = f.id === fieldOrFileQuery || f.name === fieldOrFileQuery; + const fileHasNoSection = f.section?.id == null; + return fileIdOrNameMatchesQuery && fileHasNoSection; + }); + + // If multiple files match the query throw an error otherwise set first matching file + if (matchingFiles.length > 1) { + throw new Error(errMultiple); + } + matchedFile = matchingFiles[0]; + + // If no file was found with no section, find a file in any section + if (!matchedFile) { + const matchingFilesInAnySection = files.filter((f) => { + const fileIdOrNameMatchesQuery = f.id === fieldOrFileQuery || f.name === fieldOrFileQuery; + return fileIdOrNameMatchesQuery; + }); + + if (matchingFilesInAnySection.length > 1) { + throw new Error(errMultiple); + } + matchedFile = matchingFilesInAnySection[0]; + } + } + + if (matchedField && matchedFile) { + throw new Error( + `you cannot query fields/files that are identically named.`, + ); + } + + 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) { + const fileId = matchedFile.id; + return { fileId }; + } + + return {}; +} + export const validateAuth = (): void => { const isConnect = process.env[envConnectHost] && process.env[envConnectToken]; const isServiceAccount = process.env[envServiceAccountToken]; @@ -105,29 +341,50 @@ export const extractSecret = ( } }; -// Connect loads secrets via the 1Password CLI +// Connect loads secrets via the Connect JS SDK const loadSecretsViaConnect = async ( shouldExportEnv: boolean, ): Promise => { - 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); } + + // Authenticate with the Connect SDK + let client; + try { + client = OnePasswordConnect({ + serverURL: host, + token, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Connect authentication failed: ${message}`); + } + + for (const envName of envs) { + const ref = process.env[envName]; + if (!ref) { + continue; + } + + // Parse the op ref and get the item from the Connect SDK + const parsed = parseOpRef(ref); + const item = await client.getItem(parsed.vault, parsed.item); + + // Get the secret value from the item as Connect returns a full item object + const secretValue = await getSecretFromConnectItem(client, item, parsed); + setResolvedSecret(envName, secretValue, shouldExportEnv); + } + if (shouldExportEnv) { core.exportVariable(envManagedVariables, envs.join()); }