Skip to content

* feat: add Passbolt plugin#498

Open
PaddeK wants to merge 5 commits intodmno-dev:mainfrom
PaddeK:main
Open

* feat: add Passbolt plugin#498
PaddeK wants to merge 5 commits intodmno-dev:mainfrom
PaddeK:main

Conversation

@PaddeK
Copy link
Copy Markdown

@PaddeK PaddeK commented Mar 27, 2026

  • Adds @varlock/passbolt-plugin. Registers @initPassbolt decorator, passbolt()/passboltFolder()/passboltCustomFields() resolvers, and passboltAccountKit data type.
  • Add comprehensive plugin README with usage examples
  • Update root README plugin table

- Adds @varlock/passbolt-plugin. Registers @initPassbolt decorator, passbolt()/passboltFolder()/passboltCustomFields() resolvers, and passboltAccountKit data type.
- Add comprehensive plugin README with usage examples
- Update root README plugin table
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: de95bf3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@varlock/passbolt-plugin Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Varlock plugin package (@varlock/passbolt-plugin) to fetch secrets from Passbolt, including decorators/resolvers, auth + API client logic, and documentation updates to surface the new plugin in repo docs.

Changes:

  • Introduces Passbolt plugin implementation (decorator, resolvers, Passbolt client/API + OpenPGP helpers, generated API bindings).
  • Adds new plugin package scaffolding/config (package.json, tsconfig, tsup config, bun.lock entry).
  • Adds plugin documentation (package README) and updates root README plugin table.

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
packages/plugins/passbolt/tsup.config.ts Build config for bundling the plugin entrypoint.
packages/plugins/passbolt/tsconfig.json TypeScript config for the new plugin package.
packages/plugins/passbolt/src/types.ts Passbolt-related type definitions (UUID, API payload shapes).
packages/plugins/passbolt/src/plugin.ts Registers @initPassbolt, resolvers, and the passboltAccountKit data type.
packages/plugins/passbolt/src/passbolt.ts Passbolt client implementation and resource/folder decoding logic.
packages/plugins/passbolt/src/openpgp.ts OpenPGP encode/decode + key/message helpers.
packages/plugins/passbolt/src/generatedClientFunctions.ts Generated Passbolt API client bindings.
packages/plugins/passbolt/src/apiClient.ts API wrapper for auth, resource/folder fetch, and metadata key handling.
packages/plugins/passbolt/package.json New published package metadata + deps.
packages/plugins/passbolt/README.md Comprehensive plugin documentation and usage examples.
bun.lock Locks new plugin workspace + new dependencies.
README.md Adds Passbolt plugin entry to the root plugins table.

