@opengeni/vaults 3.1.2 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Generic remote vault provider over the signed `opengeni.vault.v1` protocol.
3
+ *
4
+ * The HTTP transport is injected: the API and worker supply their DNS-pinned,
5
+ * HTTPS-only, no-redirect egress. This adapter only signs exact request
6
+ * bytes, enforces the response byte cap again, verifies the response MAC and
7
+ * strictly parses the operation result. It never retries a mutation: any
8
+ * response that cannot be verified after a write may have been applied and is
9
+ * reported as `provider_outcome_unknown`.
10
+ */
11
+ import { type VaultProviderFreshness, type VaultProviderOperations } from "@opengeni/contracts";
12
+ import { type ProviderItemMeta, type ProviderPage, type ProviderPageRequest, type ProviderVault, type VaultProvider, type VaultProviderContext } from "./provider.js";
13
+ export type RemoteVaultProviderKind = "external" | "bitwarden";
14
+ /** Case-insensitive header lookup, satisfied by `Headers`. */
15
+ export interface RemoteVaultHeaders {
16
+ get(name: string): string | null;
17
+ }
18
+ export interface RemoteVaultHttpRequest {
19
+ url: string;
20
+ method: "POST";
21
+ headers: Readonly<Record<string, string>>;
22
+ /** Send exactly these bytes; the signature covers them. */
23
+ body: string;
24
+ /** Hard deadline for connect, send and the complete response body. */
25
+ timeoutMs: number;
26
+ /** Fail instead of reading more response body bytes than this. */
27
+ maxResponseBytes: number;
28
+ signal?: AbortSignal;
29
+ }
30
+ export interface RemoteVaultHttpResponse {
31
+ status: number;
32
+ headers: RemoteVaultHeaders;
33
+ body: string;
34
+ }
35
+ /**
36
+ * Bounded HTTP egress for remote vault calls. Implementations must refuse
37
+ * redirects, enforce `timeoutMs` and `maxResponseBytes`, and apply the
38
+ * deployment's endpoint policy (HTTPS, DNS pinning, private-address refusal).
39
+ */
40
+ export interface RemoteVaultTransport {
41
+ post(request: RemoteVaultHttpRequest): Promise<RemoteVaultHttpResponse>;
42
+ }
43
+ export type RemoteVaultTransportFailure = "refused" | "network" | "timeout" | "redirect" | "too_large" | "aborted";
44
+ /**
45
+ * Transport failure. Set `requestSent: false` only when the transport knows
46
+ * no request bytes left the process (for example a refused DNS policy);
47
+ * otherwise a failed mutation is reported as an unknown outcome.
48
+ */
49
+ export declare class RemoteVaultTransportError extends Error {
50
+ readonly failure: RemoteVaultTransportFailure;
51
+ readonly requestSent: boolean;
52
+ constructor(failure: RemoteVaultTransportFailure, requestSent: boolean);
53
+ }
54
+ export type RemoteVaultFetch = (url: string, init: {
55
+ method: "POST";
56
+ headers: Record<string, string>;
57
+ body: string;
58
+ redirect: "manual";
59
+ signal: AbortSignal;
60
+ }) => Promise<{
61
+ status: number;
62
+ type?: string;
63
+ headers: RemoteVaultHeaders;
64
+ body: ReadableStream<Uint8Array> | null;
65
+ }>;
66
+ /**
67
+ * Adapts a fetch-compatible function (for example the deployment's pinned
68
+ * fetch) to the bounded transport: manual redirects are refused, the whole
69
+ * exchange shares one deadline and the body is read with a byte cap.
70
+ */
71
+ export declare function createFetchRemoteVaultTransport(fetchImpl: RemoteVaultFetch): RemoteVaultTransport;
72
+ export interface RemoteVaultProviderOptions {
73
+ provider: RemoteVaultProviderKind;
74
+ /** Registered endpoint. HTTPS only unless `allowInsecureHttp` (local/test). */
75
+ endpoint: string;
76
+ /** Per-connection secret; signs requests and derives the response key. */
77
+ signingSecret: string;
78
+ transport: RemoteVaultTransport;
79
+ /** The Opengeni connection id; must match `ctx.connectionId` when both exist. */
80
+ connectionId?: string;
81
+ /**
82
+ * Operations verified by the last describe (stored with the connection), so
83
+ * a fresh instance can answer `capabilities()` without a call. Without it,
84
+ * and before `validate`, the provider is read-only.
85
+ */
86
+ operations?: VaultProviderOperations;
87
+ timeoutMs?: number;
88
+ allowInsecureHttp?: boolean;
89
+ now?: () => number;
90
+ newRequestId?: () => string;
91
+ }
92
+ export interface RemoteVaultValidation {
93
+ accountName: string | null;
94
+ accountId: string;
95
+ operations: VaultProviderOperations;
96
+ capabilities: {
97
+ totp: boolean;
98
+ files: boolean;
99
+ };
100
+ }
101
+ export interface RemoteVaultProvider extends VaultProvider {
102
+ readonly id: RemoteVaultProviderKind;
103
+ validate(ctx: VaultProviderContext): Promise<RemoteVaultValidation>;
104
+ listVaultPage(ctx: VaultProviderContext, page: ProviderPageRequest): Promise<ProviderPage<ProviderVault>>;
105
+ listItemPage(ctx: VaultProviderContext, externalVaultId: string, page: ProviderPageRequest): Promise<ProviderPage<ProviderItemMeta>>;
106
+ /** Deliberate upstream refresh. Returns the receiver's freshness. */
107
+ refresh(ctx: VaultProviderContext): Promise<VaultProviderFreshness | null>;
108
+ }
109
+ /** Operations a remote connection can never perform through this protocol. */
110
+ export declare const REMOTE_VAULT_READ_ONLY_OPERATIONS: VaultProviderOperations;
111
+ /** Clamp a described operation set to what the v1 wire can express. */
112
+ export declare function remoteVaultOperations(operations: VaultProviderOperations): VaultProviderOperations;
113
+ /**
114
+ * Create a provider for one registered remote connection. Construction
115
+ * validates the endpoint and secret; no request is sent until a method runs.
116
+ */
117
+ export declare function createRemoteVaultProvider(options: RemoteVaultProviderOptions): RemoteVaultProvider;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createFakeVaultProvider
3
- } from "../chunk-VKPGG2FW.js";
4
- import "../chunk-7S23LHTK.js";
3
+ } from "../chunk-BEKN77JO.js";
4
+ import "../chunk-BD43JBPC.js";
5
5
  export {
6
6
  createFakeVaultProvider
7
7
  };
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createFakeVaultProvider
3
- } from "../chunk-VKPGG2FW.js";
3
+ } from "../chunk-BEKN77JO.js";
4
4
  import {
5
5
  VaultProviderError,
6
6
  createProviderCallBudget
7
- } from "../chunk-7S23LHTK.js";
7
+ } from "../chunk-BD43JBPC.js";
8
8
 
