@opengeni/core 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.
@@ -3,6 +3,7 @@ import type { Document, DocumentAuthorityKind, EntitlementsPort, GitHubAppApiPor
3
3
  import type { Database, SessionWorkflowWakeDeliveryResult } from "@opengeni/db";
4
4
  import type { VaultConnectionCredentialsPort } from "@opengeni/vaults";
5
5
  import type { OnePasswordVaultProviderFactory } from "./domain/vaults/provider-registry.js";
6
+ import type { ExternalVaultProviderFactories } from "./domain/vaults/external-provider-connections.js";
6
7
  import type { DocumentServices } from "@opengeni/documents";
7
8
  import type { EventBus } from "@opengeni/events";
8
9
  import type { Observability } from "@opengeni/observability";
@@ -192,6 +193,8 @@ export type AppDependencies = {
192
193
  * 1Password vaults report `provider_unavailable`.
193
194
  */
194
195
  onePasswordVaultProvider?: OnePasswordVaultProviderFactory | null;
196
+ /** Lazy native/remote vault adapters; null disables the first-wave integrations. */
197
+ externalVaultProviders?: ExternalVaultProviderFactories | null;
195
198
  /**
196
199
  * Optional embedding-host session ACL. Unset preserves standalone workspace
197
200
  * authorization; once bound, every session-addressed surface fails closed on
@@ -0,0 +1,126 @@
1
+ import { type Settings } from "@opengeni/config";
2
+ import { ExternalVaultProviderKind, VaultProviderCredentials, VaultProviderOperations, type ConnectionMetadata } from "@opengeni/contracts";
3
+ import { type Database } from "@opengeni/db";
4
+ import { type VaultProvider, type VaultProviderAuthority } from "@opengeni/vaults";
5
+ import { z } from "zod";
6
+ export declare const EXTERNAL_VAULT_CREDENTIAL_ROLE = "external_vault_provider";
7
+ declare const ExternalVaultConnectionDetails: z.ZodObject<{
8
+ version: z.ZodLiteral<1>;
9
+ provider: z.ZodEnum<{
10
+ bitwarden: "bitwarden";
11
+ external: "external";
12
+ keeper: "keeper";
13
+ protonpass: "protonpass";
14
+ }>;
15
+ ownership: z.ZodEnum<{
16
+ organization: "organization";
17
+ user: "user";
18
+ workspace: "workspace";
19
+ }>;
20
+ accountId: z.ZodNullable<z.ZodString>;
21
+ accountName: z.ZodNullable<z.ZodString>;
22
+ label: z.ZodNullable<z.ZodString>;
23
+ verifiedAt: z.ZodString;
24
+ operations: z.ZodObject<{
25
+ createItemTypes: z.ZodArray<z.ZodEnum<{
26
+ api_key: "api_key";
27
+ file: "file";
28
+ login: "login";
29
+ note: "note";
30
+ passkey: "passkey";
31
+ }>>;
32
+ updateItem: z.ZodBoolean;
33
+ removeField: z.ZodBoolean;
34
+ archiveItem: z.ZodBoolean;
35
+ archiveKind: z.ZodEnum<{
36
+ archive: "archive";
37
+ none: "none";
38
+ trash: "trash";
39
+ }>;
40
+ deleteItem: z.ZodLiteral<false>;
41
+ restoreItem: z.ZodBoolean;
42
+ readFiles: z.ZodBoolean;
43
+ writeFiles: z.ZodBoolean;
44
+ createTotp: z.ZodBoolean;
45
+ updateTotp: z.ZodBoolean;
46
+ createVault: z.ZodBoolean;
47
+ restoreFieldHistory: z.ZodBoolean;
48
+ }, z.core.$strict>;
49
+ verificationStatus: z.ZodOptional<z.ZodEnum<{
50
+ pending: "pending";
51
+ ready: "ready";
52
+ }>>;
53
+ }, z.core.$strict>;
54
+ export type ExternalVaultConnectionDetails = z.infer<typeof ExternalVaultConnectionDetails>;
55
+ export declare function externalVaultProviderDomain(provider: ExternalVaultProviderKind): string;
56
+ /** Metadata is a projection, never a credential/configuration passthrough. */
57
+ export declare function externalVaultConnectionDetails(metadata: Record<string, unknown> | null | undefined): ExternalVaultConnectionDetails | null;
58
+ export declare function isExternalVaultConnection(connection: ConnectionMetadata): boolean;
59
+ /** Keeper bootstrap tokens are never a stored runtime credential. */
60
+ export declare const StoredVaultProviderCredentials: z.ZodUnion<readonly [z.ZodObject<{
61
+ provider: z.ZodLiteral<"external">;
62
+ endpoint: z.ZodString;
63
+ signingSecret: z.ZodString;
64
+ }, z.core.$strict>, z.ZodObject<{
65
+ provider: z.ZodLiteral<"bitwarden">;
66
+ endpoint: z.ZodString;
67
+ signingSecret: z.ZodString;
68
+ }, z.core.$strict>, z.ZodObject<{
69
+ provider: z.ZodLiteral<"protonpass">;
70
+ token: z.ZodString;
71
+ }, z.core.$strict>, z.ZodObject<{
72
+ provider: z.ZodLiteral<"keeper">;
73
+ configuration: z.ZodRecord<z.ZodString, z.ZodString>;
74
+ }, z.core.$strict>]>;
75
+ export type StoredVaultProviderCredentials = z.infer<typeof StoredVaultProviderCredentials>;
76
+ /** Encrypted only: preserve the working credential while a reconnect is checked. */
77
+ export declare function pendingVaultCredentialBundle(current: StoredVaultProviderCredentials | null, pending: StoredVaultProviderCredentials): Record<string, unknown>;
78
+ export type ExternalVaultProviderFactory = (input: {
79
+ credentials: VaultProviderCredentials | StoredVaultProviderCredentials;
80
+ /** Stable native connection identity; adapter sessions must not cross it. */
81
+ connectionId?: string;
82
+ /** Includes credential binding and setup, before ordinary provider calls. */
83
+ signal?: AbortSignal;
84
+ /** Persist any later SDK key rotation before reporting successful readiness. */
85
+ persistCredentials?: (credentials: StoredVaultProviderCredentials) => Promise<void>;
86
+ /** Verified setup capabilities; the adapter may narrow, never widen them. */
87
+ operations?: VaultProviderOperations;
88
+ }) => Promise<{
89
+ provider: VaultProvider;
90
+ credentials: StoredVaultProviderCredentials;
91
+ }>;
92
+ export type ExternalVaultProviderFactories = Partial<Record<ExternalVaultProviderKind, ExternalVaultProviderFactory>>;
93
+ export declare function externalVaultConnectionMetadata(details: ExternalVaultConnectionDetails): Record<string, unknown>;
94
+ /** Credentials can only be decrypted after the native reach and connection tuple agree. */
95
+ export declare function loadExternalVaultConnection(db: Database, settings: Settings, input: {
96
+ workspaceId: string;
97
+ connectionId: string;
98
+ subjectId: string | null;
99
+ provider: ExternalVaultProviderKind;
100
+ /** Setup-only recovery, after independent connection-management authorization. */
101
+ allowPendingVerification?: boolean;
102
+ }): Promise<{
103
+ connection: ConnectionMetadata;
104
+ details: ExternalVaultConnectionDetails;
105
+ credentials: StoredVaultProviderCredentials;
106
+ currentCredentials: StoredVaultProviderCredentials | null;
107
+ pendingCredentials: StoredVaultProviderCredentials | null;
108
+ }>;
109
+ /** A bound adapter never accepts model-supplied identity, even via a copied context. */
110
+ export declare function bindVaultProviderAuthority(provider: VaultProvider, input: {
111
+ connectionId: string;
112
+ authority: VaultProviderAuthority;
113
+ operations: VaultProviderOperations;
114
+ revalidate: () => Promise<void>;
115
+ }): VaultProvider;
116
+ export declare function externalVaultProviderForConnection(input: {
117
+ db: Database;
118
+ settings: Settings;
119
+ workspaceId: string;
120
+ subjectId: string | null;
121
+ connectionId: string;
122
+ provider: ExternalVaultProviderKind;
123
+ factory: ExternalVaultProviderFactory;
124
+ executionContext?: Pick<VaultProviderAuthority, "sessionId" | "turnId" | "attemptId" | "siteId">;
125
+ }): Promise<VaultProvider>;
126
+ export {};
@@ -1,7 +1,8 @@
1
1
  /** Vaults and secret references: the only import point for the vault domain. */
2
2
  export { buildMissingSecretAskCard } from "@opengeni/contracts";
3
3
  export { isVaultError, VaultError, vaultErrorFromDatabase } from "@opengeni/db";
4
- export { toVaultItem, toVaultItemSummary, toVaultSummary, vaultCanManage, vaultCanWrite, vaultItemReferences, type VaultCallerAuthority, } from "./mapping.js";
4
+ export { intersectProviderOperations, providerOperationsForMode, storedProviderOperations, toVaultItem, toVaultItemSummary, toVaultSummary, vaultCanManage, vaultCanWrite, vaultItemReferences, type VaultCallerAuthority, } from "./mapping.js";
5
+ export { effectiveProviderOperations, type ProviderWriteOperation, type ProviderWriteOutcome, } from "./provider-writes.js";
5
6
  export { chooseSecretCandidate, createSecretResolver, type SecretResolverOptions, } from "./secret-resolver.js";
6
7
  export type { VaultRequestActor, VaultServiceDeps } from "./types.js";
7
8
  export { createVaultService, vaultActorIsApiKey, type VaultService } from "./vault-service.js";
@@ -3,7 +3,7 @@
3
3
  * contract shapes. Only text-kind field values are decrypted for display;
4
4
  * every other kind stays server-side until an audited reveal or a resolve.
5
5
  */
6
- import { type VaultItem, type VaultItemReferences, type VaultItemSummary, type VaultSummary } from "@opengeni/contracts";
6
+ import { VaultProviderOperations, type VaultItem, type VaultItemReferences, type VaultItemSummary, type VaultMode, type VaultSummary } from "@opengeni/contracts";
7
7
  import { type VaultItemRow, type VaultRow } from "@opengeni/db";
8
8
  /** What the caller may do beyond reading; decided by the route's grant. */
9
9
  export type VaultCallerAuthority = {
@@ -12,9 +12,25 @@ export type VaultCallerAuthority = {
12
12
  /** `account:admin` on the organization */
13
13
  accountAdmin: boolean;
14
14
  };
15
+ /** No writes at all: what a read-only link or a read-only provider vault allows. */
16
+ export declare function withoutProviderWrites(operations: VaultProviderOperations): VaultProviderOperations;
17
+ /** Operations both sides allow (an archive needs both to agree on its kind). */
18
+ export declare function intersectProviderOperations(left: VaultProviderOperations, right: VaultProviderOperations): VaultProviderOperations;
19
+ /**
20
+ * What a linked vault allows: the operations its provider last reported for
21
+ * it, narrowed by the link mode. Null for hosted vaults and for links not yet
22
+ * checked since operations were recorded (callers then ask the provider).
23
+ */
24
+ export declare function storedProviderOperations(row: Pick<VaultRow, "source" | "mode"> & {
25
+ providerOperations?: VaultProviderOperations | null;
26
+ }): VaultProviderOperations | null;
27
+ export declare function providerOperationsForMode(operations: VaultProviderOperations, mode: VaultMode): VaultProviderOperations;
15
28
  export declare function vaultCanWrite(row: VaultRow, authority: VaultCallerAuthority): boolean;
16
29
  export declare function vaultCanManage(row: VaultRow, authority: VaultCallerAuthority): boolean;
17
- export declare function toVaultSummary(row: VaultRow, authority: VaultCallerAuthority): VaultSummary;
30
+ export declare function toVaultSummary(row: VaultRow, authority: VaultCallerAuthority, options?: {
31
+ now?: Date;
32
+ refreshing?: boolean;
33
+ }): VaultSummary;
18
34
  /**
19
35
  * Ready references for an item. Every reference names the vault and the field
20
36
  * (`{{secret:Vault/Item/field}}`), so it cannot become ambiguous when another
@@ -1,7 +1,8 @@
1
1
  import type { Settings } from "@opengeni/config";
2
2
  import type { ConnectionMetadata, VaultErrorCode } from "@opengeni/contracts";
3
3
  import { type Database } from "@opengeni/db";
4
- import { type VaultProvider, type VaultProviderErrorCode, type VaultProviderRegistry } from "@opengeni/vaults";
4
+ import { type VaultProvider, type VaultProviderErrorCode, type VaultProviderRegistry, type VaultProviderAuthority } from "@opengeni/vaults";
5
+ import { type ExternalVaultProviderFactories } from "./external-provider-connections.js";
5
6
  /**
6
7
  * Provider error codes as vault API error codes (§10B.1.1). `provider_not_found`
7
8
  * is the vault (or, for reads, the field); `provider_permission_denied` is
@@ -69,4 +70,6 @@ export declare function createWorkspaceVaultProviderRegistry(input: {
69
70
  subjectId: string | null;
70
71
  hostVaultProvider?: VaultProvider | null;
71
72
  onePasswordVaultProvider?: OnePasswordVaultProviderFactory | null;
73
+ externalVaultProviders?: ExternalVaultProviderFactories | null;
74
+ executionContext?: Pick<VaultProviderAuthority, "sessionId" | "turnId" | "attemptId" | "siteId">;
72
75
  }): VaultProviderRegistry;
@@ -1,14 +1,21 @@
1
- import { type VaultFieldKind, type VaultItemType, type VaultSyncStatus } from "@opengeni/contracts";
2
- import { type Database, type VaultDbContext } from "@opengeni/db";
1
+ import { type VaultFieldKind, type VaultItemType, type VaultProviderFreshness, type VaultProviderOperations, type VaultSyncStatus } from "@opengeni/contracts";
2
+ import { type Database, type StoredProviderFreshness, type VaultDbContext } from "@opengeni/db";
3
3
  import { type ProviderItemMeta, type VaultProvider, type VaultProviderContext, type VaultProviderErrorCode, type VaultProviderRegistry } from "@opengeni/vaults";
4
4
  /**
5
- * Provider vault metadata mirror (design §10.9, OP-1/OP-4). A linked
6
- * 1Password or host vault keeps a metadata-only mirror (item names, types,
5
+ * Provider vault metadata mirror (design §10.9, OP-1/OP-4; external vaults
6
+ * §8–§9). A linked vault keeps a metadata-only mirror (item names, types,
7
7
  * websites, field names and kinds); values are always read live. A sync costs
8
- * one `listVaults` call and skips `listItems` entirely when the provider's
8
+ * one vault listing and skips the item listing entirely when the provider's
9
9
  * `contentVersion` is unchanged, so FAMILY-plan quotas are not burnt on
10
10
  * no-op refreshes.
11
11
  *
12
+ * Only a complete, consistent traversal replaces the mirror: interrupted,
13
+ * malformed or over-limit inventories (more than `VAULT_LIMITS.itemsPerVault`
14
+ * items, or an item beyond the per-item field and website limits) and
15
+ * multi-page listings without a snapshot keep the previous mirror and record
16
+ * `provider_inventory_incomplete`. Nothing is cut down to fit. An active
17
+ * provider rate limit is never bypassed, not even by a forced refresh.
18
+ *
12
19
  * The engine is storage-agnostic: the vault service binds `ProviderMirrorStore`
13
20
  * to its tables (`vault_items`, `vault_item_fields`, `vaults.sync_status`,
14
21
  * `vaults.rate_limited_until`).
@@ -36,7 +43,12 @@ export interface ProviderMirrorItem {
36
43
  export interface ProviderMirrorPlan {
37
44
  contentVersion: string | null;
38
45
  items: ProviderMirrorItem[];
39
- /** Items beyond `VAULT_LIMITS.itemsPerVault` that were not mirrored. */
46
+ /**
47
+ * Always 0. A provider vault with more than `VAULT_LIMITS.itemsPerVault`
48
+ * items, or an item beyond the field or website limits, is refused whole
49
+ * (`provider_inventory_incomplete`), never mirrored as its first items or
50
+ * fields. Kept for compatibility with earlier callers.
51
+ */
40
52
  truncated: number;
41
53
  }
42
54
  export interface ProviderSyncStatusUpdate {
@@ -46,17 +58,29 @@ export interface ProviderSyncStatusUpdate {
46
58
  rateLimitedUntil: Date | null;
47
59
  /** Set only when the provider was reached and the mirror is current. */
48
60
  lastSyncedAt: Date | null;
61
+ /**
62
+ * The provider's effective operations for this vault, recorded on every
63
+ * successful check (absent: keep the stored operations).
64
+ */
65
+ operations?: VaultProviderOperations | null;
66
+ /**
67
+ * The provider's own upstream freshness observation from this check; null
68
+ * when it reported none (absent: keep the stored observation). Never
69
+ * Opengeni's own check time.
70
+ */
71
+ freshness?: StoredProviderFreshness | null;
49
72
  }
50
73
  export interface ProviderMirrorStore {
51
- /** Replace the mirror for this vault in one transaction (upsert by external id, drop missing). */
52
- replaceMirror(plan: ProviderMirrorPlan): Promise<void>;
53
- setSyncStatus(update: ProviderSyncStatusUpdate): Promise<void>;
74
+ /** Atomically commit mirror and status if the pre-I/O epoch still matches. */
75
+ settleRefresh(update: ProviderSyncStatusUpdate, plan?: ProviderMirrorPlan): Promise<boolean>;
54
76
  }
55
77
  export interface ProviderVaultSyncTarget {
56
78
  externalVaultId: string;
57
79
  /** `contentVersion` recorded by the last successful mirror, if any. */
58
80
  mirroredContentVersion: string | null;
59
81
  rateLimitedUntil: Date | null;
82
+ /** The vault's durable value-cache epoch (0789), passed to the provider as `cacheEpoch`. */
83
+ cacheEpoch?: string | null;
60
84
  }
61
85
  export type ProviderVaultSyncResult = {
62
86
  status: "synced";
@@ -66,6 +90,10 @@ export type ProviderVaultSyncResult = {
66
90
  } | {
67
91
  status: "unchanged";
68
92
  contentVersion: string | null;
93
+ }
94
+ /** Another refresh settled after this one's read; no state was overwritten. */
95
+ | {
96
+ status: "superseded";
69
97
  } | {
70
98
  status: "skipped_rate_limited";
71
99
  rateLimitedUntil: Date;
@@ -91,15 +119,35 @@ export declare function providerPrimaryFieldId(item: Pick<ProviderItemMeta, "typ
91
119
  * Deterministic mirror projection. Items are ordered by provider name then
92
120
  * external id before de-duplication, so the same provider state always yields
93
121
  * the same Opengeni names; field names are de-duplicated within each item.
122
+ * More than `VAULT_LIMITS.itemsPerVault` items, or any item beyond the field
123
+ * or website limits, is refused with `provider_inventory_incomplete`: a
124
+ * partial mirror would present the first items (or fields, or websites) of a
125
+ * larger vault as its complete contents.
94
126
  */
95
127
  export declare function planProviderMirror(items: readonly ProviderItemMeta[], contentVersion: string | null): ProviderMirrorPlan;
128
+ /**
129
+ * One freshness observation for a whole check: `cache` when any page came
130
+ * from a cache, `live` only when every page was live, otherwise `unknown`.
131
+ * `sourceSyncedAt` is the oldest upstream synchronization any page reported
132
+ * (never later than the check itself), and unknown (null) when a non-live
133
+ * page did not say. Null when the provider reported nothing: Opengeni's own
134
+ * check time never stands in for the provider's.
135
+ */
136
+ export declare function aggregateProviderFreshness(observations: readonly VaultProviderFreshness[], checkedAt: Date): StoredProviderFreshness | null;
96
137
  /** Sync status for a provider failure; rate limits persist `rateLimitedUntil`. */
97
138
  export declare function providerSyncFailure(error: unknown, now: Date): ProviderSyncStatusUpdate & {
98
139
  syncErrorCode: VaultProviderErrorCode;
99
140
  };
100
141
  /**
101
142
  * Refresh one linked vault's mirror. While `rateLimitedUntil` is in the future
102
- * the provider is not called at all unless `force` is set.
143
+ * the provider is not called at all, even when `force` is set.
144
+ *
145
+ * `force` re-lists items even when `contentVersion` is unchanged and asks the
146
+ * provider to bypass its caches (`forceRefresh`). Every successful check
147
+ * records the provider's effective operations and its own freshness
148
+ * observation, whether or not the mirror changed. Mirror and status settle
149
+ * together against the pre-I/O epoch; an intervening settlement returns
150
+ * `superseded` without overwriting it or retrying provider calls.
103
151
  */
104
152
  export declare function syncProviderVault(input: {
105
153
  provider: VaultProvider;
@@ -107,14 +155,16 @@ export declare function syncProviderVault(input: {
107
155
  target: ProviderVaultSyncTarget;
108
156
  store: ProviderMirrorStore;
109
157
  now?: () => Date;
110
- /** Re-list items even when `contentVersion` is unchanged. */
158
+ /** Re-list items even when `contentVersion` is unchanged, bypassing provider caches. */
111
159
  force?: boolean;
112
160
  }): Promise<ProviderVaultSyncResult>;
113
- /** Binds the mirror store to the 0739 routines for one connected vault. */
114
- export declare function databaseProviderMirrorStore(db: Database, context: VaultDbContext, vaultId: string): ProviderMirrorStore;
161
+ /** Binds atomic settlement to the epoch read before provider I/O (0789). */
162
+ export declare function databaseProviderMirrorStore(db: Database, context: VaultDbContext, vaultId: string, expectedCacheEpoch: string): ProviderMirrorStore;
115
163
  /**
116
164
  * Sync one linked vault the caller reaches: load its provider refs and mirror
117
165
  * version, resolve its provider, refresh the mirror and record sync status.
166
+ * The provider context carries the vault's durable cache epoch; the
167
+ * registry binds the trusted authority, never this caller.
118
168
  */
119
169
  export declare function syncLinkedProviderVault(input: {
120
170
  db: Database;
@@ -122,8 +172,12 @@ export declare function syncLinkedProviderVault(input: {
122
172
  vaultId: string;
123
173
  providers: VaultProviderRegistry;
124
174
  budget?: VaultProviderContext["budget"];
175
+ /** Re-list items and bypass provider caches; an active rate limit still applies. */
125
176
  force?: boolean;
126
177
  now?: () => Date;
178
+ signal?: AbortSignal;
179
+ /** Runtime-only observation of the provider's freshness reports. */
180
+ onFreshness?: VaultProviderContext["onFreshness"];
127
181
  }): Promise<ProviderVaultSyncResult>;
128
182
  /** How stale a linked vault's mirror may be before a search refreshes it. */
129
183
  export declare const LINKED_VAULT_SEARCH_MAX_AGE_MS: number;
@@ -137,6 +191,12 @@ export declare const LINKED_VAULT_REFRESH_WAIT_MS = 8000;
137
191
  * the background and the next load shows it.
138
192
  */
139
193
  export declare const LINKED_VAULT_OPEN_WAIT_MS = 3000;
194
+ /**
195
+ * How long one background refresh may run. Afterwards it is abandoned for
196
+ * de-duplication, so one unresponsive provider cannot pin a vault as
197
+ * "refreshing" for every later caller.
198
+ */
199
+ export declare const LINKED_VAULT_REFRESH_TIMEOUT_MS = 60000;
140
200
  /** The vault fields a freshness check needs (a `VaultRow` has them all). */
141
201
  export type LinkedVaultFreshness = {
142
202
  id: string;
@@ -145,6 +205,8 @@ export type LinkedVaultFreshness = {
145
205
  lastSyncedAt: string | null;
146
206
  rateLimitedUntil: string | null;
147
207
  updatedAt: string;
208
+ syncErrorCode?: string | null;
209
+ providerFreshness?: StoredProviderFreshness | null;
148
210
  };
149
211
  /**
150
212
  * Whether a linked vault's mirror is older than `maxAgeMs`. A vault whose last
@@ -154,15 +216,67 @@ export type LinkedVaultFreshness = {
154
216
  */
155
217
  export declare function linkedVaultNeedsRefresh(vault: LinkedVaultFreshness, maxAgeMs: number, now: Date): boolean;
156
218
  /**
157
- * Bring stale linked vault mirrors up to date, so items added in the provider
158
- * (for example straight in 1Password) are found without anyone pressing Sync.
159
- *
160
- * Each refresh is the ordinary non-forced sync: one `listVaults` call, and an
161
- * item listing only when the provider says the vault changed. Refreshes of the
162
- * same vault share one call per process. The caller waits at most `waitMs`;
163
- * slower refreshes finish in the background and record their own status.
164
- * Never throws: failures are recorded as the vault's sync status.
219
+ * The freshness a person or agent sees for a linked vault (design §9);
220
+ * undefined for hosted vaults. `lastCheckedAt` is Opengeni's last successful
221
+ * check; `source` and `sourceSyncedAt` are only what the provider itself
222
+ * reported then. `current` means checked successfully within `maxAgeMs`;
223
+ * `stale` means not recently checked (or a rate limit just lifted);
224
+ * `refreshing` means a check is running; `unavailable` means the last check
225
+ * failed, with its safe error code and, under a rate limit, the retry time.
226
+ */
227
+ export declare function providerVaultFreshness(vault: LinkedVaultFreshness, now: Date, options?: {
228
+ maxAgeMs?: number;
229
+ refreshing?: boolean;
230
+ }): VaultProviderFreshness | undefined;
231
+ /**
232
+ * Whose refresh this is: concurrent refreshes share one provider call only
233
+ * within the same tenant, workspace and acting principal, because the
234
+ * provider registry (and its credential) is bound to that authority.
235
+ */
236
+ export declare function vaultRefreshScopeKey(context: VaultDbContext): string;
237
+ export type LinkedVaultRefreshOutcome = {
238
+ vaultId: string;
239
+ /**
240
+ * `not_due`: hosted, recently checked, or under an active rate limit (no
241
+ * provider call); `pending`: still running when the wait ended; `done`:
242
+ * finished with `result`; `failed`: the refresh itself failed unexpectedly.
243
+ */
244
+ state: "not_due" | "pending" | "done" | "failed";
245
+ result: ProviderVaultSyncResult | null;
246
+ };
247
+ export type LinkedVaultRefreshReport = {
248
+ outcomes: LinkedVaultRefreshOutcome[];
249
+ /** Vaults whose mirror changed before the wait ended. */
250
+ changed: string[];
251
+ /** Vaults whose refresh was still running when the wait ended. */
252
+ pending: string[];
253
+ };
254
+ /**
255
+ * Bring linked vault mirrors up to date, so items added in the provider (for
256
+ * example straight in 1Password) are found without anyone pressing Sync.
165
257
  *
258
+ * Without `force`, only vaults older than `maxAgeMs` refresh, with the
259
+ * ordinary sync (one vault listing; an item listing only when the provider
260
+ * says the vault changed). `force` re-lists every given provider vault and
261
+ * bypasses provider caches, but never an active rate limit. Refreshes of the
262
+ * same vault by the same principal share one call per process. The caller
263
+ * waits at most `waitMs`; slower refreshes finish in the background and
264
+ * record their own status. Never throws: failures are recorded as the vault's
265
+ * sync status.
266
+ */
267
+ export declare function refreshLinkedVaults(input: {
268
+ db: Database;
269
+ context: VaultDbContext;
270
+ vaults: readonly LinkedVaultFreshness[];
271
+ providers: VaultProviderRegistry;
272
+ /** Refresh vaults whose last check is at least this old (default: the search age). */
273
+ maxAgeMs?: number;
274
+ force?: boolean;
275
+ waitMs?: number;
276
+ now?: () => Date;
277
+ }): Promise<LinkedVaultRefreshReport>;
278
+ /**
279
+ * The ordinary stale refresh (`refreshLinkedVaults` without `force`).
166
280
  * Returns the ids of the vaults whose mirror changed before the wait ended.
167
281
  */
168
282
  export declare function refreshStaleLinkedVaults(input: {
@@ -1,11 +1,20 @@
1
- import { type VaultFieldInput, type VaultFieldKind, type VaultItemType } from "@opengeni/contracts";
2
- import { type Database, type VaultDbContext, type VaultFieldRow, type VaultRow } from "@opengeni/db";
3
- import { type ProviderCallBudget, type VaultProviderRegistry } from "@opengeni/vaults";
1
+ import { type VaultFieldInput, type VaultFieldKind, type VaultItemType, type VaultProviderOperations } from "@opengeni/contracts";
2
+ import { VaultError, type Database, type VaultDbContext, type VaultFieldRow, type VaultRow } from "@opengeni/db";
3
+ import { type ProviderCallBudget, type VaultProvider, type VaultProviderRegistry } from "@opengeni/vaults";
4
+ import { type ProviderVaultSyncResult } from "./provider-sync.js";
4
5
  /**
5
- * Edits of items in a read-write connected vault (OP-3): the change is written
6
- * to the provider (1Password `items.get` → patch → `items.put`, with the
7
- * mirrored version as the expected version), then the metadata mirror is
8
- * refreshed. Values never touch Opengeni storage.
6
+ * Edits of items in a read-write connected vault (OP-3, external vaults §10):
7
+ * the change is written to the provider (with the mirrored version as the
8
+ * expected version), then the metadata mirror is refreshed. Values never
9
+ * touch Opengeni storage.
10
+ *
11
+ * Every write is checked against the vault's effective operations first (the
12
+ * provider's current capabilities, narrowed by what it last reported for the
13
+ * vault and by the link mode), before any reference is resolved or value
14
+ * generated. A provider mutation is never retried: when its outcome is
15
+ * unknown the caller is told to check the vault, and a write that succeeded
16
+ * but could not be listed yet is reported as saved, never as a failure that
17
+ * invites saving it again.
9
18
  */
10
19
  /** One field change; the REST `VaultFieldInput` and the agent `secret_save` field both fit. */
11
20
  export interface ProviderFieldChange {
@@ -33,10 +42,38 @@ export interface ProviderWriteDeps {
33
42
  resolveReference: (raw: string) => Promise<string>;
34
43
  budget?: ProviderCallBudget;
35
44
  }
45
+ /** An operation a connected vault may not allow; reported in `vault_read_only` details. */
46
+ export type ProviderWriteOperation = "createItem" | "updateItem" | "removeField" | "createTotp" | "updateTotp" | "archiveItem" | "deleteItem" | "restoreItem" | "writeFiles" | "restoreFieldHistory";
47
+ /** The result of a provider write once the mirror was refreshed. */
48
+ export type ProviderWriteOutcome = {
49
+ status: "listed";
50
+ itemId: string;
51
+ } | {
52
+ /**
53
+ * The provider confirmed the write, but the refreshed mirror does not
54
+ * show it yet (for example the refresh was rate limited). It must not be
55
+ * saved again; it appears after the next successful sync.
56
+ */
57
+ status: "saved_not_listed";
58
+ vaultId: string;
59
+ sync: ProviderVaultSyncResult["status"] | "error";
60
+ };
36
61
  /** A connected vault the caller may change items in. */
37
62
  export declare function assertProviderVaultWritable(vault: VaultRow): void;
38
- /** Create (existing = null) or update an item in a connected read-write vault; returns its item id. */
39
- export declare function writeProviderVaultItem(deps: ProviderWriteDeps, input: {
63
+ /**
64
+ * What a connected vault allows now: the provider's current capabilities,
65
+ * narrowed by the operations it last reported for this vault (a read-only
66
+ * share, for example) and by the link mode. Permanent deletion is never
67
+ * allowed.
68
+ */
69
+ export declare function effectiveProviderOperations(vault: VaultRow, provider: Pick<VaultProvider, "capabilities"> | null): VaultProviderOperations;
70
+ /** Refuses a provider operation Opengeni never performs, with the reason. */
71
+ export declare function refuseProviderOperation(vault: VaultRow, operation: Extract<ProviderWriteOperation, "deleteItem" | "restoreItem" | "restoreFieldHistory">): never;
72
+ /**
73
+ * Create (existing = null) or update an item in a connected read-write vault
74
+ * and report whether the refreshed mirror lists it.
75
+ */
76
+ export declare function writeProviderVaultItemOutcome(deps: ProviderWriteDeps, input: {
40
77
  vault: VaultRow;
41
78
  existing: {
42
79
  itemId: string;
@@ -49,9 +86,62 @@ export declare function writeProviderVaultItem(deps: ProviderWriteDeps, input: {
49
86
  change: ProviderItemChange;
50
87
  /** Receives the names of fields whose values were generated. */
51
88
  generated?: string[];
52
- }): Promise<string>;
53
- /** Archive an item in a connected read-write vault (the provider keeps it recoverable). */
89
+ }): Promise<ProviderWriteOutcome>;
90
+ /** Thrown when a confirmed provider write is not listed yet: it must not be saved again. */
91
+ export declare function savedNotListedError(vault: VaultRow, itemName: string, outcome: Extract<ProviderWriteOutcome, {
92
+ status: "saved_not_listed";
93
+ }>): VaultError;
94
+ /**
95
+ * Create (existing = null) or update an item in a connected read-write vault;
96
+ * returns its item id. A confirmed write that is not listed yet throws
97
+ * `provider_inventory_incomplete` with `details.saved = true`.
98
+ */
99
+ export declare function writeProviderVaultItem(deps: ProviderWriteDeps, input: Parameters<typeof writeProviderVaultItemOutcome>[1]): Promise<string>;
100
+ /** Whether an item can leave this vault recoverably (moves need it before anything is written). */
101
+ export declare function providerArchiveKind(vault: VaultRow, provider: Pick<VaultProvider, "capabilities"> | null): "archive" | "trash" | null;
102
+ /**
103
+ * Refuses, before any value is read or written, an item a connected vault
104
+ * cannot receive as a new item (its type, a one-time password, or a file).
105
+ */
106
+ export declare function assertProviderVaultAccepts(deps: ProviderWriteDeps, vault: VaultRow, item: {
107
+ type: VaultItemType;
108
+ fields: ReadonlyArray<{
109
+ kind: VaultFieldKind;
110
+ }>;
111
+ }): Promise<void>;
112
+ /**
113
+ * Refuses a move out of a connected vault that cannot archive the original
114
+ * recoverably (copy instead); returns where the original will go.
115
+ */
116
+ export declare function assertProviderVaultReleases(deps: ProviderWriteDeps, vault: VaultRow): Promise<"archive" | "trash">;
117
+ /** What a fresh provider listing says about one mirrored item. */
118
+ export type ProviderItemObservation = {
119
+ listed: true;
120
+ version: string | null;
121
+ } | {
122
+ listed: false;
123
+ consistent: boolean;
124
+ };
125
+ /**
126
+ * The provider's current version of one item, from a fresh listing of its
127
+ * vault (provider caches bypassed). `version` is null when the provider does
128
+ * not version items. A provider failure is thrown as a `VaultError`.
129
+ *
130
+ * The provider contract has no conditional (compare-and-archive) operation,
131
+ * so this only narrows the window between reading an item and archiving it;
132
+ * it cannot close it.
133
+ */
134
+ export declare function observeProviderItemVersion(deps: ProviderWriteDeps, input: {
135
+ vault: VaultRow;
136
+ providerItemRef: string;
137
+ }): Promise<ProviderItemObservation>;
138
+ /**
139
+ * Archive an item in a connected read-write vault. Only a recoverable archive
140
+ * (the provider's archive or trash) is ever used; permanent deletion is not.
141
+ */
54
142
  export declare function archiveProviderVaultItem(deps: ProviderWriteDeps, input: {
55
143
  vault: VaultRow;
56
144
  itemId: string;
57
- }): Promise<void>;
145
+ }): Promise<{
146
+ archiveKind: "archive" | "trash";
147
+ }>;
@@ -0,0 +1,7 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { pinnedFetch, type OutboundNetworkSettings } from "@opengeni/network";
3
+ import { type RemoteVaultTransport } from "@opengeni/vaults";
4
+ import type { ExternalVaultProviderFactories } from "./external-provider-connections.js";
5
+ /** Shared by API and worker. No credential-bearing request uses ambient fetch. */
6
+ export declare function createPinnedRemoteVaultTransport(settings: OutboundNetworkSettings, fetchImpl?: typeof pinnedFetch): RemoteVaultTransport;
7
+ export declare function createRemoteExternalVaultFactories(settings: Pick<Settings, "environment" | "integrationsAllowPrivateNetworkTargets">): ExternalVaultProviderFactories;
@@ -4,9 +4,19 @@
4
4
  * chosen here (archived dropped, lowest tier, names over aliases, otherwise
5
5
  * ambiguous), provider values are fetched, hosted values decrypted, and the
6
6
  * batch is recorded. A call resolves all of its references or none (I2).
7
+ *
8
+ * A reference that finds nothing first refreshes the connected vaults that
9
+ * could change the answer: those that could hold it at the priority tier that
10
+ * decided the lookup or above, even when a hosted item matched without the
11
+ * field. When one of those could not be checked (unreachable, rate limited,
12
+ * needing reconnection or still refreshing), the failure names that source
13
+ * instead of reporting the secret missing, so no missing-secret ask card is
14
+ * made for a secret that may exist (external vaults §9). Hosted and
15
+ * explicitly named healthy vaults are never blocked by unrelated outages, and
16
+ * lower-priority vaults never block a miss a higher tier decided.
7
17
  */
8
18
  import { type SecretPrincipal, type SecretReference, type SecretReferenceFailure, type SecretResolver } from "@opengeni/contracts";
9
- import { type SecretReferenceCandidate, type SecretResolutionMeta } from "@opengeni/db";
19
+ import { type SecretReferenceCandidate, type SecretResolutionMeta, type VaultRow } from "@opengeni/db";
10
20
  import { type ProviderCallBudget, type VaultProviderRegistry } from "@opengeni/vaults";
11
21
  import type { VaultServiceDeps } from "./types.js";
12
22
  export type SecretResolverOptions = {
@@ -31,4 +41,22 @@ export declare function chooseSecretCandidate(reference: SecretReference, candid
31
41
  } | {
32
42
  failure: SecretReferenceFailure;
33
43
  };
44
+ /**
45
+ * The connected vaults whose mirror decides a reference that found nothing.
46
+ *
47
+ * Where the item could be: the vault a three-segment reference names; for two
48
+ * segments the named vault when the first segment names one (else every
49
+ * connected vault, since it may be `Item/field`); every connected vault for a
50
+ * bare item name; none for an ID.
51
+ *
52
+ * When some item matched but lacks the field (or the one-time password), the
53
+ * lookup keeps the highest-priority tier, so only vaults at that tier or above
54
+ * can change the answer: the connected vaults of matched items there that lack
55
+ * the field, and the connected vaults that could hold the item there but have
56
+ * no match yet. A hosted match never hides a higher-priority connected vault
57
+ * whose provider gained the item or its one-time password, and a vault of a
58
+ * lower-priority tier that is out of date or unavailable never turns a
59
+ * definite miss into an unchecked one.
60
+ */
61
+ export declare function relevantProviderVaults(reference: SecretReference, candidates: readonly SecretReferenceCandidate[], reachable: readonly VaultRow[]): VaultRow[];
34
62
  export declare function createSecretResolver(deps: VaultServiceDeps, principal: SecretPrincipal, opts: SecretResolverOptions): SecretResolver;