diff --git a/.changeset/big-swans-own.md b/.changeset/big-swans-own.md
new file mode 100644
index 00000000..a163aad2
--- /dev/null
+++ b/.changeset/big-swans-own.md
@@ -0,0 +1,7 @@
+---
+"@dmno/encrypted-vault-plugin": patch
+"@dmno/1password-plugin": patch
+"dmno": patch
+---
+
+1password plugin improvements, related refactoring
diff --git a/example-repo/.dmno/config.mts b/example-repo/.dmno/config.mts
index 313b0ef3..a3c3c0bd 100644
--- a/example-repo/.dmno/config.mts
+++ b/example-repo/.dmno/config.mts
@@ -48,11 +48,6 @@ export default defineDmnoService({
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
},
-
- TEST_PROD_AUTH: {
- extends: 'boolean',
- },
-
// OP_TOKEN_PROD: {
// extends: OnePasswordTypes.serviceAccountToken,
// },
@@ -64,7 +59,7 @@ export default defineDmnoService({
value: OnePassSecretsDev.itemById("ut2dftalm3ugmxc6klavms6tfq", "bphvvrqjegfmd5yoz4buw2aequ", "username"),
},
OP_ITEM_BY_LINK: {
- value: OnePassSecretsDev.itemByLink("https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=bphvvrqjegfmd5yoz4buw2aequ&h=dmnoinc.1password.com", "sectiontest/section-child"),
+ value: OnePassSecretsDev.itemByLink("https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=bphvvrqjegfmd5yoz4buw2aequ&h=dmnoinc.1password.com", ""),
},
OP_ITEM_BY_REFERENCE: {
value: OnePassSecretsDev.itemByReference("op://dev test/example/username"),
@@ -89,78 +84,78 @@ export default defineDmnoService({
value: 'rootonly',
},
- // VAULT_ITEM_1: {
- // value: ProdVault.item(),
- // },
- // VAULT_ITEM_WITH_SWITCH: {
- // value: switchByNodeEnv({
- // _default: NonProdVault.item(),
- // staging: switchBy('CONTEXT', {
- // 'branch-preview': ProdVault.item(),
- // 'pr-preview': ProdVault.item(),
- // }),
- // production: ProdVault.item()
- // }),
- // },
+ VAULT_ITEM_1: {
+ value: ProdVault.item(),
+ },
+ VAULT_ITEM_WITH_SWITCH: {
+ value: switchByNodeEnv({
+ _default: NonProdVault.item(),
+ staging: switchBy('CONTEXT', {
+ 'branch-preview': ProdVault.item(),
+ 'pr-preview': ProdVault.item(),
+ }),
+ production: ProdVault.item()
+ }),
+ },
PICK_TEST: {
value: (ctx) => `pick-test--${DMNO_CONFIG.ROOT_ONLY}`,
},
- // CONTACT_EMAIL: {
- // extends: DmnoBaseTypes.email({
- // normalize: true,
- // }),
- // // required: true,
- // value: 'Test@test.com'
- // },
+ CONTACT_EMAIL: {
+ extends: DmnoBaseTypes.email({
+ normalize: true,
+ }),
+ // required: true,
+ value: 'Test@test.com'
+ },
- // SOME_IPV4: {
- // extends: DmnoBaseTypes.ipAddress,
- // required: true,
- // value: '100.200.1.1'
- // },
+ SOME_IPV4: {
+ extends: DmnoBaseTypes.ipAddress,
+ required: true,
+ value: '100.200.1.1'
+ },
- // SOME_IPV6: {
- // extends: DmnoBaseTypes.ipAddress({
- // version: 6,
- // normalize: true,
- // }),
- // required: true,
- // value: '2001:0DB8:85a3:0000:0000:8a2e:0370:7334'
- // },
+ SOME_IPV6: {
+ extends: DmnoBaseTypes.ipAddress({
+ version: 6,
+ normalize: true,
+ }),
+ required: true,
+ value: '2001:0DB8:85a3:0000:0000:8a2e:0370:7334'
+ },
- // SOME_PORT: {
- // extends: DmnoBaseTypes.port,
- // required: true,
- // value: '8080'
- // },
+ SOME_PORT: {
+ extends: DmnoBaseTypes.port,
+ required: true,
+ value: '8080'
+ },
- // SOME_SEMVER: {
- // extends: DmnoBaseTypes.semver({
- // normalize: true,
- // }),
- // required: true,
- // value: '1.2.3-ALPHA.1'
- // },
+ SOME_SEMVER: {
+ extends: DmnoBaseTypes.semver({
+ normalize: true,
+ }),
+ required: true,
+ value: '1.2.3-ALPHA.1'
+ },
- // SOME_DATE: {
- // extends: DmnoBaseTypes.isoDate,
- // required: true,
- // value: new Date().toISOString()
- // },
+ SOME_DATE: {
+ extends: DmnoBaseTypes.isoDate,
+ required: true,
+ value: new Date().toISOString()
+ },
- // SOME_UUID: {
- // extends: DmnoBaseTypes.uuid,
- // required: true,
- // value: '550e8400-e29b-41d4-a716-446655440000'
- // },
+ SOME_UUID: {
+ extends: DmnoBaseTypes.uuid,
+ required: true,
+ value: '550e8400-e29b-41d4-a716-446655440000'
+ },
- // SOME_MD5: {
- // extends: DmnoBaseTypes.md5,
- // required: true,
- // value: 'd41d8cd98f00b204e9800998ecf8427e'
- // },
+ SOME_MD5: {
+ extends: DmnoBaseTypes.md5,
+ required: true,
+ value: 'd41d8cd98f00b204e9800998ecf8427e'
+ },
}
});
diff --git a/packages/docs-site/src/assets/docs-images/plugins/1password/blob-item-example.png b/packages/docs-site/src/assets/docs-images/plugins/1password/blob-item-example.png
new file mode 100644
index 00000000..3d1b2a5a
Binary files /dev/null and b/packages/docs-site/src/assets/docs-images/plugins/1password/blob-item-example.png differ
diff --git a/packages/docs-site/src/content/docs/docs/plugins/1password.mdx b/packages/docs-site/src/content/docs/docs/plugins/1password.mdx
index 81799d8c..cd7818f5 100644
--- a/packages/docs-site/src/content/docs/docs/plugins/1password.mdx
+++ b/packages/docs-site/src/content/docs/docs/plugins/1password.mdx
@@ -6,103 +6,129 @@ description: DMNO's 1Password plugin allows you to securely access your stored s
import { Steps, Icon } from '@astrojs/starlight/components';
import TabbedCode from '@/components/TabbedCode.astro';
-DMNO's 1Password plugin allows you to securely access your stored secrets in 1Password. This plugin uses the 1Password CLI and can communicate by means of a [Service Account](https://developer.1password.com/docs/service-accounts) or during local development by [integrating with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration). It is compatible with any account type. Note that rate limits vary by account type, you can read more about that in the [1Password Developer documentation](https://developer.1password.com/docs/service-accounts/rate-limits/).
+DMNO's [1Password](https://1password.com/) plugin allows you to securely access your secrets stored in 1Password. This plugin uses their [JavaScript SDK](https://github.com/1Password/onepassword-sdk-js/) to authenticate via a [service account](https://developer.1password.com/docs/service-accounts). Additionally, for local development, you can opt-in to use your system-installed [1Password CLI](https://developer.1password.com/docs/cli/get-started/) and its [integration with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration), which uses your personal account gated behind biometric unlocking of the desktop app. This plugin is compatible with any 1Password account type (personal, family, teams, business), but note that [rate limits](https://developer.1password.com/docs/service-accounts/rate-limits/) vary by account type.
-## Installation
+## Installation & setup
-Install the package in your service(s) that will use config from 1password.
+Install the package in your service(s) that will use config from 1Password.
-----
-After installation, you'll need to initialize the plugin in your dmno config and wire it up to the config path that will hold your 1password service account token. It's ok if you have not created this service account yet - we'll do that in the next section.
+After installation, you'll need to initialize the plugin in your dmno config and wire it up to the config path that will hold your 1Password service account token. It's ok if you have not created this service account yet - we'll do that in the next section.
-```typescript title='.dmno/config.mts'
-import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
+```diff lang="ts" title='.dmno/config.mts'
++import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
-const OnePassBackend = new OnePasswordDmnoPlugin('1pass/prod', {
- token: configPath('OP_TOKEN'),
-});
++const OnePassBackend = new OnePasswordDmnoPlugin('1pass', {
++ token: configPath('OP_TOKEN'),
++});
export default defineDmnoService({
schema: {
- OP_TOKEN: {
- extends: OnePasswordTypes.serviceAccountToken,
- // NOTE - the type itself is already marked as sensitive 🔐
- },
++ OP_TOKEN: {
++ extends: OnePasswordTypes.serviceAccountToken,
++ // NOTE - the type itself is already marked as sensitive 🔐
++ },
},
});
```
-:::tip
-You must give each plugin instance a unique id so we can refer to it in other services and the CLI. In this case we used `1pass/prod`, as you can imagine using multiple 1password vaults and having another plugin instance of `1pass/non-prod`.
+:::tip[Plugin instance IDs]
+You must give each plugin instance a unique id so we can refer to it in other services and the [`dmno` CLI](/docs/reference/cli/plugin/).
+
+In this case we used `1pass`, but you can imagine splitting vaults and access, and having multiple plugin instances - for example `1pass/prod` for highly sensitive production secrets and `1pass/dev` for everything else.
:::
### Injecting the plugin in monorepo services
-In a monorepo, if you are managing config for multiple services in a single vault, you should initialize the plugin instance once in your root service as seen above, and then _inject_ it in the child services. Note we must use that same id we set during initialization.
+In a monorepo, you are likely managing secrets for multiple services. If you will be using the same service account(s) to access those secrets, you can initialize a plugin instance once in your root service as seen above, and then _inject_ it in the child services. Note we must use that same id we set during initialization.
```typescript title='apps/some-service/.dmno/config.mts'
import { OnePasswordDmnoPlugin } from '@dmno/1password-plugin';
-// inject the already initialized plugin instead of re-initializing it
-const OnePassBackend = OnePasswordDmnoPlugin.injectInstance('1pass/prod');
+// 💉 inject the already initialized plugin instead of re-initializing it
+const OnePassBackend = OnePasswordDmnoPlugin.injectInstance('1pass');
```
-:::tip
-For more on 1Password security, see their best practices for [CLI](https://developer.1password.com/docs/cli/best-practices/) and [business accounts](https://support.1password.com/business-security-practices/#access-management-and-the-principle-of-least-privilege).
-:::
-
-
------------
## Setup vault & service account
+If you already use 1password and your secrets live in a vault that holds other important passwords and info, you should create a new vault and move your secrets to it, because **the access system of 1password is based on vaults, not individual items**.
+
-1. **Create a vault** in your 1Password account. This is where you'll store your secrets. You can create multiple vaults for different environments or services. [link](https://support.1password.com/create-share-vaults/#create-a-vault)
+1. **Create a vault** in your 1Password account which will be used to hold your secrets. You can create multiple vaults to segment access to different environments, services, etc. You can use any 1password app, the web app, or the CLI. [link](https://support.1password.com/create-share-vaults/#create-a-vault)
+
+2. **Create a new service account** and grant access to necessary vault(s). This is a special account used for machine-to-machine communication. This is done in the 1Password web interface. Be sure to copy the new service account token or save it in another vault. [link](https://developer.1password.com/docs/service-accounts/get-started/)
+ :::note[Vault access set during creation only]
+ Vault access rules cannot be edited after creation, so if your vault setup changes, you will need to create new service account(s) and update the tokens.
+ :::
-2. **Create a service account** in your 1Password account. This is a separate account that has access to the vault(s) you created. You can create multiple service accounts for different environments or services. [link](https://developer.1password.com/docs/service-accounts/get-started/)
+3. **Grant vault access to users/teams (optional)**. Your developers may need access to at least some of your vaults, especially if using the `op` cli based auth mentioned below. [link](https://support.1password.com/create-share-vaults-teams/#share-a-vault)
-3. **Grant vault access to the service account**. This is done in the 1Password web interface. You can add multiple service accounts to a single vault. [link](https://developer.1password.com/docs/service-accounts/manage-service-accounts/#manage-access)
+4. **Ensure vault service account access is enabled (optional)**. Each vault has a toggle to disable service account access _in general_. It is on by default, so you will likely not need to do anything. [link](https://developer.1password.com/docs/service-accounts/manage-service-accounts/#manage-access)
-4. **Grant vault access to users/teams (optional)**. During local development, if you'd rather not useon using the op cli's integration with the Desktop app
-This service account token will now serve as your "secret-zero" - which grants access to the rest of your sensitive config stored in 1password. It must be set locally and in deployed environments, and as it is sensitive, we must pass in the value as an _override_ rather than storing it within the config. Locally this usually means storing it in your `.env.local` and on a deployed environment you'll usually set it within some kind of UI, wherever you would normally pass in secrets.
+This service account token will now serve as your "secret-zero" - which grants access to the rest of your sensitive config stored in 1Password. It must be set locally (unless relying on cli-based auth) and in deployed environments. It is sensitive so we must pass in the value as an _override_ rather than storing it within the config. Locally this usually means storing it in your [`.env.local` file](/docs/guides/env-files/) and on a deployed environment you'll usually set it within some kind of UI, wherever you would normally pass in environment variables.
```diff title=".dmno/.env.local"
+OP_TOKEN=ops_abc123...
```
-Note that the config path of `OP_TOKEN` is arbitrary and you can see how it was wired up from the config to the plugin input above. If you are using multiple vaults and service accounts, you may have something more like `OP_PROD_TOKEN` and `OP_NON_PROD_TOKEN`.
+Note that the config path of `OP_TOKEN` is arbitrary and you can see how it was wired up from your config schema to the plugin input in the example above. If you are using multiple vaults and service accounts, you may have something more like `OP_TOKEN_PROD` and `OP_TOKEN_DEV`.
-:::tip
-Consider how you want to organize your vaults and service accounts. You might have a single vault for all your secrets per environment, or you might have separate vaults for each service and environment. At a minimum, DMNO recommends having separate vaults for production and non-production environments.
+:::tip[Vault organization best practices]
+Consider how you want to organize your vaults and service accounts, keeping in mind [best practices](https://support.1password.com/business-security-practices/#access-management-and-the-principle-of-least-privilege). At a minimum, DMNO recommends having a vault for highly sensitive production secrets and another for everything else.
:::
-### Desktop app integration
+### Desktop app / CLI integration (optional)
+
+During local development, you may find it convenient to skip the service account tokens and instead rely on your system's `op` CLI and its [integration with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration). This means you will be connecting to 1Password as if you were using your local 1Password desktop application, including using it's biometric unlocking features.
+
+
+1. **Opt-in while initializing the plugin**
+ ```diff lang="ts" title='.dmno/config.mts'
+ const OnePassBackend = new OnePasswordDmnoPlugin('1pass/dev', {
+ token: configPath('OP_TOKEN'),
+ + fallbackToCliBasedAuth: true,
+ });
+ ```
+
+ _Of course you can also point to a `configPath` in your schema and toggle the opt-in based on some other logic if you'd like._
+
+2. **Ensure the `op` CLI is installed**. [docs](https://developer.1password.com/docs/cli/get-started/)
+
+3. **Enable the desktop app + CLI integration**. [docs](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration)
-During local development, you may find it convenient to use the onepoas
+4. **Run `op signin` to log in on the CLI**. Ensure you are logged in to the correct account. You can run `op whoami` to see which account is currently connected to the CLI.
+
+
+With this option enabled, if the resolved service account token is empty, we will call out to the `op` cli installed on your machine (it must be in your `$PATH`) and use the auth it provides. With the desktop app integration enabled, it will call out and may trigger biometric verification to unlock. It is slick and very convenient!
+:::caution[Connecting as yourself]
+Keep in mind that this method is connecting as _YOU_ who may have more access than a tightly scoped service account. Consider only enabling this method for a plugin instance that will be handling non-production secrets.
+:::
------
-## Add your items
+## Add items to your schema
-DMNO supports a few different ways to reference items in 1Password.
+With the plugin initialized and access wired up, now we must update our config schema to know which items will be coming from 1Password and where to look. DMNO supports a few different ways to reference items in 1Password:
-### Using a env blob (recommended)
+### Using a `.env` blob
-Managing lots of individual 1password items and connecting them to your config can be a bit tedious, so we recommend storing multiple items together in a `.env` style text blob. Using this method, we'll have a single 1password item that can have one text entry per service containing the `.env` blob. This would be similar to applying a `.env.local` file as overrides, except they are secured and shared via 1password. This also makes it incredibly easy to migrate from using local `.env` files.
+Managing lots of individual 1Password items and connecting them to your config can be a bit tedious, so when getting started, we recommend storing multiple items together in a `.env` style text blob. Using this method, we'll have a single 1Password item that can have one text entry per service containing the `.env` blob. This would be similar to applying a `.env.local` file as overrides, except they are secured and shared via 1Password. This also makes it incredibly easy to migrate from using local `.env` files.
-To use this method, we need to tell the plugin which 1password item will store our `.env` blob(s). As this value is static and not sensitive, we can use a static value as our plugin input.
+To use this method, we need to tell the plugin which 1Password item will store our `.env` blob(s). As this link is not sensitive, we can use a static value as our plugin input.
-```diff lang="ts" title=".dmno/config.mts"
-import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
+Then use the `.item()` value resolver for any config values that will be stored in the linked 1Password item.
+```diff lang="ts" title=".dmno/config.mts"
const OnePassBackend = new OnePasswordDmnoPlugin('1pass/prod', {
token: configPath('OP_TOKEN'),
+ envItemLink: 'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
@@ -121,11 +147,22 @@ export default defineDmnoService({
});
```
+:::tip[Where to find an item private link]
+You can find the private link by clicking the 3 dots on the item in the 1Password interface and selecting `Copy Private Link`.
+:::
+
+Values are looked up within the linked 1Password item using a simple convention. We expect to find a _text field_ within the item with a label set to the current [service name](/docs/guides/schema/#service-names). The contents of that item are parsed as a [`.env` file](https://dotenvx.com/docs/env-file), and we look up items using the config item key. If no match is found, we will also look in an additional field with the label `_default`.
+
+![1Password blob item example](../../../../assets/docs-images/plugins/1password/blob-item-example.png)
+
+For example, in the item above, an item with the key `ONE_MORE` would fallback to the value in the `_default` field in any service that wasn't named `root`.
+
+
{/* TODO: add screenshot of 1pass item showing the entry? */}
-### Using specific 1password items
+### Using specific 1Password items
-If you already have lots of indivdual items in 1password, or you just don't want to use the blob method, you can wire up invididual config items to specific 1password items. We provide several methods to do so. Note that while a 1password reference (e.g., `op://vaultname/itemname/path`) points all the way to a specific value, the other methods only get us to an item which usually contains multiple entries (account id, secret key, etc). In these cases you must also pass in an additional path to the specific entry. These paths use the entry labels.
+If you already have lots of indivdual items in 1Password, or you just don't want to use the blob method, you can wire up invididual config items to specific 1Password items. We provide several methods to do so. Note that while a 1Password reference (e.g., `op://vaultname/itemname/path`) is easier to use in some ways, they are based on field labels and are not stable, so the other methods are preferred.
```ts
export default defineDmnoService({
@@ -134,14 +171,14 @@ export default defineDmnoService({
ITEM_WITH_LINK: {
value: OnePassBackend.itemByLink(
'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
- 'somepath',
+ 'somefieldid',
),
},
- // using vault + item UUIDs
+ // using UUIDs
ITEM_WITH_IDS: {
- value: OnePassBackend.itemById('vaultUuid', 'itemUuid', 'somepath'),
+ value: OnePassBackend.itemById('vaultUuid', 'itemUuid', 'somefieldid'),
},
- // using item reference
+ // using item reference url
ITEM_WITH_REFERENCE: {
value: OnePassBackend.itemByReference('op://vaultname/itemname/path'),
},
@@ -150,11 +187,14 @@ export default defineDmnoService({
```
:::tip[Where to find an item private link]
-You can find the private link by clicking the 3 dots on the item in the 1Password interface and selecting `Copy Private Link`.
+You can find the private link by clicking the 3 dots **on the item** in the 1Password interface and selecting `Copy Private Link`.
:::
-:::tip[Where to find an item reference]
-The secret reference for invidivual items can be found by clicking on the down arrow icon on the item and selecting `Copy Secret Reference`.
+:::tip[Where to find field IDs]
+Field IDs are not easy to get from the 1Password UI. Luckily when the supplied field ID is not found, our error message includes a list of all the possible IDs in the item. Simply start with an empty string or a bogus id like `"?"` and use the DMNO error message to find the right field ID.
+:::
-If you don't see the menu option, you may need to enable it in the 1Password Developer settings and you may need to install the 1Password CLI as well.
+:::tip[Where to find an item reference]
+The secret reference for invidivual fields within an item can be found by clicking on the down arrow icon **on the field** and selecting `Copy Secret Reference`.
:::
+
diff --git a/packages/plugins/1password/src/plugin.ts b/packages/plugins/1password/src/plugin.ts
index 96c65ba8..9ebca282 100644
--- a/packages/plugins/1password/src/plugin.ts
+++ b/packages/plugins/1password/src/plugin.ts
@@ -15,6 +15,7 @@ import { Client, createClient } from '@1password/sdk';
import { name as thisPackageName, version as thisPackageVersion } from '../package.json';
import { OnePasswordTypes } from './data-types';
+type FieldId = string;
type ItemId = string;
type VaultId = string;
type VaultName = string;
@@ -100,13 +101,16 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
token: {
description: 'this service account token will be used via the CLI to communicate with 1password',
extends: OnePasswordTypes.serviceAccountToken,
+ // TODO: add validation, token must be set unless `fallbackToCliBasedAuth` is true
// required: true,
},
envItemLink: {
description: 'link to secure note item containing dotenv style values',
extends: OnePasswordTypes.itemLink,
},
-
+ fallbackToCliBasedAuth: {
+ description: "if token is empty, use system's `op` CLI to communicate with 1password",
+ },
} satisfies DmnoPluginInputSchema;
// ^^ note this explicit `satisfies` is needed to give us better typing on our inputSchema
@@ -138,6 +142,7 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
// using sdk
if (this.opClient) {
return await ctx.getOrSetCacheItem(`1pass-sdk:V|${vaultId}/I|${itemId}`, async () => {
+ // TODO: better error handling to tell you what went wrong? no access, non existant, etc
const opItem = await this.opClient!.items.get(vaultId, itemId);
return JSON.parse(JSON.stringify(opItem)); // convert to plain object
});
@@ -157,6 +162,7 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
// using sdk
if (this.opClient) {
return await ctx.getOrSetCacheItem(`1pass-sdk:R|${referenceUrl}`, async () => {
+ // TODO: better error handling to tell you what went wrong? no access, non existant, etc
return await this.opClient!.secrets.resolve(referenceUrl);
});
}
@@ -243,7 +249,7 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
*
* To get an item's link, right click on the item and select "Copy Private Link" (or select the item and click the ellipses / more options menu)
* */
- itemByLink(privateLink: string, path?: string) {
+ itemByLink(privateLink: string, fieldIdOrPath?: FieldId | { path: string }) {
const linkValidationResult = OnePasswordTypes.itemLink().validate(privateLink);
if (linkValidationResult !== true) {
@@ -255,19 +261,22 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
const vaultId = url.searchParams.get('v')!;
const itemId = url.searchParams.get('i')!;
- return this.itemById(vaultId, itemId, path);
+ return this.itemById(vaultId, itemId, fieldIdOrPath);
}
// can read items by id - need a vault id, item id
// and then need to grab the specific data from a big json blob
// cli command `op item get bphvvrqjegfmd5yoz4buw2aequ --vault=ut2dftalm3ugmxc6klavms6tfq --format json`
- itemById(vaultId: VaultId, itemId: ItemId, path?: string) {
+ itemById(vaultId: VaultId, itemId: ItemId, fieldIdOrPath?: FieldId | { path: string }) {
+ const fieldId = _.isString(fieldIdOrPath) ? fieldIdOrPath : undefined;
+ const path = _.isObject(fieldIdOrPath) ? fieldIdOrPath.path : undefined;
return this.createResolver({
label: (ctx) => {
return _.compact([
`Vault: ${vaultId}`,
`Item: ${itemId}`,
+ fieldId && `Field: ${fieldId}`,
path && `Path: ${path}`,
]).join(', ');
},
@@ -277,9 +286,35 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
const itemObj = await this.getOpItemById(ctx, vaultId, itemId);
- // TODO: path is necessary... maybe we could return the first item or something if none is specified?
+ const sectionsById = _.keyBy(itemObj.sections, (s) => s.id);
+
+ // field selection by id
+ if (fieldId !== undefined) {
+ const field = _.find(itemObj.fields, (f) => f.id === fieldId);
+ if (field) {
+ // do we want to throw an error if we found the value but its empty?
+ return field.value;
+ }
+ // console.log(itemObj);
+ const possibleFieldIds = _.compact(_.map(itemObj.fields, (f) => {
+ if (f.value === undefined || f.value === '' || f.purpose === 'NOTES') return undefined;
+ const section = sectionsById[f.sectionId || f.section?.id];
+ return { id: f.id, label: f.label || f.title, sectionLabel: section?.label || section?.title };
+ }));
+ throw new ResolutionError(`Unable to find field ID "${fieldId}" in item`, {
+ tip: [
+ 'Perhaps you meant one of',
+ ...possibleFieldIds.map((f) => [
+ '- ',
+ f.sectionLabel ? `${f.sectionLabel} > ` : '',
+ f.label,
+ ` - ID = ${f.id}`
+ ].join('')),
+ ]
+ });
+ }
+ // field selection by path
if (path) {
- const sectionsById = _.keyBy(itemObj.sections, (s) => s.id);
const valueAtPath = _.find(itemObj.fields, (i) => {
// using the cli, each item has the reference included
if (i.reference) {
@@ -297,10 +332,10 @@ export class OnePasswordDmnoPlugin extends DmnoPlugin {
}
return valueAtPath.value;
}
+ // should we fallback to first item or?
- // TODO: better error handling to tell you what went wrong? no access, non existant, etc
- return itemObj;
+
},
});
}
@@ -337,7 +372,7 @@ export interface OnePasswordDmnoPlugin {
/** private link to item containing dotenv style values (optional) */
envItemLink?: string;
/** rely on auth from system installed `op` cli instead of a service account */
- useSystemCli?: boolean,
+ fallbackToCliBasedAuth?: boolean,
}
}