9
9
  // src/testing/static-resolver.ts
10
10
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/vaults",
3
- "version": "3.1.2",
3
+ "version": "3.2.2",
4
4
  "description": "Vault provider interface, registry, shell splicers and test doubles for Opengeni secrets.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
43
43
  },
44
44
  "dependencies": {
45
- "@opengeni/contracts": "3.1.2"
45
+ "@opengeni/contracts": "3.2.2"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=18"
package/src/index.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export * from "./provider";
2
+ export * from "./provider-pages";
3
+ export * from "./provider-capabilities";
4
+ export * from "./remote-provider";
2
5
  export * from "./splice/index";
@@ -0,0 +1,64 @@
1
+ import type { VaultProviderOperations } from "@opengeni/contracts";
2
+ import type { ProviderVault, VaultProviderCapabilities } from "./provider";
3
+
4
+ /** Preserve the published legacy port, while new adapters declare exact operations. */
5
+ export function providerOperations(
6
+ capabilities: VaultProviderCapabilities,
7
+ vault?: Pick<ProviderVault, "operations" | "writeAccess">,
8
+ ): VaultProviderOperations {
9
+ const base: VaultProviderOperations = capabilities.operations ?? {
10
+ createItemTypes: capabilities.write ? ["login", "api_key", "note"] : [],
11
+ updateItem: capabilities.write,
12
+ removeField: capabilities.write,
13
+ archiveItem: capabilities.write,
14
+ archiveKind: capabilities.write ? "archive" : "none",
15
+ deleteItem: false,
16
+ restoreItem: false,
17
+ readFiles: capabilities.files,
18
+ writeFiles: false,
19
+ createTotp: capabilities.write && capabilities.totp,
20
+ updateTotp: capabilities.write && capabilities.totp,
21
+ createVault: capabilities.createVault,
22
+ restoreFieldHistory: false,
23
+ };
24
+ const override = vault?.operations;
25
+ // An upstream vault may narrow its adapter's abilities, never widen them.
26
+ const operations: VaultProviderOperations = {
27
+ createItemTypes: base.createItemTypes.filter(
28
+ (type) => type !== "passkey" && (!override || override.createItemTypes.includes(type)),
29
+ ),
30
+ updateItem: base.updateItem && (override?.updateItem ?? true),
31
+ removeField: base.removeField && (override?.removeField ?? true),
32
+ archiveItem: base.archiveItem && (override?.archiveItem ?? true),
33
+ archiveKind: base.archiveKind,
34
+ deleteItem: false,
35
+ restoreItem: base.restoreItem && (override?.restoreItem ?? true),
36
+ readFiles: base.readFiles && (override?.readFiles ?? true),
37
+ writeFiles: base.writeFiles && (override?.writeFiles ?? true),
38
+ createTotp: base.createTotp && (override?.createTotp ?? true),
39
+ updateTotp: base.updateTotp && (override?.updateTotp ?? true),
40
+ createVault: base.createVault && (override?.createVault ?? true),
41
+ restoreFieldHistory: base.restoreFieldHistory && (override?.restoreFieldHistory ?? true),
42
+ };
43
+ if (override && override.archiveKind !== base.archiveKind) operations.archiveItem = false;
44
+ if (operations.archiveKind === "none") operations.archiveItem = false;
45
+ if (!operations.archiveItem) operations.archiveKind = "none";
46
+ operations.removeField &&= operations.updateItem;
47
+ operations.updateTotp &&= operations.updateItem;
48
+ operations.createTotp &&= operations.createItemTypes.length > 0;
49
+ if (vault?.writeAccess !== "read_only" && capabilities.write) return operations;
50
+ return {
51
+ ...operations,
52
+ createItemTypes: [],
53
+ updateItem: false,
54
+ removeField: false,
55
+ archiveItem: false,
56
+ archiveKind: "none",
57
+ restoreItem: false,
58
+ writeFiles: false,
59
+ createTotp: false,
60
+ updateTotp: false,
61
+ createVault: false,
62
+ restoreFieldHistory: false,
63
+ };
64
+ }
@@ -0,0 +1,206 @@
1
+ import { VAULT_LIMITS } from "@opengeni/contracts";
2
+ import {
3
+ VaultProviderError,
4
+ type ProviderItemMeta,
5
+ type ProviderPage,
6
+ type ProviderPageRequest,
7
+ type ProviderVault,
8
+ type VaultProvider,
9
+ type VaultProviderContext,
10
+ } from "./provider";
11
+
12
+ export const PROVIDER_PAGE_SIZE = 100;
13
+ export const PROVIDER_MAX_PAGES = 100;
14
+
15
+ function incomplete(): never {
16
+ // Do not echo untrusted cursors, item names or response bodies in errors.
17
+ throw new VaultProviderError(
18
+ "provider_inventory_incomplete",
19
+ "The provider inventory could not be read completely. The previous inventory was kept.",
20
+ );
21
+ }
22
+
23
+ /**
24
+ * A bounded provider inventory and what it proves (external vaults §8).
25
+ */
26
+ export interface ProviderInventory<T> {
27
+ items: T[];
28
+ /**
29
+ * Whether the listing also proves which items are absent. It does when it
30
+ * was one response (a single page or a legacy array listing), or when
31
+ * every page carried the same provider snapshot token. Offset or position
32
+ * cursors without a snapshot can skip an existing item that moved across
33
+ * a page boundary while the pages were read (an earlier item was deleted
34
+ * meanwhile), so such a multi-page listing proves only what it contains.
35
+ * Never remove, or report as missing, anything absent from an inventory
36
+ * that is not consistent.
37
+ */
38
+ consistent: boolean;
39
+ }
40
+
41
+ export interface ProviderPageTraversal<T> {
42
+ page: (request: ProviderPageRequest) => Promise<ProviderPage<T>>;
43
+ identity: (item: T) => string;
44
+ maxItems: number;
45
+ signal?: AbortSignal;
46
+ }
47
+
48
+ /**
49
+ * A full traversal or a refusal (`provider_inventory_incomplete`), never a
50
+ * prefix presented as the whole. A caller may remove items absent from it
51
+ * only when it is `consistent`.
52
+ */
53
+ export async function traverseProviderPages<T>(
54
+ input: ProviderPageTraversal<T>,
55
+ ): Promise<ProviderInventory<T>> {
56
+ if (!Number.isSafeInteger(input.maxItems) || input.maxItems < 1 || input.maxItems > 10_000)
57
+ incomplete();
58
+ const items: T[] = [];
59
+ const ids = new Set<string>();
60
+ const cursors = new Set<string>();
61
+ let cursor: string | undefined;
62
+ let snapshot: string | null | undefined;
63
+ for (let count = 0; count < PROVIDER_MAX_PAGES; count++) {
64
+ input.signal?.throwIfAborted();
65
+ const limit = Math.min(PROVIDER_PAGE_SIZE, input.maxItems - items.length);
66
+ if (limit < 1) incomplete();
67
+ const page = await input.page({
68
+ limit,
69
+ ...(cursor !== undefined ? { cursor } : {}),
70
+ ...(snapshot != null ? { snapshot } : {}),
71
+ });
72
+ input.signal?.throwIfAborted();
73
+ if (
74
+ !page ||
75
+ !Array.isArray(page.items) ||
76
+ page.items.length > limit ||
77
+ (page.snapshot !== null &&
78
+ (typeof page.snapshot !== "string" ||
79
+ page.snapshot.length === 0 ||
80
+ page.snapshot.length > 1024)) ||
81
+ (page.nextCursor !== null &&
82
+ (typeof page.nextCursor !== "string" ||
83
+ page.nextCursor.length === 0 ||
84
+ page.nextCursor.length > 1024))
85
+ )
86
+ incomplete();
87
+ if (snapshot !== undefined && page.snapshot !== snapshot) incomplete();
88
+ snapshot = page.snapshot;
89
+ if (page.freshness && page.freshness.state !== "current") incomplete();
90
+ for (const item of page.items) {
91
+ const id = input.identity(item);
92
+ if (typeof id !== "string" || id.length === 0 || id.length > 300 || ids.has(id)) incomplete();
93
+ ids.add(id);
94
+ items.push(item);
95
+ }
96
+ // One page is one response; later pages are consistent only under the
97
+ // same snapshot (a changed snapshot was refused above).
98
+ if (page.nextCursor === null) return { items, consistent: count === 0 || snapshot !== null };
99
+ if (items.length >= input.maxItems || cursors.has(page.nextCursor)) incomplete();
100
+ cursors.add(page.nextCursor);
101
+ cursor = page.nextCursor;
102
+ }
103
+ return incomplete();
104
+ }
105
+
106
+ /**
107
+ * Enumeration: everything a complete bounded traversal listed, or a refusal.
108
+ * It does not say whether absence is proven; use `traverseProviderPages`
109
+ * before removing anything that is not listed.
110
+ */
111
+ export async function collectProviderPages<T>(input: ProviderPageTraversal<T>): Promise<T[]> {
112
+ return (await traverseProviderPages(input)).items;
113
+ }
114
+
115
+ function validateLegacyInventory<T>(items: T[], identity: (item: T) => string, max: number): T[] {
116
+ if (!Array.isArray(items) || items.length > max) incomplete();
117
+ const ids = new Set<string>();
118
+ for (const item of items) {
119
+ const id = identity(item);
120
+ if (typeof id !== "string" || !id || id.length > 300 || ids.has(id)) incomplete();
121
+ ids.add(id);
122
+ }
123
+ return items;
124
+ }
125
+
126
+ /**
127
+ * The provider's vaults and whether absence is proven. Legacy array
128
+ * providers remain compatible (one response is consistent); new remote
129
+ * providers page natively.
130
+ */
131
+ export async function readProviderVaultInventory(
132
+ provider: VaultProvider,
133
+ ctx: VaultProviderContext,
134
+ ): Promise<ProviderInventory<ProviderVault>> {
135
+ const identity = (vault: ProviderVault) => vault.externalVaultId;
136
+ if (provider.listVaultPage) {
137
+ return traverseProviderPages({
138
+ page: async (page) => {
139
+ const result = await provider.listVaultPage!(ctx, page);
140
+ if (result.freshness) ctx.onFreshness?.(result.freshness);
141
+ return result;
142
+ },
143
+ identity,
144
+ maxItems: VAULT_LIMITS.vaultsPerOwnerScope,
145
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
146
+ });
147
+ }
148
+ ctx.signal?.throwIfAborted();
149
+ const items = await provider.listVaults(ctx);
150
+ ctx.signal?.throwIfAborted();
151
+ return {
152
+ items: validateLegacyInventory(items, identity, VAULT_LIMITS.vaultsPerOwnerScope),
153
+ consistent: true,
154
+ };
155
+ }
156
+
157
+ /** Enumeration of the provider's vaults (discovery and linking). */
158
+ export async function listProviderVaults(
159
+ provider: VaultProvider,
160
+ ctx: VaultProviderContext,
161
+ ): Promise<ProviderVault[]> {
162
+ return (await readProviderVaultInventory(provider, ctx)).items;
163
+ }
164
+
165
+ /**
166
+ * A provider vault's item metadata and whether absence is proven: only a
167
+ * consistent inventory may replace a mirror.
168
+ */
169
+ export async function readProviderItemInventory(
170
+ provider: VaultProvider,
171
+ ctx: VaultProviderContext,
172
+ externalVaultId: string,
173
+ ): Promise<ProviderInventory<ProviderItemMeta>> {
174
+ const identity = (item: ProviderItemMeta) => item.externalItemId;
175
+ if (provider.listItemPage) {
176
+ return traverseProviderPages({
177
+ page: async (page) => {
178
+ const result = await provider.listItemPage!(ctx, externalVaultId, page);
179
+ if (result.freshness) ctx.onFreshness?.(result.freshness);
180
+ return result;
181
+ },
182
+ identity,
183
+ maxItems: VAULT_LIMITS.itemsPerVault,
184
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
185
+ });
186
+ }
187
+ ctx.signal?.throwIfAborted();
188
+ const items = await provider.listItems(ctx, externalVaultId);
189
+ ctx.signal?.throwIfAborted();
190
+ return {
191
+ items: validateLegacyInventory(items, identity, VAULT_LIMITS.itemsPerVault),
192
+ consistent: true,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Enumeration of a provider vault's item metadata. It does not prove which
198
+ * items are absent; mirror replacement uses `readProviderItemInventory`.
199
+ */
200
+ export async function listProviderItems(
201
+ provider: VaultProvider,
202
+ ctx: VaultProviderContext,
203
+ externalVaultId: string,
204
+ ): Promise<ProviderItemMeta[]> {
205
+ return (await readProviderItemInventory(provider, ctx, externalVaultId)).items;
206
+ }
package/src/provider.ts CHANGED
@@ -4,6 +4,8 @@ import type {
4
4
  VaultFieldKind,
5
5
  VaultItemType,
6
6
  VaultSource,
7
+ VaultProviderOperations,
8
+ VaultProviderFreshness,
7
9
  } from "@opengeni/contracts";
8
10
 
9
11
  export interface VaultProviderCapabilities {
@@ -14,12 +16,16 @@ export interface VaultProviderCapabilities {
14
16
  groupGrants: boolean;
15
17
  /** Passkeys live only in Opengeni-hosted vaults (D20). */
16
18
  passkeys: false;
19
+ /** Additive: older in-process providers retain the legacy capability flags. */
20
+ operations?: VaultProviderOperations;
17
21
  }
18
22
  export interface ProviderVault {
19
23
  externalVaultId: string;
20
24
  name: string;
21
25
  contentVersion: string | null;
22
26
  itemCount: number | null;
27
+ writeAccess?: "read_only" | "read_write" | "unknown";
28
+ operations?: VaultProviderOperations;
23
29
  }
24
30
  export interface ProviderFieldMeta {
25
31
  externalFieldId: string;
@@ -63,13 +69,74 @@ export interface VaultProviderContext {
63
69
  connectionId: string | null;
64
70
  budget: ProviderCallBudget;
65
71
  signal?: AbortSignal;
72
+ /** Trusted native scope, supplied by the authorized registry, never model arguments. */
73
+ authority?: VaultProviderAuthority;
74
+ /** Last successful mirror-check identity; changes invalidate attempt value caches. */
75
+ cacheEpoch?: string | null;
76
+ /** Recheck upstream, without retrying a mutation or bypassing provider throttling. */
77
+ forceRefresh?: boolean;
78
+ /** Stable caller operation identity when available. No automatic replay is implied. */
79
+ idempotencyKey?: string;
80
+ /** Runtime-only observation; never supplied by an agent or persisted with values. */
81
+ onFreshness?: (freshness: VaultProviderFreshness) => void;
66
82
  }
83
+
84
+ export interface VaultProviderAuthority {
85
+ accountId: string;
86
+ workspaceId: string;
87
+ subjectId: string | null;
88
+ /** Where the encrypted connection is owned, not a replacement for the calling workspace. */
89
+ connectionWorkspaceId: string;
90
+ sessionId?: string;
91
+ turnId?: string;
92
+ attemptId?: string;
93
+ siteId?: string;
94
+ }
95
+
96
+ export interface ProviderPageRequest {
97
+ cursor?: string;
98
+ limit: number;
99
+ snapshot?: string;
100
+ }
101
+
102
+ export interface ProviderPage<T> {
103
+ items: T[];
104
+ nextCursor: string | null;
105
+ /**
106
+ * Null only when upstream cannot supply snapshot consistency. A single page
107
+ * needs none, but a multi-page listing without a snapshot proves only what
108
+ * it contains: Opengeni lists it for discovery, yet never replaces a mirror
109
+ * from it (that would drop items skipped while the pages were read). Every
110
+ * page of one listing carries the same snapshot.
111
+ */
112
+ snapshot: string | null;
113
+ freshness?: VaultProviderFreshness;
114
+ }
115
+
67
116
  export interface VaultProvider {
68
- readonly id: "onepassword" | "host" | "fake";
117
+ readonly id: Exclude<VaultSource, "opengeni"> | "fake";
69
118
  capabilities(): VaultProviderCapabilities;
70
- validate(ctx: VaultProviderContext): Promise<{ accountName: string | null }>;
119
+ validate(ctx: VaultProviderContext): Promise<{
120
+ accountName: string | null;
121
+ /** Stable identity, when supplied by the upstream account or bridge. */
122
+ accountId?: string;
123
+ }>;
124
+ /**
125
+ * One complete response, trusted as a consistent inventory. An adapter
126
+ * that pages upstream should implement `listVaultPage`/`listItemPage`, so
127
+ * page consistency is checked, rather than join pages here.
128
+ */
71
129
  listVaults(ctx: VaultProviderContext): Promise<ProviderVault[]>;
72
130
  listItems(ctx: VaultProviderContext, externalVaultId: string): Promise<ProviderItemMeta[]>;
131
+ listVaultPage?(
132
+ ctx: VaultProviderContext,
133
+ page: ProviderPageRequest,
134
+ ): Promise<ProviderPage<ProviderVault>>;
135
+ listItemPage?(
136
+ ctx: VaultProviderContext,
137
+ externalVaultId: string,
138
+ page: ProviderPageRequest,
139
+ ): Promise<ProviderPage<ProviderItemMeta>>;
73
140
  readFields(
74
141
  ctx: VaultProviderContext,
75
142
  externalVaultId: string,
@@ -105,6 +172,9 @@ export const VAULT_PROVIDER_ERROR_CODES = [
105
172
  * service reports `vault_read_only` for writes, `vault_permission_denied` otherwise.
106
173
  */
107
174
  "provider_permission_denied",
175
+ "provider_locked",
176
+ "provider_inventory_incomplete",
177
+ "provider_outcome_unknown",
108
178
  ] as const;
109
179
  export type VaultProviderErrorCode = (typeof VAULT_PROVIDER_ERROR_CODES)[number];
110
180
 
@@ -149,6 +219,10 @@ export interface VaultProviderRegistry {
149
219
  export function createVaultProviderRegistry(factories: {
150
220
  onepassword?: (connectionId: string | null) => Promise<VaultProvider>;
151
221
  host?: () => Promise<VaultProvider>;
222
+ external?: (connectionId: string | null) => Promise<VaultProvider>;
223
+ protonpass?: (connectionId: string | null) => Promise<VaultProvider>;
224
+ keeper?: (connectionId: string | null) => Promise<VaultProvider>;
225
+ bitwarden?: (connectionId: string | null) => Promise<VaultProvider>;
152
226
  }): VaultProviderRegistry {
153
227
  return {
154
228
  async forVault(vault) {
@@ -156,6 +230,14 @@ export function createVaultProviderRegistry(factories: {
156
230
  return factories.onepassword(vault.providerConnectionId);
157
231
  }
158
232
  if (vault.source === "host" && factories.host) return factories.host();
233
+ if (
234
+ vault.source !== "opengeni" &&
235
+ vault.source !== "host" &&
236
+ vault.source !== "onepassword"
237
+ ) {
238
+ const factory = factories[vault.source];
239
+ if (factory) return factory(vault.providerConnectionId);
240
+ }
159
241
  throw new VaultProviderError(
160
242
  "provider_unavailable",
161
243
  vault.source === "opengeni"