Comment on lines +191 to +193
public async findFolder (search: string, folders?: Folder[], parent: string | null = null): Promise<UUIDv4String | void> {
!this.isInitialized && await this.init();

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same pattern again: !this.isInitialized && await this.init(); is likely to violate no-unused-expressions and is harder to read than an explicit if statement.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +58
{
"name": "@varlock/passbolt-plugin",
"description": "Varlock plugin to load secrets from Passbolt Secrets Manager",
"version": "0.0.1",
"type": "module",
"homepage": "https://varlock.dev/plugins/passbolt/",
"bugs": "https://github.com/dmno-dev/varlock/issues",
"repository": {
"type": "git",
"url": "https://github.com/dmno-dev/varlock.git",
"directory": "packages/plugins/passbolt"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"./plugin": "./dist/plugin.cjs"
},
"files": ["dist"],
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"test": "vitest"
},
"keywords": [
"varlock",
"plugin",
"varlock-plugin",
"passbolt",
"secrets",
"secret-management",
"secrets-manager",
"env",
".env",
"dotenv",
"environment variables",
"env vars",
"config"
],
"author": "PaddeK",
"license": "MIT",
"engines": {
"node": ">=22"
},
"peerDependencies": {
"varlock": "workspace:^"
},
"dependencies": {
"@oazapfts/runtime": "1.2.0",
"openpgp": "6.3.0"
},
"devDependencies": {
"@env-spec/utils": "workspace:^",
"@types/node": "catalog:",
"tsup": "catalog:",
"varlock": "workspace:^",
"vitest": "catalog:"
}
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a new published package (@varlock/passbolt-plugin), but there is no accompanying changeset in .changeset/. Please add a changeset (likely a minor for the new plugin) so the release/versioning pipeline can pick it up.

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +171
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[8-9a-b][0-9a-f]{3}-[0-9a-f]{12}/i.test(resourceId)) {
throw new SchemaError(`Invalid resource ID format: "${ resourceId }"`, {
tip: 'Resource ID must be a valid UUID v4 (e.g., "01234567-0123-4567-890a-bcdef0123456")'
});
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UUID v4 validation regex is missing an end anchor ($), so strings with a valid UUID prefix but extra trailing characters will be accepted. Tighten the regex to require a full-string match (and consider anchoring the start too if you want to reject leading whitespace).

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +193
#### `passboltFolder()`

Fetch all secrets from a Passbolt resource with custom fields.

**Signatures:**

- `passboltCustomFields(resourceId)` - Fetch by resource UUID from default instance
- `passboltFolder(instanceId, resourceId)` - Fetch from a specific instance
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Reference section, the headings/signatures for passboltCustomFields() are inconsistent: the heading is passboltFolder() and the second signature line also references passboltFolder(...). This will confuse users trying to call passboltCustomFields(...); update the heading and signatures to match the actual function name.

Suggested change
#### `passboltFolder()`
Fetch all secrets from a Passbolt resource with custom fields.
**Signatures:**
- `passboltCustomFields(resourceId)` - Fetch by resource UUID from default instance
- `passboltFolder(instanceId, resourceId)` - Fetch from a specific instance
#### `passboltCustomFields()`
Fetch all secrets from a Passbolt resource with custom fields.
**Signatures:**
- `passboltCustomFields(resourceId)` - Fetch by resource UUID from default instance
- `passboltCustomFields(instanceId, resourceId)` - Fetch from a specific instance

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +14
export type ClientOptions = {
passphrase: string
privateKey: string
serverUrl: string
userId: string
duration?: number
};
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation in this file is 4 spaces in multiple places (e.g. the properties in ClientOptions), which conflicts with the repo’s 2-space indentation convention (.editorconfig) and will likely be flagged by ESLint. Please reformat to 2 spaces via bun run lint:fix.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +128
} finally {
keyMap.clear();
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUserMetadataKeys() always returns an empty map: it returns keyMap from the try, but then the finally block unconditionally clears it. Also, forEach(async ...) is not awaited, so even without the clear() the map would likely be returned before keys are added. Refactor to use for...of/Promise.all and remove the finally { keyMap.clear(); } so decoded keys are actually returned.

Suggested change
} finally {
keyMap.clear();

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +39
constructor (serverUrl: string) {
defaults.baseUrl = serverUrl;
defaults.headers = { 'Content-Type': 'application/json' };
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generatedClientFunctions uses a shared exported defaults object, and this client mutates defaults.baseUrl/defaults.headers in the constructor/login flow. That means multiple Passbolt plugin instances (or parallel resolutions) can overwrite each other’s baseUrl and Authorization header, breaking multi-instance support and creating cross-request token leakage risk. Consider generating/using a per-instance oazapfts runtime (not the shared defaults), or avoid mutating globals by wrapping fetch with per-request baseUrl/headers.

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +177
public async getResources (folderId: UUIDv4String): Promise<DecodedResource[]> {
!this.isInitialized && await this.init();

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same lint/clarity issue here: !this.isInitialized && await this.init(); is likely to be flagged by no-unused-expressions. Switch to an explicit if statement.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +14
import type {
ClientOptions,
CustomFieldKey,
CustomFieldValue,
DecodedResource,
Folder,
Resource,
UUIDv4String,
GpgAccountKit
} from './types';
import { getPrivateKey, PrivateKey, getMessage, getPublicKey } from './openpgp';
import { ApiClient, type SecretIndex, type FolderV4IndexAndView, type FolderV5IndexAndView } from './apiClient';

export type { ClientOptions, UUIDv4String } from './types';
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new Passbolt plugin source files use 4-space indentation and omit semicolons, which conflicts with the repo’s 2-space indentation convention (see .editorconfig) and the existing plugin code style (ESLint Airbnb + stylistic). This is likely to fail bun run lint. Please run the formatter/lint autofix and align the indentation/style with the rest of the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +163
public async getResource (resourceId: UUIDv4String): Promise<DecodedResource | void> {
!this.isInitialized && await this.init();

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement relies on a short-circuit expression (!this.isInitialized && await this.init();). With Airbnb ESLint, no-unused-expressions typically flags this pattern. Prefer an explicit if (!this.isInitialized) await this.init(); for clarity and lint compliance.

Copilot uses AI. Check for mistakes.
@theoephraim
Copy link
Copy Markdown
Member

Is using the openapi generated thing really necessary? I suspect the number of api endpoints actually needed for the integration is minimal and easy enough to do more manually. Trying to keep this plugins fairly thin and dependencies minimal is important.

Note that we have been using ky in a few plugins to help simplify fetch interactions, but its very small and very widely used.

@PaddeK
Copy link
Copy Markdown
Author

PaddeK commented Mar 27, 2026

It just was a fast way to get it working. I am working already on replacing everything not needed for the plugin. After that i will take care of the copilot findings. Thanks for having a look!

PaddeK added 2 commits March 28, 2026 18:52
- fix linting errors
- fix copilot findings
@PaddeK
Copy link
Copy Markdown
Author

PaddeK commented Mar 28, 2026

I should have fixed everything mentioned by @theoephraim and copilot.

```env-spec title=".env.schema"
# @plugin(@varlock/passbolt-plugin)
# @initPassbolt(accountKit=$PB_ACCOUNT_KIT, passphrase=$PB_PASSPHRASE)
# @setValuesBulk(passboltFolder("CI\/CD/DEV"))
Copy link
Copy Markdown
Member

@theoephraim theoephraim Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with this backslash? Is that how it appears in the passbolt UI?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a folder CI/CD in my passbolt instance. Because i wanted to enable users to define a deeper nested folder structure as a argument i decided to use filepath like declrataion. This requires to escape the slash in the CI/CD. Just wanted to use something that is common as delimiter and escape char.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh that makes sense. Although let’s use a different example then since the having a slash in the name is an unusual case.


---

#### `passboltFolder()`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with passbold, but would it make sense to combine these two into a single passboltBulk method that can take a folderPath or a resourceId or potentially other filters? Not necessarily saying it has to be this way - whatever is sort of normal/idiomatic for passbolt - but it may provide more flexibility to search for the specific fields to fetch.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good idea to combine them. I could also include a field filter. This filter would only be useful for single resources (passbolt()) or for the bulk variant with folderPath though. I am not sure to ignore the field argument in the bulk variant with resourceId then or its better to have two bulk functions in that case to not confuse users.

Copy link
Copy Markdown
Author

@PaddeK PaddeK Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To explain my line of thinking in more detail, i should briefly describe how Passbolt organizes information. There is a folder structure with references to parent folders and resources. A folder can contain multiple resources. Resources contains fields like username, url, password or custom fields. Custom fields is a way to create multiple key-value pairs inside a single resource.

@theoephraim
Copy link
Copy Markdown
Member

This is awesome. Thanks so much :) Left a couple small questions but otherwise seems ready to go!

@theoephraim
Copy link
Copy Markdown
Member

theoephraim commented Mar 30, 2026

Ok I was looking at the passbolt docs, and I'm not 100% certain of everything but I think I understand a bit more about what's going on. Please forgive me if I'm way off...

It seems like a "resource" is an item that contains multiple fields (username/password/totp/url/custom fields?) while a "secret" is an individual field (just the username/pass/etc). Is it easy within the UI to get these "secret" ids?

The current implementation assumes you are always fetching the password field. That may be correct most of the time, but I think you need to allow the user to specify. For example you may have a username and password and need to fetch both. It's unclear to me how custom fields work from the api docs, but likely you want to allow referring to custom fields this way too.

ITEM_USERNAME=passbolt(resource-uuid#username)
ITEM_PASSWORD=passbolt(resource-uuid)    # <- defaults to password, similar to what you have now
ITEM_PASSWORD_EXPLICIT=passbolt(resource-uuid#password)
ITEM_CUSTOM_FIELD=passbolt(resource-uuid#custom-field-key) # not sure about this??

For bulk fetching, I guess you could fetch a bunch of items and only fetch their password fields, but I'm not sure that's quite right.

You could instead allow fetching all custom fields on a single item, something like:

# @setValuesBulk(passbolt(resource-uuid, customFieldsObj=true))

This would be useful say if you had a single item that had all your env vars as custom fields, stored under their correct names. Whether that should be a new resolver or just an option, not 100% sure...

You could of course also store a big dotenv style blob within a single "password" field (assuming it can handle long multi-line items).

Looks like there is also functionality to fetch items using categories (seems like folders?) or tags - but the fact the items are meant to store multiple fields makes bulk fetching this way make less sense.

Note that we have a few other plugins that have similar setups and we have used id#key as a way to refer to specific child keys within a larger group of stuff, especially when the key is optional.

@PaddeK
Copy link
Copy Markdown
Author

PaddeK commented Mar 30, 2026

You are correct, the resource is a container for uri/username/password/totp/custom fields. Getting the resourceId (a UUID) is simple via the UI and described in the readme.

I had a look at the AWS Secrets Manager plugin and think the optional key handling is a good fit for the passbolt() single item resolver. But i dont see much use of a bulk resolver that is fetching all usernames or uri fields etc.

# @setValuesBulk(passbolt(resource-uuid, customFieldsObj=true))

That usecase is covered with the passboltCustomFields() resolver. Personaly i will only use the bulk resolver for folders because it has better handling in the UI.

You could of course also store a big dotenv style blob within a single "password" field (assuming it can handle long multi-line items).

Handling for that is not good in the UI and was the reason i looked into custom fields.

Looks like there is also functionality to fetch items using categories (seems like folders?) or tags - but the fact the items are meant to store multiple fields makes bulk fetching this way make less sense.

Categories and Tags are kinda newish (and only available in Pro or Cloud version) and the API does not return this information for a resource.

I think i will keep the dedicated passboltCustomFields() resolver for clarity and implement an optional field argument (like the AWS plugin) for the passbolt() and passboltFolder() resolvers with default password. For accessing custom field keys i would suggest something like this:

CUSTOM_FIELD_1=passbolt(resource-uuid#custom) # returns value of CUSTOM_FIELD_1 key
CUSTOM_FIELD_2=passbolt(resource-uuid#custom.CUSTOM_FIELD_1) # will also return value of CUSTOM_FIELD_1

I dont see much use for the second example but it might be useful to someone. The dot as delimiter is to distinguish between lets say username of the resource and possibly a custom field key username .

I also thought it could be nice to generate the TOTP code, the API only gives access to TOTP settings like digits/hash algorithm/secret/period and the code is only shown in the UI.

- generate TOTP code and make it accessable via optional field parameter
- update README
@PaddeK
Copy link
Copy Markdown
Author

PaddeK commented Mar 30, 2026

Just pushed discussed changes and updated the README accordingly. Let me know what you think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants