├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest.config.js ├── jest.setup.ts ├── package.json ├── src ├── cli.test.ts ├── cli.ts └── index.ts ├── tests ├── document.test.ts ├── eventsApi.test.ts ├── inject.test.ts ├── item.test.ts ├── version.test.ts └── whoami.test.ts ├── tsconfig.json ├── tsconfig.release.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@1password" 4 | ], 5 | "overrides": [ 6 | { 7 | "files": [ 8 | "*.ts" 9 | ], 10 | "parserOptions": { 11 | "project": [ 12 | "./tsconfig.json" 13 | ] 14 | }, 15 | "rules": { 16 | "@typescript-eslint/no-shadow": "off", 17 | "jsdoc/require-param": "off", 18 | "jsdoc/require-returns": "off", 19 | "@typescript-eslint/ban-types": "off", 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "@typescript-eslint/naming-convention": "off" 22 | } 23 | } 24 | ], 25 | "parser": "@typescript-eslint/parser" 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report bugs and errors found while using op-js. 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ### Your environment 10 | 11 | 12 | 13 | op-js version: 14 | 15 | 16 | 17 | CLI version: 18 | 19 | 20 | 21 | OS: 22 | 23 | ## What happened? 24 | 25 | 26 | 27 | ## What did you expect to happen? 28 | 29 | 30 | 31 | ## Steps to reproduce 32 | 33 | 1. 34 | 35 | ## Notes & Logs 36 | 37 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for op-js 4 | title: "" 5 | labels: feature-request 6 | assignees: "" 7 | --- 8 | 9 | ### Summary 10 | 11 | 12 | 13 | ### Use cases 14 | 15 | 18 | 19 | ### Proposed solution 20 | 21 | 24 | 25 | ### Is there a workaround to accomplish this today? 26 | 27 | 29 | 30 | ### References & Prior Work 31 | 32 | 34 | 35 | - 36 | - 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | registry-url: "https://registry.npmjs.org" 16 | - run: yarn install --frozen-lockfile 17 | - run: yarn build 18 | - run: yarn publish --non-interactive 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 18 17 | - run: yarn 18 | - run: yarn typecheck 19 | - run: yarn eslint 20 | - run: yarn prettier 21 | - run: yarn test:unit 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .husky/_/husky.sh 4 | 1password-credentials.json 5 | .env 6 | dist/ 7 | licenses/ 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.18.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development 4 | 5 | This is a JavaScript wrapper for 1Password CLI, written in TypeScript. 6 | 7 | Requires [Node](https://nodejs.org/en/) and [1Password CLI](https://developer.1password.com/docs/cli/). 8 | 9 | ### Dependencies 10 | 11 | To install dependencies: 12 | 13 | ```shell 14 | yarn # or yarn install 15 | ``` 16 | 17 | ### Running locally 18 | 19 | While you're working on this project you should watch for changes: 20 | 21 | ```shell 22 | yarn watch 23 | ``` 24 | 25 | This will recompile the project into the `/dist` folder whenever a file changes. You can then import the package into a local project for testing purposes. 26 | 27 | ### Linting and formatting 28 | 29 | Code should be linted and formatted where appropriate. We have commands for all types of code in this project: 30 | 31 | ```shell 32 | # Run Prettier on all TS files 33 | yarn prettier 34 | 35 | # Run ESLint on all TS files 36 | yarn eslint 37 | 38 | # Typecheck all TS files 39 | yarn typecheck 40 | ``` 41 | 42 | The above commands will only return linting reports. You can optionally attach the appropriate `--fix` / `--write` flag when running the commands, which will modify the files to fix issues that can be done so automatically. Some issues will need to be manually addressed. 43 | 44 | #### Pre-commit checks 45 | 46 | This project is set up to use [Husky](https://typicode.github.io/husky/), which allows us to hook into Git operations, and [lint-staged](https://www.npmjs.com/package/lint-staged), a way to run commands against globs of files staged in Git. 47 | 48 | When you run `git commit` Husky invokes its pre-commit hook, which runs lint-staged, resulting in all the above linter commands getting called with flags set to automatically fix issues. If the linters have issues that can't be automatically addressed the commit will be aborted, giving you a chance to manually fix things. The purpose of this is to enforce code consistency across the project. 49 | 50 | There may come a time when you need to skip these checks; to prevent the pre-commit hook from running add `--no-verify` to your commit command. 51 | 52 | ### Testing 53 | 54 | Code should be reasonably tested. We do not currently have any required coverage threshold, but if you are adding new or changing existing functionality you should consider writing/updating tests. 55 | 56 | This project uses [Jest](https://jestjs.io/). We have two types of tests at the moment. 57 | 58 | - **Unit tests** are for anything broadly applicable to the whole wrapper library that can be broken into units of code, such as command execution, helpers, error handling, etc. 59 | - **Integration tests** are intended to test out the individual command implementations, to assert their expected functionality and schemas. Currently this requires you to use biometrics with a real 1Password account by setting up the following `.env` file: 60 | 61 | ``` 62 | OP_ACCOUNT=[account URL] 63 | OP_VAULT=[vault name] 64 | ``` 65 | 66 | Commands are pretty straightforward: 67 | 68 | ```shell 69 | # Run the entire unit test suite 70 | yarn test:unit 71 | 72 | # Run the entire integration test suite 73 | yarn test:integration 74 | 75 | # Run the unit test suite, re-running on changes 76 | yarn test:unit --watch 77 | 78 | # Run only integration tests that have a specific description 79 | yarn test:integration -t="returns injected data" 80 | ``` 81 | 82 | ## Distribution 83 | 84 | ### Publishing the package 85 | 86 | We have a Workflow set up to automatically publish a new version of [the package on NPM](https://www.npmjs.com/package/@1password/op-js) whenever a new version tag is pushed. 87 | 88 | You should only need to do the following on the `main` branch: 89 | 90 | ```shell 91 | # Replace VERSION with the version you are bumping to 92 | yarn version --new-version VERSION && git push 93 | ``` 94 | 95 | This will: 96 | 97 | 1. Update the `version` property in `package.json` 98 | 2. Commit this version change 99 | 3. Create a new version tag 100 | 4. Push the commit and tag to the remote 101 | 102 | Afterward the Workflow will take over, publishing the package's new version to NPM. 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 1Password 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # op-js 2 | 3 | This package is a simple JavaScript wrapper for the [1Password CLI](https://developer.1password.com/docs/cli). It provides methods for most of the CLI's [commands](https://developer.1password.com/docs/cli/reference), and in many cases extends the CLI's ability to authenticate using [biometrics](https://developer.1password.com/docs/cli/about-biometric-unlock) to whatever Node-based application you're building. It also includes TypeScript declarations. 4 | 5 | ## Installation 6 | 7 | Install using Yarn: 8 | 9 | ```shell 10 | yarn add @1password/op-js 11 | ``` 12 | 13 | Or using NPM: 14 | 15 | ```shell 16 | npm install @1password/op-js 17 | ``` 18 | 19 | ## Usage 20 | 21 | After installation you can start using command methods: 22 | 23 | ```js 24 | import { version, item, connect } from "@1password/op-js"; 25 | 26 | // Some command functions may be directly imported 27 | version(); 28 | 29 | // But most exist on their parent command's object 30 | item.get("x1oszeq62e2ys32v9a3l2sgcwly"); 31 | 32 | // And sub-commands are nested even further 33 | connect.group.revoke({ 34 | group: "MyGroup", 35 | allServers: true, 36 | }); 37 | ``` 38 | 39 | The CLI takes flags as `kebab-case`, however to align better with JS object convention all flags should be provided as `camelCase`. 40 | 41 | ### Flags 42 | 43 | All command methods support support [global command flags](https://developer.1password.com/docs/cli/reference#global-flags), as well as their own flags, but this package also provides a helper to set global command flags do you don't need to each time. For example: 44 | 45 | ```js 46 | import { setGlobalFlags } from "@1password/op-js"; 47 | 48 | setGlobalFlags({ 49 | account: "example.1password.com", 50 | }); 51 | ``` 52 | 53 | Note that you should not try to set the `--format` flag as this is set under the hood to `json` for all commands that can return JSON format; it is otherwise a string response. 54 | 55 | ### Validating the CLI 56 | 57 | Since this package depends on the 1Password CLI it's up to the user to install it, and the types may depend on a specific version. There is a function that your application can call to validate that the user has the CLI installed at a specific version: 58 | 59 | ```js 60 | import { validateCli } from "@1password/op-js"; 61 | 62 | validateCli().catch((error) => { 63 | console.log("CLI is not valid:", error.message); 64 | }); 65 | 66 | // defaults to the recommended version, but you can supply a semver: 67 | validateCli(">=2.3.1").catch((error) => { 68 | console.log("CLI is not valid:", error.message); 69 | }); 70 | ``` 71 | 72 | ### Authentication 73 | 74 | By default `op-js` uses system authentication (e.g. biometrics), but it also supports automated authentication via [Connect Server](https://developer.1password.com/docs/connect) or [Service Account](https://developer.1password.com/docs/service-accounts). 75 | 76 | **Connect** 77 | 78 | If you've got a Connect Server set up you can supply your host and token: 79 | 80 | ``` 81 | import { setConnect } from "@1password/op-js"; 82 | 83 | setConnect("https://connect.myserver.com", "1kjhd9872hd981865s"); 84 | ``` 85 | 86 | Alternatively you can use environment variables when executing the code that uses `op-js`: 87 | 88 | ``` 89 | OP_CONNECT_HOST=https://connect.myserver.com 90 | OP_CONNECT_TOKEN=1kjhd9872hd981865s 91 | ``` 92 | 93 | **Service Account** 94 | 95 | If you're using service accounts you can supply your token: 96 | 97 | ``` 98 | import { setServiceAccount } from "@1password/op-js"; 99 | 100 | setServiceAccount("1kjhd9872hd981865s"); 101 | ``` 102 | 103 | Alternatively you can use environment variables when executing the code that uses `op-js`: 104 | 105 | ``` 106 | OP_SERVICE_ACCOUNT_TOKEN=1kjhd9872hd981865s 107 | ``` 108 | 109 | ### Available commands and functions 110 | 111 | There are roughly 70 commands available for use, so you're encouraged to check out the main [`index.ts`](./src/index.ts) file to get a better sense of what's available. Generally, though, here are the top-level commands/namespaces you can import: 112 | 113 | - `version` - Retrieve the current version of the CLI 114 | - `inject` - Inject secrets into a config file 115 | - `read` - Read a secret by secret references 116 | - `account` - Manage accounts 117 | - `document` - Manage documents in a vault 118 | - `eventsApi` - Create an Events API integration token 119 | - `connect` - Manage Connect groups, services, tokens, and vaults 120 | - `item` - Manage vault items and templates 121 | - `vault` - Manage account vaults 122 | - `user` - Manage account users 123 | - `group` - Manage groups and their users 124 | - `whoami` - Get details about the authenticated account 125 | 126 | ## Contributing and feedback 127 | 128 | 🐛 If you find an issue you'd like to report, or otherwise have feedback, please [file a new Issue](https://github.com/1Password/op-js/issues/new). 129 | 130 | 🧑‍💻 If you'd like to contribute to the project please start by filing or commenting on an [Issue](https://github.com/1Password/op-js/issues) so we can track the work. Refer to the [Contributing doc](https://github.com/1Password/op-js/blob/main/CONTRIBUTING.md) for development setup instructions. 131 | 132 | 💬 Share your feedback and connect with the Developer Products team in the [1Password Developers Slack](https://developer.1password.com/joinslack) workspace. 133 | 134 | ## License 135 | 136 | MIT 137 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types/build/Config').DefaultOptions} */ 2 | module.exports = { 3 | roots: [""], 4 | testEnvironment: "jest-environment-jsdom", 5 | preset: "ts-jest/presets/js-with-ts", 6 | transform: { 7 | "^.+\\.(ts|js)?$": "ts-jest", 8 | }, 9 | testPathIgnorePatterns: [ 10 | "/dist", 11 | "/node_modules", 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { setGlobalFlags } from "./src"; 3 | 4 | declare global { 5 | namespace jest { 6 | interface Matchers { 7 | toMatchSchema(schema: Joi.AnySchema): R; 8 | } 9 | } 10 | } 11 | 12 | expect.extend({ 13 | toMatchSchema: (receivedInput: any, schema: Joi.AnySchema) => { 14 | const { error } = schema.validate(receivedInput); 15 | const pass = error === undefined; 16 | return { 17 | message: () => 18 | pass 19 | ? "Success" 20 | : `Error: ${error.message}\n\nReceived: ${JSON.stringify( 21 | receivedInput, 22 | null, 23 | 2, 24 | )}`, 25 | pass, 26 | }; 27 | }, 28 | }); 29 | 30 | if (process.env.npm_lifecycle_event === "test:integration") { 31 | for (const envVar of ["ACCOUNT", "VAULT"]) { 32 | if (!process.env[`OP_${envVar}`]) { 33 | throw new Error( 34 | `OP_${envVar} environment variable is required for integration tests.`, 35 | ); 36 | } 37 | } 38 | 39 | setGlobalFlags({ 40 | account: process.env.OP_ACCOUNT, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@1password/op-js", 3 | "version": "0.1.13", 4 | "description": "A typed JS wrapper for the 1Password CLI", 5 | "main": "./dist/index.js", 6 | "types": "./dist/src/index.d.ts", 7 | "files": [ 8 | "dist/" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/1Password/op-js" 13 | }, 14 | "license": "MIT", 15 | "scripts": { 16 | "build": "license-checker-rseidelsohn --direct --files licenses && yarn compile --minify && tsc -p tsconfig.release.json --emitDeclarationOnly", 17 | "compile": "esbuild src/index.ts src/cli.ts --platform=node --format=cjs --outdir=dist", 18 | "eslint": "eslint -c .eslintrc.json 'src/*.ts'", 19 | "prepare": "husky install", 20 | "prettier": "prettier --check 'src/*.ts'", 21 | "test:unit": "jest --testMatch '/src/*.test.ts'", 22 | "test:integration": "jest --testMatch '/tests/*.test.ts' --setupFilesAfterEnv '/jest.setup.ts' --runInBand", 23 | "typecheck": "tsc -p tsconfig.release.json --noEmit", 24 | "watch": "yarn compile --watch" 25 | }, 26 | "prettier": "@1password/prettier-config", 27 | "lint-staged": { 28 | "src/*.ts": [ 29 | "prettier --write", 30 | "eslint -c .eslintrc.json --fix" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@1password/eslint-config": "^4.3.0", 35 | "@1password/prettier-config": "^1.1.3", 36 | "@types/jest": "^29.5.12", 37 | "@types/node": "^20.12.12", 38 | "@types/semver": "^7.5.8", 39 | "@typescript-eslint/eslint-plugin": "^7.9.0", 40 | "esbuild": "^0.21.2", 41 | "eslint": "^8.57.0", 42 | "husky": "^9.0.11", 43 | "jest": "^29.7.0", 44 | "jest-environment-jsdom": "^29.6.2", 45 | "joi": "^17.13.1", 46 | "license-checker-rseidelsohn": "^4.3.0", 47 | "lint-staged": "^15.2.2", 48 | "prettier": "^3.2.5", 49 | "prettier-plugin-organize-imports": "^3.2.4", 50 | "ts-jest": "^29.1.2", 51 | "typescript": "5.4.5" 52 | }, 53 | "dependencies": { 54 | "lookpath": "^1.2.2", 55 | "semver": "^7.6.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cli.test.ts: -------------------------------------------------------------------------------- 1 | import child_process from "child_process"; 2 | import * as lookpath from "lookpath"; 3 | import { 4 | camelToHyphen, 5 | CLI, 6 | cli, 7 | CLIError, 8 | createFieldAssignment, 9 | createFlags, 10 | defaultClientInfo, 11 | ExecutionError, 12 | parseFlagValue, 13 | sanitizeInput, 14 | semverToInt, 15 | ValidationError, 16 | } from "./cli"; 17 | 18 | jest.mock("child_process"); 19 | jest.mock("lookpath"); 20 | 21 | const fakeOpPath = "/path/to/op"; 22 | 23 | type cliCallArgs = [ 24 | string, 25 | string[], 26 | { 27 | stdio: string; 28 | input: string; 29 | env: Record; 30 | }, 31 | ]; 32 | 33 | const expectOpCommand = ( 34 | received: { 35 | call: cliCallArgs; 36 | }, 37 | expected: string, 38 | ): void => { 39 | const actual = `${received.call[0]} ${received.call[1].join(" ")}`; 40 | expected = `op ${expected} --format=json`; 41 | 42 | expect(actual).toBe(expected); 43 | }; 44 | 45 | export const executeSpy = ( 46 | params: Parameters, 47 | { 48 | error = null, 49 | stderr = "", 50 | stdout = "{}", 51 | }: { 52 | error?: null | Error; 53 | stderr?: string; 54 | stdout?: string; 55 | } = {}, 56 | ) => { 57 | jest.spyOn(child_process, "spawnSync").mockReturnValue({ 58 | error, 59 | stderr, 60 | stdout, 61 | }); 62 | 63 | const response = cli.execute(...params); 64 | const spy = child_process.spawnSync as jest.Mock; 65 | const call = spy.mock.calls[0] as cliCallArgs; 66 | spy.mockReset(); 67 | 68 | return { 69 | call, 70 | response, 71 | }; 72 | }; 73 | 74 | describe("ValidationError", () => { 75 | describe("not-found", () => { 76 | it("sets the correct message", () => { 77 | const error = new ValidationError("not-found"); 78 | expect(error.message).toBe("Could not find `op` executable"); 79 | }); 80 | }); 81 | 82 | describe("version", () => { 83 | const requiredVersion = "3.2.1"; 84 | const currentVersion = "1.2.3"; 85 | 86 | it("sets the correct message", () => { 87 | const error = new ValidationError( 88 | "version", 89 | requiredVersion, 90 | currentVersion, 91 | ); 92 | expect(error.message).toBe( 93 | `CLI version ${currentVersion} does not satisfy required version ${requiredVersion}`, 94 | ); 95 | }); 96 | 97 | it("attaches the current and required versions", () => { 98 | const error = new ValidationError( 99 | "version", 100 | requiredVersion, 101 | currentVersion, 102 | ); 103 | expect(error.requiredVersion).toBe(requiredVersion); 104 | expect(error.currentVersion).toBe(currentVersion); 105 | }); 106 | }); 107 | }); 108 | 109 | describe("CLIError", () => { 110 | const dateTime = "2022/06/04 17:59:15"; 111 | const message = "authorization prompt dismissed, please try again"; 112 | 113 | it("attaches the status code", () => { 114 | const error = new CLIError("", 1); 115 | expect(error.status).toBe(1); 116 | }); 117 | 118 | it("parses an error message from op CLI", () => { 119 | const error = new CLIError(`[ERROR] ${dateTime} ${message}`, 1); 120 | expect(error.timestamp).toEqual(new Date(dateTime)); 121 | expect(error.message).toEqual(message); 122 | }); 123 | 124 | it("gracefully handles not being able to parse op error", () => { 125 | const invalidError = "invalid error"; 126 | const error = new CLIError(invalidError, 1); 127 | expect(error.timestamp).toBeUndefined(); 128 | expect(error.message).toEqual("Unknown error"); 129 | expect(error.originalMessage).toEqual(invalidError); 130 | }); 131 | }); 132 | 133 | describe("semverToInt", () => { 134 | it("converts a semver string to build number", () => { 135 | expect(semverToInt("0.1.2")).toBe("000102"); 136 | expect(semverToInt("1.2.3")).toBe("010203"); 137 | expect(semverToInt("12.2.39")).toBe("120239"); 138 | expect(semverToInt("2.1.284")).toBe("0201284"); 139 | }); 140 | }); 141 | 142 | describe("camelToHyphen", () => { 143 | it("converts camel case to hyphens", () => { 144 | expect(camelToHyphen("someFlag")).toEqual("some-flag"); 145 | }); 146 | 147 | it("correctly handles pascal case", () => { 148 | expect(camelToHyphen("SomeFlag")).toEqual("some-flag"); 149 | }); 150 | }); 151 | 152 | describe("sanitizeInput", () => { 153 | it("should handle special characters", () => { 154 | expect(sanitizeInput('abc"test')).toEqual('abc\\"test'); 155 | expect(sanitizeInput('abc\\"test')).toEqual('abc\\"test'); 156 | 157 | expect(sanitizeInput("test.test")).toEqual("test.test"); 158 | expect(sanitizeInput("test\\.test")).toEqual("test\\.test"); 159 | 160 | expect(sanitizeInput("def$test")).toEqual("def\\$test"); 161 | expect(sanitizeInput("def\\$test")).toEqual("def\\$test"); 162 | 163 | expect(sanitizeInput("xyz'test")).toEqual("xyz\\'test"); 164 | expect(sanitizeInput("xyz\\'test")).toEqual("xyz\\'test"); 165 | 166 | expect(sanitizeInput("fds`test")).toEqual("fds\\`test"); 167 | expect(sanitizeInput("fds\\`test")).toEqual("fds\\`test"); 168 | }); 169 | 170 | it("should handle section delimiter", () => { 171 | expect(sanitizeInput("test.test")).toEqual("test.test"); 172 | expect(sanitizeInput("test\\.test.test")).toEqual("test\\.test.test"); 173 | }); 174 | }); 175 | 176 | describe("parseFlagValue", () => { 177 | it("parses string type values", () => { 178 | expect(parseFlagValue("foo")).toEqual("=foo"); 179 | }); 180 | 181 | it("parses string array type values", () => { 182 | expect(parseFlagValue(["foo", "bar"])).toEqual("=foo,bar"); 183 | }); 184 | 185 | it("parses boolean type values", () => { 186 | expect(parseFlagValue(true)).toEqual(""); 187 | }); 188 | 189 | it("parses type field selector values", () => { 190 | expect( 191 | parseFlagValue({ 192 | type: ["OTP"], 193 | }), 194 | ).toEqual("=type=OTP"); 195 | }); 196 | 197 | it("parses label field selector values", () => { 198 | expect( 199 | parseFlagValue({ 200 | label: ["username", "password"], 201 | }), 202 | ).toEqual("=label=username,label=password"); 203 | }); 204 | }); 205 | 206 | describe("createFlags", () => { 207 | it("creates flags from a flag object", () => { 208 | expect(createFlags({ someFlag: "foo" })).toEqual(["--some-flag=foo"]); 209 | }); 210 | 211 | it("ignores null and falsey values", () => { 212 | expect( 213 | createFlags({ someFlag: "foo", anotherFlag: false, andAnother: null }), 214 | ).toEqual(["--some-flag=foo"]); 215 | }); 216 | }); 217 | 218 | describe("createFieldAssignment", () => { 219 | it("creates a field assignment from a field assignment object", () => { 220 | expect(createFieldAssignment(["username", "text", "foo"])).toEqual( 221 | "username[text]=foo", 222 | ); 223 | expect(createFieldAssignment(["password", "concealed", "abc123"])).toEqual( 224 | "password[concealed]=abc123", 225 | ); 226 | }); 227 | }); 228 | 229 | describe("cli", () => { 230 | describe("setClientInfo", () => { 231 | it("allows you to specify custom user agent details", () => { 232 | const clientInfo = { 233 | name: "foo-bar", 234 | id: "FOO", 235 | build: "120239", 236 | }; 237 | cli.setClientInfo(clientInfo); 238 | 239 | const execute = executeSpy([["foo"]]); 240 | expect(execute.call[2].env).toEqual( 241 | expect.objectContaining({ 242 | OP_INTEGRATION_NAME: clientInfo.name, 243 | OP_INTEGRATION_ID: clientInfo.id, 244 | OP_INTEGRATION_BUILDNUMBER: clientInfo.build, 245 | }), 246 | ); 247 | 248 | // Reset client info 249 | cli.setClientInfo(defaultClientInfo); 250 | }); 251 | }); 252 | 253 | describe("connect", () => { 254 | it("does not set connect env vars if not supplied", () => { 255 | cli.connect = undefined; 256 | 257 | const execute = executeSpy([["foo"]]); 258 | const envVars = Object.keys(execute.call[2].env); 259 | 260 | expect(envVars).not.toContain("OP_CONNECT_HOST"); 261 | expect(envVars).not.toContain("OP_CONNECT_TOKEN"); 262 | }); 263 | 264 | it("passes connect env vars if supplied", () => { 265 | cli.connect = { 266 | host: "https://connect.myserver.com", 267 | token: "1kjhd9872hd981865s", 268 | }; 269 | 270 | const execute = executeSpy([["foo"]]); 271 | expect(execute.call[2].env).toEqual( 272 | expect.objectContaining({ 273 | OP_CONNECT_HOST: cli.connect.host, 274 | OP_CONNECT_TOKEN: cli.connect.token, 275 | }), 276 | ); 277 | 278 | // Reset connect info 279 | cli.connect = undefined; 280 | }); 281 | }); 282 | 283 | describe("service account", () => { 284 | it("does not set service account var if not supplied", () => { 285 | cli.serviceAccountToken = undefined; 286 | 287 | const execute = executeSpy([["foo"]]); 288 | const envVars = Object.keys(execute.call[2].env); 289 | 290 | expect(envVars).not.toContain("OP_SERVICE_ACCOUNT_TOKEN"); 291 | }); 292 | 293 | it("passes service account var if supplied", () => { 294 | cli.serviceAccountToken = "1kjhd9872hd981865s"; 295 | 296 | const execute = executeSpy([["foo"]]); 297 | expect(execute.call[2].env).toEqual( 298 | expect.objectContaining({ 299 | OP_SERVICE_ACCOUNT_TOKEN: cli.serviceAccountToken, 300 | }), 301 | ); 302 | 303 | // Reset service account info 304 | cli.serviceAccountToken = undefined; 305 | }); 306 | }); 307 | 308 | describe("validate", () => { 309 | it("throws an error when the op cli is not found", async () => { 310 | const lookpathSpy = jest 311 | .spyOn(lookpath, "lookpath") 312 | .mockResolvedValue(undefined); 313 | 314 | await expect(cli.validate()).rejects.toEqual( 315 | new ValidationError("not-found"), 316 | ); 317 | 318 | lookpathSpy.mockRestore(); 319 | }); 320 | 321 | it("throws an error when the cli does not meet the version requirements", async () => { 322 | const lookpathSpy = jest 323 | .spyOn(lookpath, "lookpath") 324 | .mockResolvedValue(fakeOpPath); 325 | const spawnSpy = jest 326 | .spyOn(child_process, "spawnSync") 327 | .mockReturnValue({ 328 | error: null, 329 | stderr: "", 330 | stdout: "1.0.0", 331 | }); 332 | 333 | await expect(cli.validate()).rejects.toEqual( 334 | new ValidationError("version", CLI.recommendedVersion, "1.0.0"), 335 | ); 336 | 337 | lookpathSpy.mockRestore(); 338 | spawnSpy.mockRestore(); 339 | }); 340 | 341 | it("does not throw when cli is fully valid", async () => { 342 | CLI.recommendedVersion = ">=2.0.0"; 343 | 344 | const lookpathSpy = jest 345 | .spyOn(lookpath, "lookpath") 346 | .mockResolvedValue(fakeOpPath); 347 | const spawnSpy = jest 348 | .spyOn(child_process, "spawnSync") 349 | .mockReturnValue({ 350 | error: null, 351 | stderr: "", 352 | stdout: "2.1.0", 353 | }); 354 | 355 | await expect(cli.validate()).resolves.toBeUndefined(); 356 | 357 | lookpathSpy.mockRestore(); 358 | spawnSpy.mockRestore(); 359 | }); 360 | 361 | it("can handle beta versions", async () => { 362 | CLI.recommendedVersion = ">=2.0.0"; 363 | 364 | const lookpathSpy = jest 365 | .spyOn(lookpath, "lookpath") 366 | .mockResolvedValue(fakeOpPath); 367 | const spawnSpy = jest 368 | .spyOn(child_process, "spawnSync") 369 | .mockReturnValue({ 370 | error: null, 371 | stderr: "", 372 | stdout: "2.0.1.beta.12", 373 | }); 374 | 375 | await expect(cli.validate()).resolves.toBeUndefined(); 376 | 377 | lookpathSpy.mockRestore(); 378 | spawnSpy.mockRestore(); 379 | }); 380 | 381 | it("can take a custom version", async () => { 382 | const lookpathSpy = jest 383 | .spyOn(lookpath, "lookpath") 384 | .mockResolvedValue(fakeOpPath); 385 | const spawnSpy = jest 386 | .spyOn(child_process, "spawnSync") 387 | .mockReturnValue({ 388 | error: null, 389 | stderr: "", 390 | stdout: "2.1.0", 391 | }); 392 | 393 | await expect(cli.validate(">=2.0.0")).resolves.toBeUndefined(); 394 | 395 | lookpathSpy.mockRestore(); 396 | spawnSpy.mockRestore(); 397 | }); 398 | }); 399 | 400 | describe("execute", () => { 401 | it("constructs and calls an op command", () => { 402 | const execute = executeSpy([ 403 | ["example", "command"], 404 | { 405 | args: ["howdy"], 406 | flags: { foo: "bar", lorem: true, howdy: ["dolor", "sit"] }, 407 | }, 408 | ]); 409 | expectOpCommand( 410 | execute, 411 | `example command howdy --foo=bar --lorem --howdy=dolor,sit`, 412 | ); 413 | }); 414 | 415 | it("handles field assignment arguments", () => { 416 | const execute = executeSpy([ 417 | ["foo"], 418 | { 419 | args: [ 420 | ["username", "text", "foo"], 421 | ["password", "concealed", "abc123"], 422 | ], 423 | }, 424 | ]); 425 | expectOpCommand( 426 | execute, 427 | `foo username[text]=foo password[concealed]=abc123`, 428 | ); 429 | }); 430 | 431 | it("throws on invalid args", () => { 432 | expect(() => 433 | executeSpy([ 434 | ["foo"], 435 | { 436 | args: [null], 437 | }, 438 | ]), 439 | ).toThrow(new TypeError("Invalid argument")); 440 | }); 441 | 442 | it("sanitizes input in commands, arguments, and flags", () => { 443 | const execute = executeSpy([ 444 | ['"foo'], 445 | { 446 | args: ['bar"'], 447 | flags: { $lorem: "`ipsum`" }, 448 | }, 449 | ]); 450 | expectOpCommand(execute, `\\"foo bar\\" --\\$lorem=\\\`ipsum\\\``); 451 | }); 452 | 453 | it("sanitizes field assignments", () => { 454 | const execute = executeSpy([ 455 | ["foo"], 456 | { 457 | args: [ 458 | // @ts-expect-error we're testing invalid input 459 | ["$username", "'text'", "\\foo"], 460 | ], 461 | }, 462 | ]); 463 | expectOpCommand(execute, `foo \\$username[\\'text\\']=\\\\foo`); 464 | }); 465 | 466 | it("throws if there's an error", () => { 467 | const message = "bar"; 468 | expect(() => 469 | executeSpy([["foo"]], { error: new Error(message) }), 470 | ).toThrow(new ExecutionError(message, 0)); 471 | }); 472 | 473 | it("throws if there's a stderr", () => { 474 | const stderr = "bar"; 475 | expect(() => executeSpy([["foo"]], { stderr })).toThrow( 476 | new CLIError(stderr, 0), 477 | ); 478 | }); 479 | 480 | it("parses command JSON responses by default", () => { 481 | const data = { foo: "bar" }; 482 | const execute = executeSpy([["foo"]], { stdout: JSON.stringify(data) }); 483 | expect(execute.response).toEqual(data); 484 | }); 485 | 486 | it("can also return non-JSON responses", () => { 487 | const message = "some message"; 488 | const execute = executeSpy([["foo"], { json: false }], { 489 | stdout: message, 490 | }); 491 | expect(execute.response).toEqual(message); 492 | }); 493 | 494 | it("passes in user agent env vars, using default client info", () => { 495 | const execute = executeSpy([["foo"]]); 496 | expect(execute.call[2].env).toEqual( 497 | expect.objectContaining({ 498 | OP_INTEGRATION_NAME: defaultClientInfo.name, 499 | OP_INTEGRATION_ID: defaultClientInfo.id, 500 | OP_INTEGRATION_BUILDNUMBER: defaultClientInfo.build, 501 | }), 502 | ); 503 | }); 504 | }); 505 | 506 | describe("globalFlags", () => { 507 | it("applies global flags to a command", () => { 508 | cli.globalFlags = { 509 | account: "my.b5test.com", 510 | isoTimestamps: true, 511 | }; 512 | 513 | const execute = executeSpy([["foo"]]); 514 | expectOpCommand(execute, `foo --account=my.b5test.com --iso-timestamps`); 515 | }); 516 | }); 517 | }); 518 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import { lookpath } from "lookpath"; 3 | import semverCoerce from "semver/functions/coerce"; 4 | import semverSatisfies from "semver/functions/satisfies"; 5 | import { version } from "../package.json"; 6 | import { 7 | FieldAssignment, 8 | FieldLabelSelector, 9 | FieldTypeSelector, 10 | GlobalFlags, 11 | } from "."; 12 | 13 | export type FlagValue = 14 | | string 15 | | string[] 16 | | boolean 17 | | FieldLabelSelector 18 | | FieldTypeSelector; 19 | export type Flags = Record; 20 | type Arg = string | FieldAssignment; 21 | 22 | export interface ClientInfo { 23 | name: string; 24 | id: string; 25 | build: string; 26 | } 27 | 28 | export type ValidationErrorType = "not-found" | "version"; 29 | export class ValidationError extends Error { 30 | public constructor( 31 | public type: ValidationErrorType, 32 | public requiredVersion?: string, 33 | public currentVersion?: string, 34 | ) { 35 | let message: string; 36 | switch (type) { 37 | case "not-found": { 38 | message = "Could not find `op` executable"; 39 | break; 40 | } 41 | case "version": { 42 | message = `CLI version ${currentVersion} does not satisfy required version ${requiredVersion}`; 43 | break; 44 | } 45 | } 46 | 47 | super(message); 48 | this.name = "ValidationError"; 49 | } 50 | } 51 | 52 | export class ExecutionError extends Error { 53 | public constructor( 54 | message: string, 55 | public status: number, 56 | ) { 57 | super(message); 58 | this.name = "ExecutionError"; 59 | } 60 | } 61 | 62 | export class CLIError extends ExecutionError { 63 | private static errorRegex = 64 | /\[ERROR] (\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}) (.+)/; 65 | public timestamp?: Date; 66 | 67 | public constructor( 68 | public originalMessage: string, 69 | status: number, 70 | ) { 71 | const errorMatch = originalMessage.match(CLIError.errorRegex); 72 | let parsedMessage: string; 73 | let parsedTimestamp: Date; 74 | 75 | if (errorMatch) { 76 | parsedMessage = errorMatch[2]; 77 | parsedTimestamp = new Date(errorMatch[1]); 78 | } else { 79 | parsedMessage = "Unknown error"; 80 | } 81 | 82 | super(parsedMessage, status); 83 | this.name = "CLIError"; 84 | this.timestamp = parsedTimestamp; 85 | } 86 | } 87 | 88 | export const semverToInt = (input: string) => 89 | input 90 | .split(".") 91 | .map((n) => n.padStart(2, "0")) 92 | .join(""); 93 | 94 | export const camelToHyphen = (str: string) => 95 | str.replaceAll(/([A-Za-z])(?=[A-Z])/g, "$1-").toLowerCase(); 96 | 97 | const specialCharacters = ['"', "$", "'", "\\", "`"]; 98 | const escapableCharacters = new Set([...specialCharacters, "."]); 99 | 100 | export const sanitizeInput = (str: string) => { 101 | let newStr = ""; 102 | let isEscaped = false; 103 | for (let i = 0; i < str.length; i++) { 104 | if (str[i] === "\\") { 105 | isEscaped = escapableCharacters.has(str[i + 1]); 106 | if (!isEscaped) { 107 | newStr += "\\"; 108 | } 109 | } else if (!isEscaped && specialCharacters.includes(str[i])) { 110 | newStr += "\\"; 111 | isEscaped = false; 112 | } 113 | newStr += str[i]; 114 | } 115 | return newStr; 116 | }; 117 | 118 | const equalArray = (a: any[], b: any[]) => 119 | a.length === b.length && a.every((val, index) => val === b[index]); 120 | 121 | export const parseFlagValue = (value: FlagValue) => { 122 | if (typeof value === "string") { 123 | return `=${sanitizeInput(value)}`; 124 | } 125 | 126 | if (Array.isArray(value)) { 127 | return `=${value.join(",")}`; 128 | } 129 | 130 | if (typeof value === "object") { 131 | let fields = ""; 132 | 133 | if ("label" in value) { 134 | fields += (value.label || []).map((label) => `label=${label}`).join(","); 135 | } 136 | 137 | if ("type" in value) { 138 | fields += (value.type || []).map((type) => `type=${type}`).join(","); 139 | } 140 | 141 | if (fields.length > 0) { 142 | return `=${sanitizeInput(fields)}`; 143 | } 144 | } 145 | 146 | // If we get here, it's a boolean and boolean CLI flags don't have a value 147 | return ""; 148 | }; 149 | 150 | export const createFlags = (flags: Flags): string[] => 151 | Object.entries(flags) 152 | .filter(([_, value]) => Boolean(value)) 153 | .map( 154 | ([flag, value]) => 155 | `--${camelToHyphen(sanitizeInput(flag))}${parseFlagValue(value)}`, 156 | ); 157 | 158 | export const createFieldAssignment = ([ 159 | label, 160 | type, 161 | value, 162 | ]: FieldAssignment): string => 163 | `${sanitizeInput(label)}[${sanitizeInput(type)}]=${sanitizeInput(value)}`; 164 | 165 | export const defaultClientInfo: ClientInfo = { 166 | name: "1Password for JavaScript", 167 | id: "JS", 168 | build: semverToInt(version), 169 | }; 170 | 171 | export class CLI { 172 | public static recommendedVersion = ">=2.4.0"; 173 | public clientInfo: ClientInfo = defaultClientInfo; 174 | public globalFlags: Partial = {}; 175 | public connect?: { host: string; token: string }; 176 | public serviceAccountToken: string; 177 | 178 | public setClientInfo(clientInfo: ClientInfo) { 179 | this.clientInfo = clientInfo; 180 | } 181 | 182 | public getVersion(): string { 183 | return this.execute([], { flags: { version: true }, json: false }); 184 | } 185 | 186 | public async validate(requiredVersion: string = CLI.recommendedVersion) { 187 | const cliExists = !!(await lookpath("op")); 188 | 189 | if (!cliExists) { 190 | throw new ValidationError("not-found"); 191 | } 192 | 193 | const version = this.getVersion(); 194 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call 195 | const semVersion = semverCoerce(version); 196 | 197 | if (!semverSatisfies(semVersion, requiredVersion)) { 198 | throw new ValidationError("version", requiredVersion, version); 199 | } 200 | } 201 | 202 | private createParts( 203 | subCommand: string[], 204 | args: Arg[], 205 | flags: Flags, 206 | json: boolean, 207 | ): string[] { 208 | const parts = subCommand.map((part) => sanitizeInput(part)); 209 | 210 | for (const arg of args) { 211 | if (typeof arg === "string") { 212 | parts.push(sanitizeInput(arg)); 213 | // If it's an array assume it's a field assignment 214 | } else if (Array.isArray(arg)) { 215 | parts.push(createFieldAssignment(arg)); 216 | } else { 217 | throw new TypeError("Invalid argument"); 218 | } 219 | } 220 | 221 | if (json) { 222 | flags = { ...flags, format: "json" }; 223 | } 224 | 225 | // Version >=2.6.2 of the CLI changed how it handled piped input 226 | // in order to fix an issue with item creation, but in the process 227 | // it broke piping for other commands. We have a macOS/Linux-only 228 | // workaround, but not one for Windows, so for now we cannot support 229 | // the inject command on Windows past this version until the CLI 230 | // team fixes the issue. 231 | if (equalArray(subCommand, ["inject"])) { 232 | const version = semverCoerce(cli.getVersion()); 233 | if (semverSatisfies(version, ">=2.6.2")) { 234 | if (process.platform === "win32") { 235 | throw new ExecutionError( 236 | "Inject is not supported on Windows for version >=2.6.2 of the CLI", 237 | 1, 238 | ); 239 | } else { 240 | flags = { ...flags, inFile: "/dev/stdin" }; 241 | } 242 | } 243 | } 244 | 245 | return [ 246 | ...parts, 247 | ...createFlags({ 248 | ...this.globalFlags, 249 | ...flags, 250 | }), 251 | ]; 252 | } 253 | 254 | public execute | void>( 255 | subCommand: string[], 256 | { 257 | args = [], 258 | flags = {}, 259 | stdin, 260 | json = true, 261 | }: { 262 | args?: Arg[]; 263 | flags?: Flags; 264 | stdin?: string | Record; 265 | json?: boolean; 266 | } = {}, 267 | ): TData { 268 | let input: NodeJS.ArrayBufferView; 269 | const parts = this.createParts(subCommand, args, flags, json); 270 | 271 | if (stdin) { 272 | input = Buffer.from( 273 | typeof stdin === "string" ? stdin : JSON.stringify(stdin), 274 | ); 275 | } 276 | 277 | const { status, error, stdout, stderr } = spawnSync("op", parts, { 278 | stdio: input ? "pipe" : ["ignore", "pipe", "pipe"], 279 | input, 280 | env: { 281 | ...process.env, 282 | ...(this.connect && { 283 | OP_CONNECT_HOST: this.connect.host, 284 | OP_CONNECT_TOKEN: this.connect.token, 285 | }), 286 | ...(this.serviceAccountToken && { 287 | OP_SERVICE_ACCOUNT_TOKEN: this.serviceAccountToken, 288 | }), 289 | OP_INTEGRATION_NAME: this.clientInfo.name, 290 | OP_INTEGRATION_ID: this.clientInfo.id, 291 | OP_INTEGRATION_BUILDNUMBER: this.clientInfo.build, 292 | }, 293 | }); 294 | 295 | if (error) { 296 | throw new ExecutionError(error.message, status); 297 | } 298 | 299 | const cliError = stderr.toString(); 300 | if (cliError.length > 0) { 301 | throw new CLIError(cliError, status); 302 | } 303 | 304 | const output = stdout.toString().trim(); 305 | 306 | if (output.length === 0) { 307 | return; 308 | } 309 | 310 | if (!json) { 311 | return output as TData; 312 | } 313 | 314 | try { 315 | return JSON.parse(output) as TData; 316 | } catch (error) { 317 | console.log(output); 318 | throw error; 319 | } 320 | } 321 | } 322 | 323 | export const cli = new CLI(); 324 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import semverCoerce from "semver/functions/coerce"; 2 | import semverSatisfies from "semver/functions/satisfies"; 3 | import { cli, ClientInfo, CLIError, Flags } from "./cli"; 4 | 5 | export { 6 | CLIError, 7 | ExecutionError, 8 | semverToInt, 9 | ValidationError, 10 | ValidationErrorType, 11 | } from "./cli"; 12 | 13 | type CommandFlags = Partial< 14 | TOptional & GlobalFlags 15 | >; 16 | 17 | // Section: Global Flags 18 | 19 | export interface GlobalFlags { 20 | account: string; 21 | cache: boolean; 22 | config: string; 23 | encoding: 24 | | "utf-8" 25 | | "shift-jis" 26 | | "shiftjis" 27 | | "sjis" 28 | | "s-jis" 29 | | "shift_jis" 30 | | "s_jis" 31 | | "gbk"; 32 | isoTimestamps: boolean; 33 | session: string; 34 | } 35 | 36 | /** 37 | * Set user agent information passed to the CLI. 38 | * 39 | * Note: this is intended for internal usage; using could result in unexpected behaviour 40 | */ 41 | export const setClientInfo = (clientInfo: ClientInfo) => 42 | cli.setClientInfo(clientInfo); 43 | 44 | /** 45 | * Set any of the {@link GlobalFlags} on the CLI command. 46 | */ 47 | export const setGlobalFlags = (flags: Partial) => { 48 | cli.globalFlags = flags; 49 | }; 50 | 51 | /** 52 | * Set a Connect host and token 53 | * 54 | * Alternative to running with `OP_CONNECT_HOST` and `OP_CONNECT_TOKEN` set 55 | * 56 | * {@link https://developer.1password.com/docs/connect/} 57 | */ 58 | export const setConnect = (host: string, token: string) => { 59 | cli.connect = { host, token }; 60 | }; 61 | 62 | /** 63 | * Set a Service Account token 64 | * 65 | * Alternative to running with `OP_SERVICE_ACCOUNT_TOKEN` set 66 | * 67 | * {@link https://developer.1password.com/docs/service-accounts/} 68 | */ 69 | export const setServiceAccount = (token: string) => { 70 | cli.serviceAccountToken = token; 71 | }; 72 | 73 | // Section: CLI setup 74 | 75 | /** 76 | * Validate that the user's CLI setup is valid for this wrapper. 77 | */ 78 | export const validateCli = async (requiredVersion?: string) => 79 | await cli.validate(requiredVersion); 80 | 81 | /** 82 | * Retrieve the current version of the CLI. 83 | */ 84 | export const version = () => cli.getVersion(); 85 | 86 | // Section: Secret Injection 87 | 88 | export const inject = { 89 | /** 90 | * Inject secrets into and return the data 91 | * 92 | * {@link https://developer.1password.com/docs/cli/reference/commands/inject} 93 | */ 94 | data: (input: string, flags: CommandFlags<{}> = {}) => 95 | cli.execute(["inject"], { 96 | flags, 97 | json: false, 98 | stdin: input, 99 | }), 100 | 101 | /** 102 | * Inject secrets into data and write the result to a file 103 | * 104 | * {@link https://developer.1password.com/docs/cli/reference/commands/inject} 105 | */ 106 | toFile: ( 107 | input: string, 108 | outFile: string, 109 | flags: CommandFlags<{ 110 | fileMode: string; 111 | force: boolean; 112 | }> = {}, 113 | ) => 114 | cli.execute(["inject"], { 115 | flags: { outFile, ...flags }, 116 | json: false, 117 | stdin: input, 118 | }), 119 | }; 120 | 121 | // Section: Reading Secret References 122 | 123 | export const read = { 124 | /** 125 | * Read a secret by secret reference and return its value 126 | * 127 | * {@link https://developer.1password.com/docs/cli/reference/commands/read} 128 | */ 129 | parse: ( 130 | reference: string, 131 | flags: CommandFlags<{ noNewline: boolean }> = {}, 132 | ) => cli.execute(["read"], { args: [reference], flags, json: false }), 133 | 134 | /** 135 | * Read a secret by secret reference and save it to a file 136 | * 137 | * Returns the path to the file. 138 | * 139 | * {@link https://developer.1password.com/docs/cli/reference/commands/read} 140 | */ 141 | toFile: ( 142 | reference: string, 143 | outputPath: string, 144 | flags: CommandFlags<{ noNewline: boolean }> = {}, 145 | ) => 146 | cli.execute(["read"], { 147 | args: [reference], 148 | flags: { outFile: outputPath, ...flags }, 149 | json: false, 150 | }), 151 | }; 152 | 153 | // Section: Accounts 154 | 155 | export type AccountType = 156 | | "BUSINESS" 157 | | "TEAM" 158 | | "FAMILY" 159 | | "INDIVIDUAL" 160 | | "UNKNOWN"; 161 | 162 | export type AccountState = 163 | | "REGISTERED" 164 | | "ACTIVE" 165 | | "SUSPENDED" 166 | | "DELETED" 167 | | "PURGING" 168 | | "PURGED" 169 | | "UNKNOWN"; 170 | 171 | export interface Account { 172 | id: string; 173 | name: string; 174 | domain: string; 175 | type: AccountType; 176 | state: AccountState; 177 | created_at: string; 178 | } 179 | 180 | export interface ListAccount { 181 | url: string; 182 | email: string; 183 | user_uuid: string; 184 | account_uuid: string; 185 | shorthand?: string; 186 | } 187 | 188 | export const account = { 189 | /** 190 | * Add a new 1Password account to sign in to for the first time. 191 | * 192 | * TODO: This cannot yet be implemented from the JS wrapper because it 193 | * requires interactive input from the CLI, which we do not support. 194 | * 195 | * add: ( 196 | * flags: CommandFlags< 197 | * { 198 | * raw: boolean; 199 | * shorthand: string; 200 | * signin: boolean; 201 | * }, 202 | * { 203 | * address: string; 204 | * email: string; 205 | * secretKey: string; 206 | * } 207 | * >, 208 | * ) => 209 | * cli.execute(["account", "add"], { 210 | * flags, 211 | * json: false, 212 | * }), 213 | */ 214 | 215 | /** 216 | * Remove a 1Password account from this device. 217 | * 218 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/account#account-forget} 219 | */ 220 | forget: ( 221 | account: string | null, 222 | flags: CommandFlags<{ all: boolean }> = {}, 223 | ) => 224 | cli.execute(["account", "forget"], { 225 | args: [account], 226 | flags, 227 | json: false, 228 | }), 229 | 230 | /** 231 | * Get details about your account. 232 | * 233 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/account#account-get} 234 | */ 235 | get: (flags: CommandFlags = {}) => 236 | cli.execute(["account", "get"], { 237 | flags, 238 | }), 239 | 240 | /** 241 | * List users and accounts set up on this device. 242 | * 243 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/account#account-list} 244 | */ 245 | list: (flags: CommandFlags = {}) => 246 | cli.execute(["account", "list"], { 247 | flags, 248 | }), 249 | }; 250 | 251 | // Section: Who Am I 252 | 253 | export const whoami = (): ListAccount | null => { 254 | try { 255 | return cli.execute(["whoami"]); 256 | } catch (error) { 257 | if (error instanceof CLIError && error.message.includes("signed in")) { 258 | return null; 259 | } else { 260 | throw error; 261 | } 262 | } 263 | }; 264 | 265 | // Section: Documents 266 | 267 | export interface Document { 268 | id: string; 269 | title: string; 270 | version: number; 271 | vault: { 272 | id: string; 273 | name: string; 274 | }; 275 | last_edited_by?: string; 276 | created_at: string; 277 | updated_at: string; 278 | "overview.ainfo"?: string; 279 | } 280 | 281 | export interface CreatedDocument { 282 | uuid: string; 283 | createdAt: string; 284 | updatedAt: string; 285 | vaultUuid: string; 286 | } 287 | 288 | export const document = { 289 | /** 290 | * Create a document item with data or a file on disk. 291 | * 292 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/document#document-create} 293 | */ 294 | create: ( 295 | dataOrFile: string, 296 | flags: CommandFlags<{ 297 | fileName: string; 298 | tags: string[]; 299 | title: string; 300 | vault: string; 301 | }> = {}, 302 | fromFile = false, 303 | ) => 304 | cli.execute(["document", "create"], { 305 | args: [fromFile ? dataOrFile : ""], 306 | flags, 307 | stdin: fromFile ? undefined : dataOrFile, 308 | }), 309 | 310 | /** 311 | * Permanently delete a document. 312 | * 313 | * Set `archive` to move it to the Archive instead. 314 | * 315 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/document#document-delete} 316 | */ 317 | delete: ( 318 | nameOrId: string, 319 | flags: CommandFlags<{ archive: boolean; vault: string }> = {}, 320 | ) => 321 | cli.execute(["document", "delete"], { 322 | args: [nameOrId], 323 | flags, 324 | }), 325 | 326 | /** 327 | * Update a document. 328 | * 329 | * Replaces the file contents with the provided file path or data. 330 | * 331 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/document#document-edit} 332 | */ 333 | edit: ( 334 | nameOrId: string, 335 | dataOrFile: string, 336 | flags: CommandFlags<{ 337 | fileName: string; 338 | tags: string[]; 339 | title: string; 340 | vault: string; 341 | }> = {}, 342 | fromFile = false, 343 | ) => 344 | cli.execute(["document", "edit"], { 345 | args: [nameOrId, fromFile ? dataOrFile : ""], 346 | flags, 347 | stdin: fromFile ? undefined : dataOrFile, 348 | }), 349 | 350 | /** 351 | * Download a document and return its contents. 352 | * 353 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/document#document-get} 354 | */ 355 | get: ( 356 | nameOrId: string, 357 | flags: CommandFlags<{ 358 | includeArchive: boolean; 359 | vault: string; 360 | }> = {}, 361 | ) => 362 | cli.execute(["document", "get"], { 363 | args: [nameOrId], 364 | flags, 365 | json: false, 366 | }), 367 | 368 | /** 369 | * Download a document and save it to a file. 370 | * 371 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/document#document-get} 372 | */ 373 | toFile: ( 374 | nameOrId: string, 375 | outputPath: string, 376 | flags: CommandFlags<{ 377 | includeArchive: boolean; 378 | vault: string; 379 | }> = {}, 380 | ) => 381 | cli.execute(["document", "get"], { 382 | args: [nameOrId], 383 | flags: { 384 | output: outputPath, 385 | ...flags, 386 | }, 387 | json: false, 388 | }), 389 | 390 | /** 391 | * List documents. 392 | * 393 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/document#document-list} 394 | */ 395 | list: ( 396 | flags: CommandFlags<{ 397 | includeArchive: boolean; 398 | vault: string; 399 | }> = {}, 400 | ) => 401 | cli.execute(["document", "list"], { 402 | flags, 403 | }), 404 | }; 405 | 406 | // Section: Events API 407 | 408 | export const eventsApi = { 409 | /** 410 | * Create an Events API integration token. 411 | * 412 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/events-api#subcommands} 413 | */ 414 | create: ( 415 | name: string, 416 | flags: CommandFlags<{ 417 | expiresIn: string; 418 | features: ("signinattempts" | "itemusages")[]; 419 | }> = {}, 420 | ) => 421 | cli.execute(["events-api", "create"], { 422 | args: [name], 423 | flags, 424 | json: false, 425 | }), 426 | }; 427 | 428 | // Section: Connect 429 | 430 | export type ConnectServerState = "ACTIVE" | "REVOKED"; 431 | 432 | export interface VaultClaim { 433 | id: string; 434 | acl: VaultPermisson[]; 435 | } 436 | 437 | export interface ConnectServer { 438 | id: string; 439 | name: string; 440 | state: UserState; 441 | created_at: string; 442 | creator_id: string; 443 | tokens_version: number; 444 | } 445 | 446 | export interface ConnectServerToken { 447 | id: string; 448 | name: string; 449 | state: ConnectServerState; 450 | issuer: string; 451 | audience: string; 452 | features: string[]; 453 | vaults: VaultClaim[]; 454 | created_at: string; 455 | integration_id: string; 456 | } 457 | 458 | export const connect = { 459 | group: { 460 | /** 461 | * Grant a group access to manage Secrets Automation. 462 | * 463 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-group-grant} 464 | */ 465 | grant: ( 466 | group: string, 467 | flags: CommandFlags<{ 468 | allServers: boolean; 469 | server: string; 470 | }> = {}, 471 | ) => 472 | cli.execute(["connect", "group", "grant"], { 473 | flags: { group, ...flags }, 474 | json: false, 475 | }), 476 | 477 | /** 478 | * Revoke a group's access to manage Secrets Automation. 479 | * 480 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-group-revoke} 481 | */ 482 | revoke: ( 483 | group: string, 484 | flags: CommandFlags<{ 485 | allServers: boolean; 486 | server: string; 487 | }> = {}, 488 | ) => 489 | cli.execute(["connect", "group", "revoke"], { 490 | flags: { group, ...flags }, 491 | json: false, 492 | }), 493 | }, 494 | 495 | server: { 496 | /** 497 | * Add a 1Password Connect server to your account and generate a credentials file for it. 498 | * 499 | * Creates a credentials file in the CWD. 500 | * 501 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-server-create} 502 | */ 503 | create: ( 504 | name: string, 505 | flags: CommandFlags<{ 506 | vaults: string[]; 507 | }> = {}, 508 | ) => 509 | cli.execute(["connect", "server", "create"], { 510 | args: [name], 511 | flags, 512 | json: false, 513 | }), 514 | 515 | /** 516 | * Remove a Connect server. 517 | * 518 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-server-delete} 519 | */ 520 | delete: (nameOrId: string, flags: CommandFlags = {}) => 521 | cli.execute(["connect", "server", "delete"], { 522 | args: [nameOrId], 523 | flags, 524 | }), 525 | 526 | /** 527 | * Rename a Connect server. 528 | * 529 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-server-edit} 530 | */ 531 | edit: (nameOrId: string, newName: string, flags: CommandFlags<{}> = {}) => 532 | cli.execute(["connect", "server", "edit"], { 533 | args: [nameOrId], 534 | flags: { name: newName, ...flags }, 535 | json: false, 536 | }), 537 | 538 | /** 539 | * Get details about a Connect server. 540 | * 541 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-server-get} 542 | */ 543 | get: (nameOrId: string, flags: CommandFlags = {}) => 544 | cli.execute(["connect", "server", "get"], { 545 | args: [nameOrId], 546 | flags, 547 | }), 548 | 549 | /** 550 | * Get a list of Connect servers. 551 | * 552 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-server-list} 553 | */ 554 | list: (flags: CommandFlags = {}) => 555 | cli.execute(["connect", "server", "list"], { 556 | flags, 557 | }), 558 | }, 559 | 560 | token: { 561 | /** 562 | * Issue a new token for a Connect server. 563 | * 564 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-token-create} 565 | */ 566 | create: ( 567 | name: string, 568 | server: string, 569 | flags: CommandFlags<{ 570 | expiresIn: string; 571 | vaults: string[]; 572 | }> = {}, 573 | ) => 574 | cli.execute(["connect", "token", "create"], { 575 | args: [name], 576 | flags: { server, ...flags }, 577 | json: false, 578 | }), 579 | 580 | /** 581 | * Revoke a token for a Connect server. 582 | * 583 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-token-delete} 584 | */ 585 | delete: ( 586 | token: string, 587 | flags: CommandFlags<{ 588 | server: string; 589 | }> = {}, 590 | ) => 591 | cli.execute(["connect", "token", "delete"], { 592 | args: [token], 593 | flags, 594 | json: false, 595 | }), 596 | 597 | /** 598 | * Rename a Connect token. 599 | * 600 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-token-edit} 601 | */ 602 | edit: ( 603 | token: string, 604 | newName: string, 605 | flags: CommandFlags<{ 606 | server: string; 607 | }> = {}, 608 | ) => 609 | cli.execute(["connect", "token", "edit"], { 610 | args: [token], 611 | flags: { name: newName, ...flags }, 612 | json: false, 613 | }), 614 | 615 | /** 616 | * List tokens for Connect servers. 617 | * 618 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-token-list} 619 | */ 620 | list: ( 621 | flags: CommandFlags<{ 622 | server: string; 623 | }> = {}, 624 | ) => 625 | cli.execute(["connect", "token", "list"], { 626 | flags, 627 | }), 628 | }, 629 | 630 | vault: { 631 | /** 632 | * Grant a Connect server access to a vault. 633 | * 634 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-vault-grant} 635 | */ 636 | grant: (server: string, vault: string, flags: CommandFlags<{}> = {}) => 637 | cli.execute(["connect", "vault", "grant"], { 638 | flags: { server, vault, ...flags }, 639 | json: false, 640 | }), 641 | 642 | /** 643 | * Revoke a Connect server's access to a vault. 644 | * 645 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/connect#connect-vault-revoke} 646 | */ 647 | revoke: (server: string, vault: string, flags: CommandFlags<{}> = {}) => 648 | cli.execute(["connect", "vault", "revoke"], { 649 | flags: { server, vault, ...flags }, 650 | json: false, 651 | }), 652 | }, 653 | }; 654 | 655 | // Section: Items 656 | export type InputCategory = 657 | | "Email Account" 658 | | "Medical Record" 659 | | "Password" 660 | | "Bank Account" 661 | | "Membership" 662 | | "Reward Program" 663 | | "Credit Card" 664 | | "Driver License" 665 | | "Outdoor License" 666 | | "Passport" 667 | | "Wireless Router" 668 | | "Social Security Number" 669 | | "Software License" 670 | | "API Credential" 671 | | "Database" 672 | | "Document" 673 | | "Identity" 674 | | "Login" 675 | | "Secure Note" 676 | | "Server" 677 | // Disabled until CLI gets this category 678 | // | "Crypto Wallet" 679 | | "SSH Key"; 680 | 681 | export type OutputCategory = 682 | | "EMAIL_ACCOUNT" 683 | | "MEDICAL_RECORD" 684 | | "PASSWORD" 685 | | "BANK_ACCOUNT" 686 | | "MEMBERSHIP" 687 | | "REWARD_PROGRAM" 688 | | "CREDIT_CARD" 689 | | "DRIVER_LICENSE" 690 | | "OUTDOOR_LICENSE" 691 | | "PASSPORT" 692 | | "WIRELESS_ROUTER" 693 | | "SOCIAL_SECURITY_NUMBER" 694 | | "SOFTWARE_LICENSE" 695 | | "API_CREDENTIAL" 696 | | "DATABASE" 697 | | "DOCUMENT" 698 | | "IDENTITY" 699 | | "LOGIN" 700 | | "SECURE_NOTE" 701 | | "SERVER" 702 | // Disabled until CLI gets this category 703 | // | "CRYPTO_WALLET" 704 | | "SSH_KEY"; 705 | 706 | export type PasswordStrength = 707 | | "TERRIBLE" 708 | | "WEAK" 709 | | "FAIR" 710 | | "GOOD" 711 | | "VERY_GOOD" 712 | | "EXCELLENT" 713 | | "FANTASTIC"; 714 | 715 | // These are the possible field types you can 716 | // use to *create* an item 717 | export type FieldAssignmentType = 718 | | "concealed" 719 | | "text" 720 | | "email" 721 | | "url" 722 | | "date" 723 | | "monthYear" 724 | | "phone" 725 | // Used for deleting a field 726 | | "delete"; 727 | 728 | // These are the possible field types you can 729 | // use when querying fields by type 730 | export type QueryFieldType = 731 | | "string" 732 | | "concealed" 733 | | "date" 734 | | "phone" 735 | | "address" 736 | | "URL" 737 | | "email" 738 | | "monthYear" 739 | | "gender" 740 | | "cctype" 741 | | "ccnum" 742 | | "reference" 743 | | "menu" 744 | | "month" 745 | | "OTP" 746 | | "file" 747 | | "sshKey"; 748 | 749 | // These are the possible field types that can be 750 | // returned on a item's field 751 | export type ResponseFieldType = 752 | | "UNKNOWN" 753 | | "ADDRESS" 754 | | "CONCEALED" 755 | | "CREDIT_CARD_NUMBER" 756 | | "CREDIT_CARD_TYPE" 757 | | "DATE" 758 | | "EMAIL" 759 | | "GENDER" 760 | | "MENU" 761 | | "MONTH_YEAR" 762 | | "OTP" 763 | | "PHONE" 764 | | "REFERENCE" 765 | | "STRING" 766 | | "URL" 767 | | "FILE" 768 | | "SSHKEY"; 769 | 770 | export type FieldPurpose = "USERNAME" | "PASSWORD" | "NOTE"; 771 | 772 | export type FieldAssignment = [ 773 | label: string, 774 | type: FieldAssignmentType, 775 | value: string, 776 | purpose?: FieldPurpose, 777 | ]; 778 | 779 | export interface FieldLabelSelector { 780 | label?: string[]; 781 | } 782 | export interface FieldTypeSelector { 783 | type?: QueryFieldType[]; 784 | } 785 | 786 | export interface Section { 787 | id: string; 788 | label?: string; 789 | } 790 | 791 | interface BaseField { 792 | id: string; 793 | type: ResponseFieldType; 794 | label: string; 795 | reference?: string; 796 | section?: Section; 797 | tags?: string[]; 798 | } 799 | 800 | export type ValueField = BaseField & { 801 | value: string; 802 | }; 803 | 804 | export type GenericField = ValueField & { 805 | type: 806 | | "STRING" 807 | | "URL" 808 | | "ADDRESS" 809 | | "DATE" 810 | | "MONTH_YEAR" 811 | | "EMAIL" 812 | | "PHONE" 813 | | "REFERENCE"; 814 | }; 815 | 816 | export type UsernameField = ValueField & { 817 | type: "STRING"; 818 | purpose: "USERNAME"; 819 | }; 820 | 821 | export type NotesField = ValueField & { 822 | type: "STRING"; 823 | purpose: "NOTES"; 824 | }; 825 | 826 | export type OtpField = ValueField & { 827 | type: "OTP"; 828 | totp: string; 829 | }; 830 | 831 | export type PasswordField = ValueField & { 832 | type: "CONCEALED"; 833 | purpose: "PASSWORD"; 834 | entropy: number; 835 | password_details: { 836 | entropy?: number; 837 | generated?: boolean; 838 | strength: PasswordStrength; 839 | }; 840 | }; 841 | 842 | export interface File { 843 | id: string; 844 | name: string; 845 | size: number; 846 | content_path: string; 847 | section: Section; 848 | } 849 | 850 | export interface URL { 851 | label?: string; 852 | primary: boolean; 853 | href: string; 854 | } 855 | 856 | export type Field = 857 | | UsernameField 858 | | PasswordField 859 | | OtpField 860 | | NotesField 861 | | GenericField; 862 | 863 | export interface Item { 864 | id: string; 865 | title: string; 866 | version?: number; 867 | vault: { 868 | id: string; 869 | name: string; 870 | }; 871 | category: OutputCategory; 872 | last_edited_by?: string; 873 | created_at: string; 874 | updated_at: string; 875 | additional_information?: string; 876 | sections?: Section[]; 877 | tags?: string[]; 878 | fields?: Field[]; 879 | files?: File[]; 880 | urls?: URL[]; 881 | } 882 | 883 | export interface ItemTemplate { 884 | title: string; 885 | vault: { 886 | id: string; 887 | }; 888 | category: OutputCategory; 889 | fields: Field[]; 890 | } 891 | 892 | export interface ListItemTemplate { 893 | uuid: string; 894 | name: string; 895 | } 896 | 897 | export const item = { 898 | /** 899 | * Create an item. 900 | * 901 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-create} 902 | */ 903 | create: ( 904 | assignments: FieldAssignment[], 905 | flags: CommandFlags<{ 906 | category: InputCategory; 907 | dryRun: boolean; 908 | generatePassword: string | boolean; 909 | tags: string[]; 910 | template: string; 911 | title: string; 912 | url: string; 913 | vault: string; 914 | }> = {}, 915 | ) => { 916 | const options: { 917 | flags: Flags; 918 | args?: FieldAssignment[]; 919 | stdin?: Record; 920 | } = { 921 | flags, 922 | }; 923 | 924 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call 925 | const version = semverCoerce(cli.getVersion()); 926 | 927 | // Prior to 2.6.2 the CLI didn't handle field assignments correctly 928 | // within scripts, so if we're below that version we need to pipe the 929 | // fields in via stdin 930 | if (semverSatisfies(version, ">=2.6.2")) { 931 | options.args = assignments; 932 | } else { 933 | options.stdin = { 934 | fields: assignments.map(([label, type, value, purpose]) => { 935 | const data = { 936 | label, 937 | type, 938 | value, 939 | }; 940 | 941 | if (purpose) { 942 | Object.assign(data, { purpose }); 943 | } 944 | 945 | return data; 946 | }), 947 | }; 948 | } 949 | 950 | return cli.execute(["item", "create"], options); 951 | }, 952 | 953 | /** 954 | * Permanently delete an item. 955 | * 956 | * Set `archive` to move it to the Archive instead. 957 | * 958 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-delete} 959 | */ 960 | delete: ( 961 | nameOrIdOrLink: string, 962 | flags: CommandFlags<{ 963 | archive: boolean; 964 | vault: string; 965 | }> = {}, 966 | ) => 967 | cli.execute(["item", "delete"], { 968 | args: [nameOrIdOrLink], 969 | flags, 970 | json: false, 971 | }), 972 | 973 | /** 974 | * Edit an item's details. 975 | * 976 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-edit} 977 | */ 978 | edit: ( 979 | nameOrIdOrLink: string, 980 | assignments: FieldAssignment[], 981 | flags: CommandFlags<{ 982 | dryRun: boolean; 983 | generatePassword: string | boolean; 984 | tags: string[]; 985 | title: string; 986 | url: string; 987 | vault: string; 988 | }> = {}, 989 | ) => 990 | cli.execute(["item", "edit"], { 991 | args: [nameOrIdOrLink, ...assignments], 992 | flags, 993 | }), 994 | 995 | /** 996 | * Return details about an item. 997 | * 998 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-get} 999 | */ 1000 | get: ( 1001 | nameOrIdOrLink: string, 1002 | flags: CommandFlags<{ 1003 | fields: FieldLabelSelector | FieldTypeSelector; 1004 | includeArchive: boolean; 1005 | vault: string; 1006 | }> = {}, 1007 | ) => 1008 | cli.execute(["item", "get"], { 1009 | args: [nameOrIdOrLink], 1010 | flags, 1011 | }), 1012 | 1013 | /** 1014 | * Output the primary one-time password for this item. 1015 | * 1016 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-get} 1017 | */ 1018 | otp: ( 1019 | nameOrIdOrLink: string, 1020 | flags: CommandFlags<{ 1021 | includeArchive: boolean; 1022 | vault: string; 1023 | }> = {}, 1024 | ) => 1025 | cli.execute(["item", "get"], { 1026 | args: [nameOrIdOrLink], 1027 | flags: { otp: true, ...flags }, 1028 | json: false, 1029 | }), 1030 | 1031 | /** 1032 | * Get a shareable link for the item. 1033 | * 1034 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-get} 1035 | */ 1036 | shareLink: ( 1037 | nameOrIdOrLink: string, 1038 | flags: CommandFlags<{ 1039 | includeArchive: boolean; 1040 | vault: string; 1041 | }> = {}, 1042 | ) => 1043 | cli.execute(["item", "get"], { 1044 | args: [nameOrIdOrLink], 1045 | flags: { shareLink: true, ...flags }, 1046 | json: false, 1047 | }), 1048 | 1049 | /** 1050 | * List items. 1051 | * 1052 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-list} 1053 | */ 1054 | list: ( 1055 | flags: CommandFlags<{ 1056 | categories: InputCategory[]; 1057 | includeArchive: boolean; 1058 | long: boolean; 1059 | tags: string[]; 1060 | vault: string; 1061 | }> = {}, 1062 | ) => cli.execute(["item", "list"], { flags }), 1063 | 1064 | /** 1065 | * Share an item. 1066 | * 1067 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item#item-share} 1068 | */ 1069 | share: ( 1070 | nameOrId: string, 1071 | flags: CommandFlags<{ 1072 | emails: string[]; 1073 | expiry: string; 1074 | vault: string; 1075 | viewOnce: boolean; 1076 | }> = {}, 1077 | ) => 1078 | cli.execute(["item", "share"], { 1079 | args: [nameOrId], 1080 | flags, 1081 | json: false, 1082 | }), 1083 | 1084 | template: { 1085 | /** 1086 | * Return a template for an item type. 1087 | * 1088 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item/#item-template-get} 1089 | */ 1090 | get: (category: InputCategory, flags: CommandFlags = {}) => 1091 | cli.execute(["item", "template", "get"], { 1092 | args: [category], 1093 | flags, 1094 | }), 1095 | 1096 | /** 1097 | * Lists available item type templates. 1098 | * 1099 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/item/#item-template-list} 1100 | */ 1101 | list: (flags: CommandFlags = {}) => 1102 | cli.execute(["item", "template", "list"], { flags }), 1103 | }, 1104 | }; 1105 | 1106 | // Section: Vaults 1107 | 1108 | export type VaultIcon = 1109 | | "airplane" 1110 | | "application" 1111 | | "art-supplies" 1112 | | "bankers-box" 1113 | | "brown-briefcase" 1114 | | "brown-gate" 1115 | | "buildings" 1116 | | "cabin" 1117 | | "castle" 1118 | | "circle-of-dots" 1119 | | "coffee" 1120 | | "color-wheel" 1121 | | "curtained-window" 1122 | | "document" 1123 | | "doughnut" 1124 | | "fence" 1125 | | "galaxy" 1126 | | "gears" 1127 | | "globe" 1128 | | "green-backpack" 1129 | | "green-gem" 1130 | | "handshake" 1131 | | "heart-with-monitor" 1132 | | "house" 1133 | | "id-card" 1134 | | "jet" 1135 | | "large-ship" 1136 | | "luggage" 1137 | | "plant" 1138 | | "porthole" 1139 | | "puzzle" 1140 | | "rainbow" 1141 | | "record" 1142 | | "round-door" 1143 | | "sandals" 1144 | | "scales" 1145 | | "screwdriver" 1146 | | "shop" 1147 | | "tall-window" 1148 | | "treasure-chest" 1149 | | "vault-door" 1150 | | "vehicle" 1151 | | "wallet" 1152 | | "wrench"; 1153 | 1154 | export type VaultPermisson = 1155 | // Teams have three permissions 1156 | | "allow_viewing" 1157 | | "allow_editing" 1158 | | "allow_managing" 1159 | // Business has the above and more granular options 1160 | | "view_items" 1161 | | "view_and_copy_passwords" 1162 | | "view_item_history" 1163 | | "create_items" 1164 | | "edit_items" 1165 | | "archive_items" 1166 | | "delete_items" 1167 | | "import_items" 1168 | | "export_items" 1169 | | "copy_and_share_items" 1170 | | "print_items" 1171 | | "manage_vault"; 1172 | 1173 | export type VaultType = 1174 | | "PERSONAL" 1175 | | "EVERYONE" 1176 | | "TRANSFER" 1177 | | "USER_CREATED" 1178 | | "UNKNOWN"; 1179 | 1180 | export interface Vault { 1181 | id: string; 1182 | name: string; 1183 | attribute_version: number; 1184 | content_version: number; 1185 | type: VaultType; 1186 | created_at: string; 1187 | updated_at: string; 1188 | items?: number; 1189 | } 1190 | 1191 | export type AbbreviatedVault = Pick; 1192 | 1193 | interface VaultAccess { 1194 | vault_id: string; 1195 | vault_name: string; 1196 | permissions: string; 1197 | } 1198 | 1199 | export type VaultUserAccess = VaultAccess & { 1200 | user_id: string; 1201 | user_email: string; 1202 | }; 1203 | 1204 | export type VaultGroupAccess = VaultAccess & { 1205 | group_id: string; 1206 | group_name: string; 1207 | }; 1208 | 1209 | export interface VaultGroup { 1210 | id: string; 1211 | name: string; 1212 | description: string; 1213 | state: GroupState; 1214 | created_at: string; 1215 | permissions: VaultPermisson[]; 1216 | } 1217 | 1218 | export interface VaultUser { 1219 | id: string; 1220 | name: string; 1221 | email: string; 1222 | type: GroupRole; 1223 | state: UserState; 1224 | permissions: VaultPermisson[]; 1225 | } 1226 | 1227 | export const vault = { 1228 | /** 1229 | * Create a new vault 1230 | * 1231 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-create} 1232 | */ 1233 | create: ( 1234 | name: string, 1235 | flags: CommandFlags<{ 1236 | allowAdminsToManage: "true" | "false"; 1237 | description: string; 1238 | icon: VaultIcon; 1239 | }> = {}, 1240 | ) => cli.execute(["vault", "create"], { args: [name], flags }), 1241 | 1242 | /** 1243 | * Remove a vault. 1244 | * 1245 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-delete} 1246 | */ 1247 | delete: (nameOrId: string, flags: CommandFlags = {}) => 1248 | cli.execute(["vault", "delete"], { 1249 | args: [nameOrId], 1250 | flags, 1251 | json: false, 1252 | }), 1253 | 1254 | /** 1255 | * Edit a vault's name, description, icon or Travel Mode status. 1256 | * 1257 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-edit} 1258 | */ 1259 | edit: ( 1260 | nameOrId: string, 1261 | flags: CommandFlags<{ 1262 | description: string; 1263 | icon: VaultIcon; 1264 | name: string; 1265 | travelMode: "on" | "off"; 1266 | }> = {}, 1267 | ) => 1268 | cli.execute(["vault", "edit"], { 1269 | args: [nameOrId], 1270 | flags, 1271 | json: false, 1272 | }), 1273 | 1274 | /** 1275 | * Get details about a vault. 1276 | * 1277 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-get} 1278 | */ 1279 | get: (nameOrId: string, flags: CommandFlags = {}) => 1280 | cli.execute(["vault", "get"], { args: [nameOrId], flags }), 1281 | 1282 | /** 1283 | * List vaults. 1284 | * 1285 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-list} 1286 | */ 1287 | list: ( 1288 | flags: CommandFlags<{ 1289 | group: string; 1290 | user: string; 1291 | }> = {}, 1292 | ) => cli.execute(["vault", "list"], { flags }), 1293 | 1294 | group: { 1295 | /** 1296 | * Grant a group permissions in a vault. 1297 | * 1298 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-group-grant} 1299 | */ 1300 | grant: ( 1301 | flags: CommandFlags<{ 1302 | group: string; 1303 | permissions: VaultPermisson[]; 1304 | vault: string; 1305 | }> = {}, 1306 | ) => 1307 | cli.execute(["vault", "group", "grant"], { 1308 | flags: { noInput: true, ...flags }, 1309 | }), 1310 | 1311 | /** 1312 | * Revoke a group's permissions in a vault, in part or in full 1313 | * 1314 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-group-revoke} 1315 | */ 1316 | revoke: ( 1317 | flags: CommandFlags<{ 1318 | group: string; 1319 | permissions: VaultPermisson[]; 1320 | vault: string; 1321 | }> = {}, 1322 | ) => 1323 | cli.execute(["vault", "group", "revoke"], { 1324 | flags: { noInput: true, ...flags }, 1325 | }), 1326 | 1327 | /** 1328 | * List all the groups that have access to the given vault 1329 | * 1330 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-group-list} 1331 | */ 1332 | list: (vault: string, flags: CommandFlags = {}) => 1333 | cli.execute(["vault", "group", "list"], { 1334 | args: [vault], 1335 | flags, 1336 | }), 1337 | }, 1338 | 1339 | user: { 1340 | /** 1341 | * Grant a user permissions in a vault 1342 | * 1343 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-user-grant} 1344 | */ 1345 | grant: ( 1346 | flags: CommandFlags<{ 1347 | user: string; 1348 | permissions: VaultPermisson[]; 1349 | vault: string; 1350 | }> = {}, 1351 | ) => 1352 | cli.execute(["vault", "user", "grant"], { 1353 | flags: { noInput: true, ...flags }, 1354 | }), 1355 | 1356 | /** 1357 | * Revoke a user's permissions in a vault, in part or in full 1358 | * 1359 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-user-revoke} 1360 | */ 1361 | revoke: ( 1362 | flags: CommandFlags<{ 1363 | user: string; 1364 | permissions: VaultPermisson[]; 1365 | vault: string; 1366 | }> = {}, 1367 | ) => 1368 | cli.execute(["vault", "user", "revoke"], { 1369 | flags: { noInput: true, ...flags }, 1370 | }), 1371 | 1372 | /** 1373 | * List all users with access to the vault and their permissions 1374 | * 1375 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/vault#vault-user-list} 1376 | */ 1377 | list: (vault: string, flags: CommandFlags = {}) => 1378 | cli.execute(["vault", "user", "list"], { 1379 | args: [vault], 1380 | flags, 1381 | }), 1382 | }, 1383 | }; 1384 | 1385 | // Section: Users 1386 | 1387 | export type UserType = "MEMBER" | "GUEST" | "SERVICE_ACCOUNT" | "UNKNOWN"; 1388 | 1389 | export type UserState = 1390 | | "ACTIVE" 1391 | | "PENDING" 1392 | | "DELETED" 1393 | | "SUSPENDED" 1394 | | "RECOVERY_STARTED" 1395 | | "RECOVERY_ACCEPTED" 1396 | | "TRANSFER_PENDING" 1397 | | "TRANSFER_STARTED" 1398 | | "TRANSFER_ACCEPTED" 1399 | | "EMAIL_VERIFIED_BUT_REGISTRATION_INCOMPLETE" 1400 | | "TEAM_REGISTRATION_INITIATED" 1401 | | "UNKNOWN"; 1402 | 1403 | export interface User { 1404 | id: string; 1405 | name: string; 1406 | email: string; 1407 | type: UserType; 1408 | state: UserState; 1409 | created_at: string; 1410 | updated_at: string; 1411 | last_auth_at: string; 1412 | } 1413 | 1414 | export type AbbreviatedUser = Pick< 1415 | User, 1416 | "id" | "name" | "email" | "type" | "state" 1417 | >; 1418 | 1419 | export const user = { 1420 | /** 1421 | * Confirm a user who has accepted their invitation to the 1Password account. 1422 | * 1423 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-confirm} 1424 | */ 1425 | confirm: (emailOrNameOrId: string, flags: CommandFlags<{}> = {}) => 1426 | cli.execute(["user", "confirm"], { 1427 | args: [emailOrNameOrId], 1428 | flags, 1429 | json: false, 1430 | }), 1431 | 1432 | /** 1433 | * Confirm all users who have accepted their invitation to the 1Password account. 1434 | * 1435 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-confirm} 1436 | */ 1437 | confirmAll: (flags: CommandFlags<{}> = {}) => 1438 | cli.execute(["user", "confirm"], { 1439 | flags: { all: true, ...flags }, 1440 | json: false, 1441 | }), 1442 | 1443 | /** 1444 | * Remove a user and all their data from the account. 1445 | * 1446 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-delete} 1447 | */ 1448 | delete: (emailOrNameOrId: string, flags: CommandFlags = {}) => 1449 | cli.execute(["user", "delete"], { 1450 | args: [emailOrNameOrId], 1451 | flags, 1452 | json: false, 1453 | }), 1454 | 1455 | /** 1456 | * Change a user's name or Travel Mode status 1457 | * 1458 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-edit} 1459 | */ 1460 | edit: ( 1461 | emailOrNameOrId: string, 1462 | flags: CommandFlags<{ 1463 | name: string; 1464 | travelMode: "on" | "off"; 1465 | }> = {}, 1466 | ) => 1467 | cli.execute(["user", "edit"], { 1468 | args: [emailOrNameOrId], 1469 | flags, 1470 | json: false, 1471 | }), 1472 | 1473 | /** 1474 | * Get details about a user. 1475 | * 1476 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-get} 1477 | */ 1478 | get: (emailOrNameOrId: string, flags: CommandFlags<{}> = {}) => 1479 | cli.execute(["user", "get"], { args: [emailOrNameOrId], flags }), 1480 | 1481 | /** 1482 | * Get details about the current user. 1483 | * 1484 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-get} 1485 | */ 1486 | me: (flags: CommandFlags<{}> = {}) => 1487 | cli.execute(["user", "get"], { flags: { me: true, ...flags } }), 1488 | 1489 | /** 1490 | * Get the user's public key fingerprint. 1491 | * 1492 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-get} 1493 | */ 1494 | fingerprint: (emailOrNameOrId: string, flags: CommandFlags<{}> = {}) => 1495 | cli.execute(["user", "get"], { 1496 | args: [emailOrNameOrId], 1497 | flags: { fingerprint: true, ...flags }, 1498 | json: false, 1499 | }), 1500 | 1501 | /** 1502 | * Get the user's public key. 1503 | * 1504 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-get} 1505 | */ 1506 | publicKey: (emailOrNameOrId: string, flags: CommandFlags<{}> = {}) => 1507 | cli.execute(["user", "get"], { 1508 | args: [emailOrNameOrId], 1509 | flags: { publicKey: true, ...flags }, 1510 | json: false, 1511 | }), 1512 | 1513 | /** 1514 | * List users. 1515 | * 1516 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-list} 1517 | */ 1518 | list: ( 1519 | flags: CommandFlags<{ 1520 | group: string; 1521 | vault: string; 1522 | }> = {}, 1523 | ) => cli.execute(["user", "list"], { flags }), 1524 | 1525 | /** 1526 | * Provision a user in the authenticated account. 1527 | * 1528 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-invite} 1529 | */ 1530 | provision: ( 1531 | email: string, 1532 | name: string, 1533 | flags: CommandFlags<{ 1534 | language: string; 1535 | }>, 1536 | ) => 1537 | cli.execute(["user", "provision"], { 1538 | flags: { email, name, ...flags }, 1539 | }), 1540 | 1541 | /** 1542 | * Reactivate a suspended user. 1543 | * 1544 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-reactivate} 1545 | */ 1546 | reactivate: (emailOrNameOrId: string, flags: CommandFlags = {}) => 1547 | cli.execute(["user", "reactivate"], { 1548 | args: [emailOrNameOrId], 1549 | flags, 1550 | json: false, 1551 | }), 1552 | 1553 | /** 1554 | * Suspend a user. 1555 | * 1556 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/user#user-suspend} 1557 | */ 1558 | suspend: ( 1559 | emailOrNameOrId: string, 1560 | flags: CommandFlags<{ deauthorizeDevicesAfter: string }> = {}, 1561 | ) => 1562 | cli.execute(["user", "suspend"], { 1563 | args: [emailOrNameOrId], 1564 | flags, 1565 | json: false, 1566 | }), 1567 | }; 1568 | 1569 | // Section: Groups 1570 | 1571 | export type GroupRole = "MEMBER" | "MANAGER"; 1572 | 1573 | export type GroupState = "ACTIVE" | "DELETED" | "INACTIVE"; 1574 | 1575 | export type GroupType = 1576 | | "ADMINISTRATORS" 1577 | | "OWNERS" 1578 | | "RECOVERY" 1579 | | "TEAM_MEMBERS" 1580 | | "USER_DEFINED" 1581 | | "UNKNOWN_TYPE" 1582 | | "SECURITY"; 1583 | 1584 | export interface Group { 1585 | id: string; 1586 | name: string; 1587 | description: string; 1588 | state: GroupState; 1589 | created_at: string; 1590 | updated_at: string; 1591 | type: GroupType; 1592 | } 1593 | 1594 | export type CreatedGroup = Omit; 1595 | 1596 | export type AppreviatedGroup = Pick< 1597 | Group, 1598 | "id" | "name" | "description" | "state" | "created_at" 1599 | >; 1600 | 1601 | export type GroupUser = AbbreviatedUser & { 1602 | role: GroupRole; 1603 | }; 1604 | 1605 | export const group = { 1606 | /** 1607 | * Create a group. 1608 | * 1609 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-create} 1610 | */ 1611 | create: (name: string, flags: CommandFlags<{ description: string }> = {}) => 1612 | cli.execute(["group", "create"], { args: [name], flags }), 1613 | 1614 | /** 1615 | * Remove a group. 1616 | * 1617 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-delete} 1618 | */ 1619 | delete: (nameOrId: string, flags: CommandFlags = {}) => 1620 | cli.execute(["group", "delete"], { 1621 | args: [nameOrId], 1622 | flags, 1623 | json: false, 1624 | }), 1625 | 1626 | /** 1627 | * Change a group's name or description. 1628 | * 1629 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-edit} 1630 | */ 1631 | edit: ( 1632 | nameOrId: string, 1633 | flags: CommandFlags<{ 1634 | description: string; 1635 | name: string; 1636 | }> = {}, 1637 | ) => 1638 | cli.execute(["group", "edit"], { 1639 | args: [nameOrId], 1640 | flags, 1641 | json: false, 1642 | }), 1643 | 1644 | /** 1645 | * Get details about a group. 1646 | * 1647 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-get} 1648 | */ 1649 | get: (nameOrId: string, flags: CommandFlags = {}) => 1650 | cli.execute(["group", "get"], { args: [nameOrId], flags }), 1651 | 1652 | /** 1653 | * List groups. 1654 | * 1655 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-list} 1656 | */ 1657 | list: ( 1658 | flags: CommandFlags<{ 1659 | vault: string; 1660 | user: string; 1661 | }> = {}, 1662 | ) => cli.execute(["group", "list"], { flags }), 1663 | 1664 | user: { 1665 | /** 1666 | * Grant a user access to a group. 1667 | * 1668 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-user-grant} 1669 | */ 1670 | grant: ( 1671 | flags: CommandFlags<{ 1672 | group: string; 1673 | role: GroupRole; 1674 | user: string; 1675 | }> = {}, 1676 | ) => cli.execute(["group", "user", "grant"], { flags, json: false }), 1677 | 1678 | /** 1679 | * Retrieve users that belong to a group. 1680 | * 1681 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-user-list} 1682 | */ 1683 | list: (group: string, flags: CommandFlags = {}) => 1684 | cli.execute(["group", "user", "list"], { 1685 | args: [group], 1686 | flags, 1687 | }), 1688 | 1689 | /** 1690 | * Revoke a user's access to a vault or group. 1691 | * 1692 | * {@link https://developer.1password.com/docs/cli/reference/management-commands/group#group-user-revoke} 1693 | */ 1694 | revoke: ( 1695 | flags: CommandFlags<{ 1696 | group: string; 1697 | user: string; 1698 | }> = {}, 1699 | ) => cli.execute(["group", "user", "revoke"], { flags, json: false }), 1700 | }, 1701 | }; 1702 | -------------------------------------------------------------------------------- /tests/document.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmSync } from "fs"; 2 | import Joi from "joi"; 3 | import { document } from "../src"; 4 | 5 | describe("document", () => { 6 | it("CRUDs documents", () => { 7 | const create = document.create("Created Value", { 8 | vault: process.env.OP_VAULT, 9 | title: "Created Document", 10 | fileName: "created-doc.txt", 11 | }); 12 | expect(create).toMatchSchema( 13 | Joi.object({ 14 | uuid: Joi.string().required(), 15 | createdAt: Joi.string().required(), 16 | updatedAt: Joi.string().required(), 17 | vaultUuid: Joi.string().required(), 18 | }).required(), 19 | ); 20 | 21 | const list = document.list({ vault: process.env.OP_VAULT }); 22 | expect(list).toMatchSchema( 23 | Joi.array() 24 | .items({ 25 | id: Joi.string().required(), 26 | title: Joi.string().required(), 27 | version: Joi.number().required(), 28 | vault: { 29 | id: Joi.string().required().allow(""), 30 | name: Joi.string().allow(""), 31 | }, 32 | last_edited_by: Joi.string().optional(), 33 | created_at: Joi.string().required(), 34 | updated_at: Joi.string().required(), 35 | "overview.ainfo": Joi.string(), 36 | }) 37 | .required(), 38 | ); 39 | 40 | const edit = document.edit(create.uuid, "Updated Value", { 41 | title: "Updated Value", 42 | fileName: "updated-doc.txt", 43 | }); 44 | expect(edit).toBeUndefined(); 45 | 46 | const get = document.get(create.uuid); 47 | expect(get).toMatchSchema(Joi.string().required()); 48 | 49 | const fileName = `${__dirname}/test.txt`; 50 | const toFile = document.toFile(create.uuid, `${__dirname}/test.txt`); 51 | expect(toFile).toBeUndefined(); 52 | expect(existsSync(fileName)).toBe(true); 53 | rmSync(fileName); 54 | 55 | const del = document.delete(create.uuid); 56 | expect(del).toBeUndefined(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/eventsApi.test.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { eventsApi } from "../src"; 3 | 4 | describe("eventsApi", () => { 5 | it("creates an Events API token", () => { 6 | // eslint-disable-next-line no-restricted-syntax 7 | const random = Math.random().toString(); 8 | const create = eventsApi.create(`Token ${random}`, { 9 | expiresIn: "1m", 10 | features: ["signinattempts", "itemusages"], 11 | }); 12 | expect(create).toMatchSchema(Joi.string().required()); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/inject.test.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { inject, item } from "../src"; 3 | 4 | describe("inject", () => { 5 | describe("data", () => { 6 | it("returns injected data", () => { 7 | const value = "bar"; 8 | const injectable = item.create([["foo", "text", value]], { 9 | vault: process.env.OP_VAULT, 10 | category: "Login", 11 | title: "Injectable", 12 | }); 13 | const reference = injectable.fields.find( 14 | (f) => f.label === "foo", 15 | ).reference; 16 | 17 | const result = inject.data(`foo ${reference}`); 18 | expect(result).toMatchSchema(Joi.string().required()); 19 | expect(result).toEqual(`foo ${value}`); 20 | 21 | item.delete(injectable.id); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/item.test.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { item } from "../src"; 3 | 4 | const valueFieldSchema = Joi.object({ 5 | id: Joi.string().required(), 6 | type: Joi.string() 7 | .valid( 8 | "STRING", 9 | "URL", 10 | "ADDRESS", 11 | "DATE", 12 | "MONTH_YEAR", 13 | "EMAIL", 14 | "PHONE", 15 | "REFERENCE", 16 | ) 17 | .required(), 18 | purpose: Joi.string().valid("USERNAME", "PASSWORD", "NOTES").required(), 19 | label: Joi.string().required(), 20 | value: Joi.string().optional(), 21 | reference: Joi.string().required(), 22 | }); 23 | 24 | const valueFieldsSchema = Joi.array().items(valueFieldSchema); 25 | 26 | const itemSchema = Joi.object({ 27 | id: Joi.string().required(), 28 | title: Joi.string().required(), 29 | version: Joi.number().optional(), 30 | vault: { 31 | id: Joi.string().required().allow(""), 32 | name: Joi.string().allow(""), 33 | }, 34 | category: Joi.string().required(), 35 | last_edited_by: Joi.string().optional(), 36 | created_at: Joi.string().required(), 37 | updated_at: Joi.string().required(), 38 | additional_information: Joi.string().optional(), 39 | sections: Joi.array() 40 | .items({ 41 | id: Joi.string().required(), 42 | }) 43 | .optional(), 44 | tags: Joi.array().items(Joi.string().required()).optional(), 45 | fields: Joi.array() 46 | .items({ 47 | id: Joi.string().required(), 48 | type: Joi.string().required(), 49 | label: Joi.string().required(), 50 | purpose: Joi.string().optional(), 51 | value: Joi.string().optional(), 52 | reference: Joi.string().optional(), 53 | section: { 54 | id: Joi.string().required(), 55 | }, 56 | tags: Joi.array().items(Joi.string().required()).optional(), 57 | entropy: Joi.number().optional(), 58 | password_details: Joi.object({ 59 | entropy: Joi.number().optional(), 60 | generated: Joi.boolean().optional(), 61 | strength: Joi.string().required(), 62 | }) 63 | .optional() 64 | .allow({}), 65 | }) 66 | .optional(), 67 | files: Joi.array() 68 | .items({ 69 | id: Joi.string().required(), 70 | name: Joi.string().required(), 71 | size: Joi.number().required(), 72 | content_path: Joi.string().required(), 73 | section: { 74 | id: Joi.string().required(), 75 | }, 76 | }) 77 | .optional(), 78 | urls: Joi.array() 79 | .items({ 80 | label: Joi.string().optional(), 81 | primary: Joi.boolean().required(), 82 | href: Joi.string().required(), 83 | }) 84 | .optional(), 85 | }).required(); 86 | 87 | describe("item", () => { 88 | it("CRUDs items", () => { 89 | const create = item.create([["username", "text", "created"]], { 90 | vault: process.env.OP_VAULT, 91 | category: "Login", 92 | title: "Created Login", 93 | url: "https://example.com", 94 | }); 95 | expect(create).toMatchSchema(itemSchema); 96 | 97 | const edit = item.edit(create.id, [["username", "text", "updated"]], { 98 | title: "Updated Login", 99 | }); 100 | expect(edit).toMatchSchema(itemSchema); 101 | 102 | const getItem = item.get(create.id); 103 | expect(getItem).toMatchSchema(itemSchema); 104 | 105 | const getFieldByLabel = item.get(create.id, { 106 | fields: { label: ["username"] }, 107 | }); 108 | expect(getFieldByLabel).toMatchSchema(valueFieldSchema); 109 | 110 | const getFieldByType = item.get(create.id, { 111 | fields: { type: ["string"] }, 112 | }); 113 | expect(getFieldByType).toMatchSchema(valueFieldsSchema); 114 | 115 | const del = item.delete(create.id); 116 | expect(del).toBeUndefined(); 117 | }); 118 | 119 | it("CRUDs items with sections", () => { 120 | const create = item.create([["my\\.section.username", "text", "created"]], { 121 | vault: process.env.OP_VAULT, 122 | category: "Login", 123 | title: "Created Login", 124 | url: "https://example.com", 125 | generatePassword: true 126 | }); 127 | expect(create).toMatchSchema(itemSchema); 128 | 129 | const edit = item.edit(create.id, [["my\\.section.username", "text", "updated"]], { 130 | title: "Updated Login", 131 | }); 132 | expect(edit).toMatchSchema(itemSchema); 133 | 134 | const get = item.get(create.id); 135 | expect(get).toMatchSchema(itemSchema); 136 | 137 | const del = item.delete(create.id); 138 | expect(del).toBeUndefined(); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /tests/version.test.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { version } from "../src"; 3 | 4 | describe("version", () => { 5 | it("returns the version number", () => { 6 | expect(version()).toMatchSchema(Joi.string().required()); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/whoami.test.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { whoami } from "../src"; 3 | 4 | describe("whoami", () => { 5 | it("returns the authenticated user", () => { 6 | const result = whoami(); 7 | expect(result).toMatchSchema( 8 | Joi.alternatives( 9 | // If you're not authenticated, you'll get null 10 | null, 11 | // If you're authenticated, you'll get your account details 12 | Joi.object({ 13 | url: Joi.string().required(), 14 | email: Joi.string().required(), 15 | user_uuid: Joi.string().required(), 16 | account_uuid: Joi.string().required(), 17 | }), 18 | ), 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "AMD", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "Node", 8 | "resolveJsonModule": true, 9 | "noImplicitAny": false, 10 | "baseUrl": ".", 11 | "types": [ 12 | "node", 13 | "jest" 14 | ], 15 | }, 16 | "include": [ 17 | "jest.setup.ts", 18 | "src/*.ts", 19 | "tests/*.ts" 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declaration": true, 6 | }, 7 | "include": [ 8 | "src/index.ts", 9 | "src/cli.ts", 10 | ] 11 | } 12 | --------------------------------------------------------------------------------