Conversation
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 detectedLatest commit: de95bf3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
There was a problem hiding this comment.
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. |
| public async findFolder (search: string, folders?: Folder[], parent: string | null = null): Promise<UUIDv4String | void> { | ||
| !this.isInitialized && await this.init(); | ||
|
|
There was a problem hiding this comment.
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.
| { | ||
| "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:" | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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")' | ||
| }); | ||
| } |
There was a problem hiding this comment.
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).
packages/plugins/passbolt/README.md
Outdated
| #### `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 |
There was a problem hiding this comment.
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.
| #### `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 |
| export type ClientOptions = { | ||
| passphrase: string | ||
| privateKey: string | ||
| serverUrl: string | ||
| userId: string | ||
| duration?: number | ||
| }; |
There was a problem hiding this comment.
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.
| } finally { | ||
| keyMap.clear(); |
There was a problem hiding this comment.
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.
| } finally { | |
| keyMap.clear(); |
| constructor (serverUrl: string) { | ||
| defaults.baseUrl = serverUrl; | ||
| defaults.headers = { 'Content-Type': 'application/json' }; | ||
| } |
There was a problem hiding this comment.
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.
| public async getResources (folderId: UUIDv4String): Promise<DecodedResource[]> { | ||
| !this.isInitialized && await this.init(); | ||
|
|
There was a problem hiding this comment.
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.
| 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'; |
There was a problem hiding this comment.
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.
| public async getResource (resourceId: UUIDv4String): Promise<DecodedResource | void> { | ||
| !this.isInitialized && await this.init(); | ||
|
|
There was a problem hiding this comment.
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.
|
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. |
|
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! |
- fix linting errors - fix copilot findings
|
I should have fixed everything mentioned by @theoephraim and copilot. |
packages/plugins/passbolt/README.md
Outdated
| ```env-spec title=".env.schema" | ||
| # @plugin(@varlock/passbolt-plugin) | ||
| # @initPassbolt(accountKit=$PB_ACCOUNT_KIT, passphrase=$PB_PASSPHRASE) | ||
| # @setValuesBulk(passboltFolder("CI\/CD/DEV")) |
There was a problem hiding this comment.
What's up with this backslash? Is that how it appears in the passbolt UI?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
This is awesome. Thanks so much :) Left a couple small questions but otherwise seems ready to go! |
|
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: 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 |
|
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
That usecase is covered with the
Handling for that is not good in the UI and was the reason i looked into custom fields.
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 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 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
|
Just pushed discussed changes and updated the README accordingly. Let me know what you think. |