Skip to content
DMNO
🚧 DMNO is still in beta! Use with caution!
✨ If you've tried DMNO or looked through the docs, let us know what you think!

1Password plugin

DMNO’s 1Password plugin allows you to securely access your secrets stored in 1Password. This plugin uses their JavaScript SDK to authenticate using a service account. Additionally, for local development, you can opt-in to use your system-installed 1Password CLI and its integration with the 1Password desktop app. This plugin is compatible with any 1Password account type (personal, family, teams, business), but note that rate limits vary by account type.

Installation & setup

Install the package in the service(s) that will use config from 1Password.

Terminal window
npm add @dmno/1password-plugin

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.

.dmno/config.mts
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
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 🔐
},
},
});

Injecting the plugin in monorepo services

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 child services. Note we must use that same id we set during initialization.

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');

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 which will be used to hold your secrets. You can create multiple vaults to segment access to different environments, services, etc. This can be done using any 1password app, the web app, or the CLI. link

  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 can only be done in the 1Password web interface. Be sure to copy the new service account token or save it in another vault. link

  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

  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

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 and on a deployed environment you’ll usually set it within some kind of UI, wherever you would normally pass in environment variables.

.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 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.

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. This means you will be connecting to 1Password as if you were using your local 1Password desktop application, including using its biometric unlocking features.

  1. Opt-in while initializing the plugin

    .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

  3. Enable the desktop app + CLI integration. docs

  4. Run op signin to sign 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 secure and very convenient!


Add items to your schema

With the plugin initialized and access wired up, now we must update our config schema to connect specific config values to data stored in 1Password. DMNO supports a few different ways to reference items in 1Password:

Using a .env blob

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 and look up items by their key - similar to applying a .env.local file as overrides, except they are secured and shared via 1Password. This also makes it easier to migrate from passing around .env files.

  1. Create a new item within your vault. Select Secure Note as the item type and be sure to give it a descriptive name (e.g., Prod secrets).

  2. Create a new field in the item. Click + add more and select Text to add a new multi-line text field. Change the default label of text to the service name you want to store secrets for (e.g., root). You can also use the special name _default if you are only dealing with a single service.

  3. Add your secrets to the text field as if it was another .env file that would be loaded as overrides. For example:

    SOME_API_KEY=super-secret-key
    ANOTHER_ITEM="quotes work too"

    You can also come back and do this later.

  4. Wire up plugin instance to the new item using its private link. While viewing the item in the 1Password app, click the 3 dots in the top right and click Copy Private Link. As this link does not contain anything sensitive, we can use a static value as our plugin input.

  5. Update items in your config schema to use the .item() value resolver for anything that will be stored in the linked 1Password item. When we resolve your config values, if a match is not found, it will result in a ResolutionError with helpful info about how to fix it.

Your dmno config should end up looking like this:

.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',
});
export default defineDmnoService({
schema: {
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
},
SOME_API_KEY: {
sensitive: true,
value: OnePassBackend.item(),
}
},
});

And your 1password item may look this:

1Password blob item example

Key lookup details and example

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. The contents of that item are parsed as a .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.

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.

You can also override the key used to lookup the value in the .env blob. This can be useful if you need to save multiple values toggled by some other logic.

.dmno/config.mts
export default defineDmnoService({
schema: {
SOME_API_SECRET: {
sensitive: true,
value: switchBy('APP_ENV', {
// uses the default key of "SOME_API_SECRET"
_default: OnePassBackendDev.item(),
// uses overridden key
staging: OnePassBackendDev.item('SOME_API_SECRET_STAGING'),
// uses the default key but looking in a different 1pass item
staging: OnePassBackendProduction.item(),
}),
},
},
});

Using specific 1Password items

If you already have lots of individual items in 1Password, or you just don’t want to use the blob method, we provide several methods to wire up individual config items to specific values in 1Password. Note that while 1Password reference URIs (e.g., op://vaultname/itemname/path) are easier to use in some ways, they are based on field labels and are not stable, so the other methods are preferred.

export default defineDmnoService({
schema: {
// using item private link
ITEM_WITH_LINK: {
value: OnePassBackend.itemByLink(
'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
'somefieldid',
),
},
// using UUIDs
ITEM_WITH_IDS: {
value: OnePassBackend.itemById('vaultUuid', 'itemUuid', 'somefieldid'),
},
// using item reference url
ITEM_WITH_REFERENCE: {
value: OnePassBackend.itemByReference('op://vaultname/itemname/path'),
},
},
});

Caching

In order to avoid rate limits and keep dev server restarts extremely fast, we heavily cache data fetched from external sources. After updating secrets in 1password, if the item has been cached, you’ll need to clear the cache to see it take effect.

  • Use the dmno clear-cache command to clear the cache once
  • The dmno resolve and dmno run commands have cache related flags:
    • --skip-cache - skips caching logic altogether
    • --clear-cache - clears the cache once before continuing as normal