@opengeni/core 3.0.1 → 3.1.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.
- package/README.md +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault service for people and API keys (routes) and for agents (secret
|
|
3
|
+
* tools). Routes check the permission each operation needs; the database
|
|
4
|
+
* routines decide reach (who sees which vault) and integrity. This layer
|
|
5
|
+
* encrypts values before writes, decrypts display values after reads, and
|
|
6
|
+
* maps rows to the contract shapes.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
generatePassword,
|
|
11
|
+
isValidTotpInput,
|
|
12
|
+
parseSecretReference,
|
|
13
|
+
parseTotpInput,
|
|
14
|
+
secretNameKey,
|
|
15
|
+
SecretReferenceError,
|
|
16
|
+
totpCodeSync,
|
|
17
|
+
totpRemainingMs,
|
|
18
|
+
systemSecretClock,
|
|
19
|
+
VAULT_LIMITS,
|
|
20
|
+
type CreateVaultItemRequest,
|
|
21
|
+
type CreateVaultRequest,
|
|
22
|
+
type ListVaultItemsQuery,
|
|
23
|
+
type MoveVaultItemRequest,
|
|
24
|
+
type RevealVaultFieldRequest,
|
|
25
|
+
type RevealVaultFieldResponse,
|
|
26
|
+
type SecretDeleteInput,
|
|
27
|
+
type SecretDeleteOutput,
|
|
28
|
+
type SecretGetInput,
|
|
29
|
+
type SecretMoveInput,
|
|
30
|
+
type SecretMoveOutput,
|
|
31
|
+
type SecretPrincipal,
|
|
32
|
+
type SecretSaveInput,
|
|
33
|
+
type SecretSaveOutput,
|
|
34
|
+
type SecretSearchInput,
|
|
35
|
+
type SecretSearchOutput,
|
|
36
|
+
type SessionSecretsView,
|
|
37
|
+
type UpdateVaultItemRequest,
|
|
38
|
+
type UpdateVaultRequest,
|
|
39
|
+
type VaultFieldInput,
|
|
40
|
+
type VaultFieldKind,
|
|
41
|
+
type VaultFieldVersionMeta,
|
|
42
|
+
type VaultItem,
|
|
43
|
+
type VaultItemSummary,
|
|
44
|
+
type VaultPolicy,
|
|
45
|
+
type VaultSummary,
|
|
46
|
+
type VaultVersionCreatedByKind,
|
|
47
|
+
} from "@opengeni/contracts";
|
|
48
|
+
import {
|
|
49
|
+
archiveVaultItem,
|
|
50
|
+
createVault,
|
|
51
|
+
decryptEnvironmentValue,
|
|
52
|
+
deleteVault,
|
|
53
|
+
deleteVaultItemPermanently,
|
|
54
|
+
encryptEnvironmentValue,
|
|
55
|
+
ensureDefaultVault,
|
|
56
|
+
ensureTaskVault,
|
|
57
|
+
getSession,
|
|
58
|
+
getSessionSecretsView,
|
|
59
|
+
getVault,
|
|
60
|
+
getVaultItem,
|
|
61
|
+
listVaultFieldVersions,
|
|
62
|
+
listVaultItems,
|
|
63
|
+
listVaultsForSubject,
|
|
64
|
+
vaultItemCursor,
|
|
65
|
+
moveVaultItem,
|
|
66
|
+
readVaultFieldCiphertext,
|
|
67
|
+
resolveSecretReferenceCandidates,
|
|
68
|
+
restoreVaultItem,
|
|
69
|
+
setSessionVaultPolicy,
|
|
70
|
+
updateVault,
|
|
71
|
+
upsertVaultItem,
|
|
72
|
+
VaultError,
|
|
73
|
+
type SessionSecretsViewRow,
|
|
74
|
+
type VaultDbContext,
|
|
75
|
+
type VaultFieldCiphertext,
|
|
76
|
+
type VaultFieldWrite,
|
|
77
|
+
type VaultItemRow,
|
|
78
|
+
type VaultItemWrite,
|
|
79
|
+
type VaultRow,
|
|
80
|
+
} from "@opengeni/db";
|
|
81
|
+
import {
|
|
82
|
+
createProviderCallBudget,
|
|
83
|
+
EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
84
|
+
isVaultProviderError,
|
|
85
|
+
} from "@opengeni/vaults";
|
|
86
|
+
import { hasPermission } from "../../access";
|
|
87
|
+
import {
|
|
88
|
+
toVaultItem,
|
|
89
|
+
toVaultItemSummary,
|
|
90
|
+
toVaultSummary,
|
|
91
|
+
vaultItemReferences,
|
|
92
|
+
type VaultCallerAuthority,
|
|
93
|
+
} from "./mapping";
|
|
94
|
+
import { vaultErrorCodeForProvider } from "./provider-registry";
|
|
95
|
+
import {
|
|
96
|
+
LINKED_VAULT_OPEN_WAIT_MS,
|
|
97
|
+
LINKED_VAULT_SEARCH_MAX_AGE_MS,
|
|
98
|
+
refreshStaleLinkedVaults,
|
|
99
|
+
} from "./provider-sync";
|
|
100
|
+
import {
|
|
101
|
+
archiveProviderVaultItem,
|
|
102
|
+
writeProviderVaultItem,
|
|
103
|
+
type ProviderWriteDeps,
|
|
104
|
+
} from "./provider-writes";
|
|
105
|
+
import { chooseSecretCandidate, createSecretResolver } from "./secret-resolver";
|
|
106
|
+
import type { VaultRequestActor, VaultServiceDeps } from "./types";
|
|
107
|
+
|
|
108
|
+
export interface VaultService {
|
|
109
|
+
list(actor: VaultRequestActor, workspaceId: string): Promise<VaultSummary[]>;
|
|
110
|
+
get(actor: VaultRequestActor, workspaceId: string, vaultId: string): Promise<VaultSummary>;
|
|
111
|
+
create(
|
|
112
|
+
actor: VaultRequestActor,
|
|
113
|
+
workspaceId: string,
|
|
114
|
+
req: CreateVaultRequest,
|
|
115
|
+
): Promise<VaultSummary>;
|
|
116
|
+
update(
|
|
117
|
+
actor: VaultRequestActor,
|
|
118
|
+
workspaceId: string,
|
|
119
|
+
vaultId: string,
|
|
120
|
+
req: UpdateVaultRequest,
|
|
121
|
+
): Promise<VaultSummary>;
|
|
122
|
+
remove(
|
|
123
|
+
actor: VaultRequestActor,
|
|
124
|
+
workspaceId: string,
|
|
125
|
+
vaultId: string,
|
|
126
|
+
confirmName: string,
|
|
127
|
+
): Promise<void>;
|
|
128
|
+
listItems(
|
|
129
|
+
actor: VaultRequestActor,
|
|
130
|
+
workspaceId: string,
|
|
131
|
+
vaultId: string,
|
|
132
|
+
q: ListVaultItemsQuery,
|
|
133
|
+
): Promise<{ items: VaultItemSummary[]; nextCursor: string | null }>;
|
|
134
|
+
getItem(actor: VaultRequestActor, workspaceId: string, itemId: string): Promise<VaultItem>;
|
|
135
|
+
createItem(
|
|
136
|
+
actor: VaultRequestActor,
|
|
137
|
+
workspaceId: string,
|
|
138
|
+
vaultId: string,
|
|
139
|
+
req: CreateVaultItemRequest,
|
|
140
|
+
): Promise<VaultItem>;
|
|
141
|
+
updateItem(
|
|
142
|
+
actor: VaultRequestActor,
|
|
143
|
+
workspaceId: string,
|
|
144
|
+
itemId: string,
|
|
145
|
+
req: UpdateVaultItemRequest,
|
|
146
|
+
): Promise<VaultItem>;
|
|
147
|
+
archiveItem(actor: VaultRequestActor, workspaceId: string, itemId: string): Promise<void>;
|
|
148
|
+
restoreItem(actor: VaultRequestActor, workspaceId: string, itemId: string): Promise<VaultItem>;
|
|
149
|
+
deleteItemPermanently(
|
|
150
|
+
actor: VaultRequestActor,
|
|
151
|
+
workspaceId: string,
|
|
152
|
+
itemId: string,
|
|
153
|
+
): Promise<void>;
|
|
154
|
+
moveItem(
|
|
155
|
+
actor: VaultRequestActor,
|
|
156
|
+
workspaceId: string,
|
|
157
|
+
itemId: string,
|
|
158
|
+
req: MoveVaultItemRequest,
|
|
159
|
+
): Promise<VaultItem>;
|
|
160
|
+
revealField(
|
|
161
|
+
actor: VaultRequestActor,
|
|
162
|
+
workspaceId: string,
|
|
163
|
+
fieldId: string,
|
|
164
|
+
req: RevealVaultFieldRequest & { itemId?: string },
|
|
165
|
+
): Promise<RevealVaultFieldResponse>;
|
|
166
|
+
listFieldVersions(
|
|
167
|
+
actor: VaultRequestActor,
|
|
168
|
+
workspaceId: string,
|
|
169
|
+
fieldId: string,
|
|
170
|
+
): Promise<VaultFieldVersionMeta[]>;
|
|
171
|
+
restoreFieldVersion(
|
|
172
|
+
actor: VaultRequestActor,
|
|
173
|
+
workspaceId: string,
|
|
174
|
+
itemId: string,
|
|
175
|
+
fieldId: string,
|
|
176
|
+
version: number,
|
|
177
|
+
): Promise<VaultItem>;
|
|
178
|
+
sessionSecrets(
|
|
179
|
+
actor: VaultRequestActor,
|
|
180
|
+
workspaceId: string,
|
|
181
|
+
sessionId: string,
|
|
182
|
+
): Promise<SessionSecretsView>;
|
|
183
|
+
setSessionPolicy(
|
|
184
|
+
actor: VaultRequestActor,
|
|
185
|
+
workspaceId: string,
|
|
186
|
+
sessionId: string,
|
|
187
|
+
policy: VaultPolicy,
|
|
188
|
+
): Promise<SessionSecretsView>;
|
|
189
|
+
// Agent paths (the secret_* tools).
|
|
190
|
+
searchForAgent(principal: SecretPrincipal, input: SecretSearchInput): Promise<SecretSearchOutput>;
|
|
191
|
+
saveForAgent(principal: SecretPrincipal, input: SecretSaveInput): Promise<SecretSaveOutput>;
|
|
192
|
+
moveForAgent(principal: SecretPrincipal, input: SecretMoveInput): Promise<SecretMoveOutput>;
|
|
193
|
+
archiveForAgent(
|
|
194
|
+
principal: SecretPrincipal,
|
|
195
|
+
input: SecretDeleteInput,
|
|
196
|
+
): Promise<SecretDeleteOutput>;
|
|
197
|
+
readForAgent(
|
|
198
|
+
principal: SecretPrincipal,
|
|
199
|
+
input: SecretGetInput,
|
|
200
|
+
permissions: readonly string[],
|
|
201
|
+
): Promise<RevealVaultFieldResponse>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function subjectContext(actor: VaultRequestActor, workspaceId: string): VaultDbContext {
|
|
205
|
+
return {
|
|
206
|
+
kind: "subject",
|
|
207
|
+
accountId: actor.grant.accountId,
|
|
208
|
+
workspaceId,
|
|
209
|
+
subjectId: actor.grant.subjectId,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function principalContext(principal: SecretPrincipal): VaultDbContext {
|
|
214
|
+
return { kind: "principal", principal };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function authorityOf(actor: VaultRequestActor): VaultCallerAuthority {
|
|
218
|
+
return {
|
|
219
|
+
write: hasPermission(actor.grant.permissions, "secrets:write", actor.grant.permissionMode),
|
|
220
|
+
accountAdmin: actor.accountAdmin,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** API keys and configured keys are not people. */
|
|
225
|
+
export function vaultActorIsApiKey(actor: VaultRequestActor): boolean {
|
|
226
|
+
return actor.grant.principalKind === "api_key" || actor.grant.principalKind === "configured_key";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function createdByKind(actor: VaultRequestActor): VaultVersionCreatedByKind {
|
|
230
|
+
return vaultActorIsApiKey(actor) ? "api_key" : "human";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function utf8Size(value: string): number {
|
|
234
|
+
return Buffer.byteLength(value, "utf8");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function requireKey(deps: VaultServiceDeps): Uint8Array {
|
|
238
|
+
if (deps.encryptionKey === null) {
|
|
239
|
+
throw new VaultError(
|
|
240
|
+
"encryption_key_missing",
|
|
241
|
+
"vaults require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return deps.encryptionKey;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function defaultFieldKind(name: string): VaultFieldKind {
|
|
248
|
+
return ["username", "user", "email", "login", "account", "rpid"].includes(secretNameKey(name))
|
|
249
|
+
? "text"
|
|
250
|
+
: "concealed";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type FieldSource = {
|
|
254
|
+
fieldId?: string | undefined;
|
|
255
|
+
name: string;
|
|
256
|
+
kind?: VaultFieldKind | undefined;
|
|
257
|
+
value?: string | undefined;
|
|
258
|
+
generate?: VaultFieldInput["generate"] | undefined;
|
|
259
|
+
fromReference?: string | undefined;
|
|
260
|
+
fileBase64?: string | undefined;
|
|
261
|
+
fileName?: string | undefined;
|
|
262
|
+
restoreVersion?: number | undefined;
|
|
263
|
+
remove?: true | undefined;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
type ItemSource = {
|
|
267
|
+
type?: CreateVaultItemRequest["type"] | undefined;
|
|
268
|
+
name?: string | undefined;
|
|
269
|
+
websites?: string[] | undefined;
|
|
270
|
+
expiresAt?: string | null | undefined;
|
|
271
|
+
primaryFieldName?: string | undefined;
|
|
272
|
+
fields?: FieldSource[] | undefined;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export function createVaultService(deps: VaultServiceDeps): VaultService {
|
|
276
|
+
const clock = deps.clock ?? systemSecretClock;
|
|
277
|
+
|
|
278
|
+
/** Encrypt one field input into the routine's write shape. */
|
|
279
|
+
async function fieldWrite(
|
|
280
|
+
input: FieldSource,
|
|
281
|
+
existing: VaultItemRow | null,
|
|
282
|
+
resolveReference: (raw: string) => Promise<string>,
|
|
283
|
+
generated: string[],
|
|
284
|
+
): Promise<VaultFieldWrite> {
|
|
285
|
+
const current = existing?.fields.find(
|
|
286
|
+
(field) =>
|
|
287
|
+
(input.fieldId !== undefined && field.id === input.fieldId) ||
|
|
288
|
+
secretNameKey(field.name) === secretNameKey(input.name),
|
|
289
|
+
);
|
|
290
|
+
const name = current?.name ?? input.name;
|
|
291
|
+
const write: VaultFieldWrite = { name };
|
|
292
|
+
if (current) write.fieldId = current.id;
|
|
293
|
+
if (current && current.name !== input.name) write.newName = input.name;
|
|
294
|
+
if (input.remove) return { ...write, remove: true };
|
|
295
|
+
if (input.restoreVersion !== undefined)
|
|
296
|
+
return { ...write, restoreVersion: input.restoreVersion };
|
|
297
|
+
let kind: VaultFieldKind = input.kind ?? current?.kind ?? defaultFieldKind(input.name);
|
|
298
|
+
let plaintext: string;
|
|
299
|
+
let size: number;
|
|
300
|
+
if (input.fileBase64 !== undefined) {
|
|
301
|
+
kind = "file";
|
|
302
|
+
const bytes = Buffer.from(input.fileBase64, "base64");
|
|
303
|
+
plaintext = bytes.toString("base64");
|
|
304
|
+
size = bytes.byteLength;
|
|
305
|
+
if (input.fileName !== undefined) write.fileName = input.fileName;
|
|
306
|
+
} else if (input.generate !== undefined) {
|
|
307
|
+
plaintext = generatePassword(input.generate);
|
|
308
|
+
size = utf8Size(plaintext);
|
|
309
|
+
generated.push(input.name);
|
|
310
|
+
} else if (input.fromReference !== undefined) {
|
|
311
|
+
plaintext = await resolveReference(input.fromReference);
|
|
312
|
+
size = utf8Size(plaintext);
|
|
313
|
+
} else {
|
|
314
|
+
plaintext = input.value ?? "";
|
|
315
|
+
size = utf8Size(plaintext);
|
|
316
|
+
}
|
|
317
|
+
if (kind === "totp") {
|
|
318
|
+
if (!isValidTotpInput(plaintext)) {
|
|
319
|
+
throw new VaultError(
|
|
320
|
+
"invalid_secret_name",
|
|
321
|
+
`${input.name} is not a valid two-factor setup key or otpauth:// link`,
|
|
322
|
+
{ field: input.name },
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
plaintext = plaintext.trim();
|
|
326
|
+
}
|
|
327
|
+
const limit = kind === "file" ? VAULT_LIMITS.fileValueMaxBytes : VAULT_LIMITS.textValueMaxBytes;
|
|
328
|
+
if (size > limit) {
|
|
329
|
+
throw new VaultError(
|
|
330
|
+
"secret_value_too_large",
|
|
331
|
+
`${input.name} is larger than ${limit} bytes`,
|
|
332
|
+
{
|
|
333
|
+
field: input.name,
|
|
334
|
+
limit,
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
...write,
|
|
340
|
+
kind,
|
|
341
|
+
valueEncrypted: encryptEnvironmentValue(requireKey(deps), plaintext),
|
|
342
|
+
valueSize: size,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** `fromReference` for people: resolve with their reach, audited as a use. */
|
|
347
|
+
function humanReferenceResolver(actor: VaultRequestActor, workspaceId: string) {
|
|
348
|
+
return async (raw: string): Promise<string> =>
|
|
349
|
+
await resolveTextReference(
|
|
350
|
+
{
|
|
351
|
+
kind: "human",
|
|
352
|
+
accountId: actor.grant.accountId,
|
|
353
|
+
workspaceId,
|
|
354
|
+
subjectId: actor.grant.subjectId,
|
|
355
|
+
verifiedHuman: !vaultActorIsApiKey(actor),
|
|
356
|
+
viaApiKey: vaultActorIsApiKey(actor),
|
|
357
|
+
},
|
|
358
|
+
raw,
|
|
359
|
+
"vault_item_copy",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function resolveTextReference(
|
|
364
|
+
principal: SecretPrincipal,
|
|
365
|
+
raw: string,
|
|
366
|
+
toolName: string,
|
|
367
|
+
): Promise<string> {
|
|
368
|
+
let reference;
|
|
369
|
+
try {
|
|
370
|
+
reference = parseSecretReference(raw);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw new VaultError(
|
|
373
|
+
"invalid_secret_name",
|
|
374
|
+
`fromReference is not a valid reference: ${raw}`,
|
|
375
|
+
{
|
|
376
|
+
reason: error instanceof Error ? error.message : "invalid",
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const resolver = createSecretResolver(deps, principal, { capabilityEnabled: true });
|
|
381
|
+
const values = await resolver.resolve([reference], {
|
|
382
|
+
surface: principal.kind === "attempt" ? "mcp" : "human_reveal",
|
|
383
|
+
toolName,
|
|
384
|
+
});
|
|
385
|
+
const value = [...values.values()][0]!;
|
|
386
|
+
if (value.material.type === "text") return value.material.value;
|
|
387
|
+
if (value.material.type === "bytes") return Buffer.from(value.material.value).toString("utf8");
|
|
388
|
+
return totpCodeSync(value.material.params, clock.now());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function itemWrite(
|
|
392
|
+
req: ItemSource,
|
|
393
|
+
existing: VaultItemRow | null,
|
|
394
|
+
resolveReference: (raw: string) => Promise<string>,
|
|
395
|
+
createdBy: VaultVersionCreatedByKind,
|
|
396
|
+
generated: string[] = [],
|
|
397
|
+
): Promise<VaultItemWrite> {
|
|
398
|
+
const write: VaultItemWrite = { createdByKind: createdBy };
|
|
399
|
+
if (req.name !== undefined) write.name = req.name;
|
|
400
|
+
if (req.type !== undefined) write.type = req.type;
|
|
401
|
+
if (req.websites !== undefined) write.websites = req.websites;
|
|
402
|
+
if (req.expiresAt !== undefined) write.expiresAt = req.expiresAt;
|
|
403
|
+
if (req.primaryFieldName !== undefined) write.primaryFieldName = req.primaryFieldName;
|
|
404
|
+
if (req.fields !== undefined) {
|
|
405
|
+
write.fields = [];
|
|
406
|
+
for (const field of req.fields) {
|
|
407
|
+
write.fields.push(await fieldWrite(field, existing, resolveReference, generated));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return write;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function providerWriteDeps(actor: VaultRequestActor, workspaceId: string): ProviderWriteDeps {
|
|
414
|
+
return {
|
|
415
|
+
db: deps.db,
|
|
416
|
+
context: subjectContext(actor, workspaceId),
|
|
417
|
+
providers: deps.providers ?? EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
418
|
+
resolveReference: humanReferenceResolver(actor, workspaceId),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function key(): Uint8Array | null {
|
|
423
|
+
return deps.encryptionKey;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Move or copy an item when either side is a connected (provider) vault.
|
|
428
|
+
* Values never live in both stores' databases at once: each field is read
|
|
429
|
+
* from the source (service-side, unaudited like other internal copies),
|
|
430
|
+
* written to the target through its own path, and a move then archives the
|
|
431
|
+
* source so the old item stays recoverable. Opengeni-to-Opengeni moves keep
|
|
432
|
+
* the single-statement database move, which preserves the item id.
|
|
433
|
+
*/
|
|
434
|
+
async function moveAcrossProviders(input: {
|
|
435
|
+
context: VaultDbContext;
|
|
436
|
+
providerWrites: ProviderWriteDeps;
|
|
437
|
+
itemId: string;
|
|
438
|
+
targetVaultId: string;
|
|
439
|
+
mode: "move" | "copy";
|
|
440
|
+
newName?: string | undefined;
|
|
441
|
+
createdBy: VaultVersionCreatedByKind;
|
|
442
|
+
}): Promise<VaultItemRow | null> {
|
|
443
|
+
const source = await getVaultItem(deps.db, input.context, { itemId: input.itemId });
|
|
444
|
+
const target = await getVault(deps.db, input.context, { vaultId: input.targetVaultId });
|
|
445
|
+
if (source.vaultSource === "opengeni" && target.source === "opengeni") return null;
|
|
446
|
+
if (source.archivedAt !== null) {
|
|
447
|
+
throw new VaultError("vault_item_not_found", "item is archived; restore it first", {
|
|
448
|
+
itemId: source.id,
|
|
449
|
+
archived: true,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const sourceVault = await getVault(deps.db, input.context, { vaultId: source.vaultId });
|
|
453
|
+
if (input.mode === "move" && sourceVault.mode === "read_only") {
|
|
454
|
+
throw new VaultError("vault_read_only", `${sourceVault.name} is read-only; copy instead`, {
|
|
455
|
+
vaultId: sourceVault.id,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (source.type === "passkey") {
|
|
459
|
+
// A passkey's sign count and credential stay with the store that created it.
|
|
460
|
+
throw new VaultError(
|
|
461
|
+
"provider_unavailable",
|
|
462
|
+
"passkeys stay in the vault they were created in",
|
|
463
|
+
{
|
|
464
|
+
itemId: source.id,
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const name = input.newName ?? source.name;
|
|
469
|
+
const fields: FieldSource[] = [];
|
|
470
|
+
for (const field of source.fields) {
|
|
471
|
+
if (field.currentVersion === null && source.vaultSource === "opengeni") continue;
|
|
472
|
+
const read = await readVaultFieldCiphertext(deps.db, input.context, {
|
|
473
|
+
fieldId: field.id,
|
|
474
|
+
purpose: "internal",
|
|
475
|
+
metadata: { reason: "vault_item_move" },
|
|
476
|
+
});
|
|
477
|
+
const revealed = await revealFromCiphertext(read);
|
|
478
|
+
fields.push(
|
|
479
|
+
revealed.encoding === "base64"
|
|
480
|
+
? {
|
|
481
|
+
name: field.name,
|
|
482
|
+
kind: field.kind,
|
|
483
|
+
fileBase64: revealed.value,
|
|
484
|
+
...(field.fileName ? { fileName: field.fileName } : {}),
|
|
485
|
+
}
|
|
486
|
+
: { name: field.name, kind: field.kind, value: revealed.value },
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const change = {
|
|
490
|
+
type: source.type,
|
|
491
|
+
name,
|
|
492
|
+
websites: source.websites,
|
|
493
|
+
...(source.expiresAt ? { expiresAt: source.expiresAt } : {}),
|
|
494
|
+
fields,
|
|
495
|
+
};
|
|
496
|
+
let created: VaultItemRow;
|
|
497
|
+
if (target.source !== "opengeni") {
|
|
498
|
+
const itemId = await writeProviderVaultItem(input.providerWrites, {
|
|
499
|
+
vault: target,
|
|
500
|
+
existing: null,
|
|
501
|
+
change,
|
|
502
|
+
});
|
|
503
|
+
created = await getVaultItem(deps.db, input.context, { itemId });
|
|
504
|
+
} else {
|
|
505
|
+
const write = await itemWrite(
|
|
506
|
+
change,
|
|
507
|
+
null,
|
|
508
|
+
input.providerWrites.resolveReference,
|
|
509
|
+
input.createdBy,
|
|
510
|
+
);
|
|
511
|
+
created = await upsertVaultItem(deps.db, input.context, {
|
|
512
|
+
...write,
|
|
513
|
+
vaultId: target.id,
|
|
514
|
+
mode: "create",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (input.mode === "move") {
|
|
518
|
+
if (source.vaultSource !== "opengeni") {
|
|
519
|
+
await archiveProviderVaultItem(input.providerWrites, {
|
|
520
|
+
vault: sourceVault,
|
|
521
|
+
itemId: source.id,
|
|
522
|
+
});
|
|
523
|
+
} else {
|
|
524
|
+
await archiveVaultItem(deps.db, input.context, { itemId: source.id });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return created;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function toItem(row: VaultItemRow): VaultItem {
|
|
531
|
+
return toVaultItem(row, key());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function revealFromCiphertext(
|
|
535
|
+
read: VaultFieldCiphertext,
|
|
536
|
+
): Promise<RevealVaultFieldResponse> {
|
|
537
|
+
let plaintext: string;
|
|
538
|
+
let encoding: "utf8" | "base64" = read.kind === "file" ? "base64" : "utf8";
|
|
539
|
+
if (read.source === "opengeni") {
|
|
540
|
+
if (read.valueEncrypted === null) {
|
|
541
|
+
throw new VaultError("vault_field_not_found", "the field has no value at that version");
|
|
542
|
+
}
|
|
543
|
+
plaintext = decryptEnvironmentValue(requireKey(deps), read.valueEncrypted);
|
|
544
|
+
} else {
|
|
545
|
+
try {
|
|
546
|
+
const provider = await (deps.providers ?? EMPTY_VAULT_PROVIDER_REGISTRY).forVault({
|
|
547
|
+
source: read.source,
|
|
548
|
+
providerConnectionId: read.providerConnectionId,
|
|
549
|
+
});
|
|
550
|
+
// Providers charge the budget per call themselves.
|
|
551
|
+
const budget = createProviderCallBudget(VAULT_LIMITS.providerReadsPerAttempt);
|
|
552
|
+
const [value] = await provider.readFields(
|
|
553
|
+
{ connectionId: read.providerConnectionId, budget },
|
|
554
|
+
read.providerVaultRef ?? "",
|
|
555
|
+
read.providerItemRef ?? "",
|
|
556
|
+
[read.providerFieldRef ?? ""],
|
|
557
|
+
);
|
|
558
|
+
if (!value)
|
|
559
|
+
throw new VaultError("vault_field_not_found", "the connected vault has no value");
|
|
560
|
+
if (value.material.type === "bytes") {
|
|
561
|
+
plaintext = Buffer.from(value.material.value).toString("base64");
|
|
562
|
+
encoding = "base64";
|
|
563
|
+
} else if (value.material.type === "text") {
|
|
564
|
+
plaintext = value.material.value;
|
|
565
|
+
} else {
|
|
566
|
+
plaintext = value.material.params.secretBase32;
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (isVaultProviderError(error)) {
|
|
570
|
+
throw new VaultError(vaultErrorCodeForProvider(error.code, "read"), error.message);
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
let totp: RevealVaultFieldResponse["totp"] = null;
|
|
576
|
+
if (read.kind === "totp" && isValidTotpInput(plaintext)) {
|
|
577
|
+
const params = parseTotpInput(plaintext);
|
|
578
|
+
const now = clock.now();
|
|
579
|
+
totp = {
|
|
580
|
+
code: totpCodeSync(params, now),
|
|
581
|
+
periodSeconds: params.periodSeconds,
|
|
582
|
+
expiresAt: new Date(now + totpRemainingMs(params, now)).toISOString(),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return { value: plaintext, encoding, version: read.version, totp };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function toSessionView(
|
|
589
|
+
row: SessionSecretsViewRow,
|
|
590
|
+
actor: VaultCallerAuthority,
|
|
591
|
+
capabilityEnabled: boolean,
|
|
592
|
+
): SessionSecretsView {
|
|
593
|
+
return {
|
|
594
|
+
capabilityEnabled,
|
|
595
|
+
policy: row.policy,
|
|
596
|
+
effectiveVaultIds: row.effectiveVaultIds,
|
|
597
|
+
narrowedBySessionId: row.narrowedBySessionId,
|
|
598
|
+
taskVault: row.taskVault ? toVaultSummary(row.taskVault, actor) : null,
|
|
599
|
+
taskItems: row.taskItems.map((item) => toVaultItemSummary(item, key())),
|
|
600
|
+
reachableVaults: row.reachableVaults.map((vault) => ({
|
|
601
|
+
...toVaultSummary(vault, actor),
|
|
602
|
+
selected: vault.selected,
|
|
603
|
+
selectable: vault.selectable,
|
|
604
|
+
})),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function sessionCapabilityEnabled(
|
|
609
|
+
workspaceId: string,
|
|
610
|
+
sessionId: string,
|
|
611
|
+
): Promise<boolean> {
|
|
612
|
+
const session = await getSession(deps.db, workspaceId, sessionId);
|
|
613
|
+
return session?.agent?.capabilities.secrets ?? true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** A reachable vault by ID or name for an agent (names: current names first). */
|
|
617
|
+
async function agentVault(principal: SecretPrincipal, nameOrId: string): Promise<VaultRow> {
|
|
618
|
+
const vaults = await listVaultsForSubject(deps.db, principalContext(principal), {
|
|
619
|
+
workspaceId: principal.workspaceId,
|
|
620
|
+
});
|
|
621
|
+
const byId = vaults.find((vault) => vault.id === nameOrId.toLowerCase());
|
|
622
|
+
if (byId) return byId;
|
|
623
|
+
const matches = vaults.filter((vault) => secretNameKey(vault.name) === secretNameKey(nameOrId));
|
|
624
|
+
if (matches.length === 0) {
|
|
625
|
+
throw new VaultError("vault_not_found", `no reachable vault is named ${nameOrId}`);
|
|
626
|
+
}
|
|
627
|
+
const lowest = Math.min(...matches.map((vault) => vault.tier));
|
|
628
|
+
const best = matches.filter((vault) => vault.tier === lowest);
|
|
629
|
+
if (best.length > 1) {
|
|
630
|
+
throw new VaultError(
|
|
631
|
+
"vault_name_conflict",
|
|
632
|
+
`${best.length} vaults are named ${nameOrId}; use the vault ID`,
|
|
633
|
+
{
|
|
634
|
+
vaultIds: best.map((vault) => vault.id),
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return best[0]!;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** The one item a reference names, for agent tools that act on items. */
|
|
642
|
+
async function agentItemByReference(
|
|
643
|
+
principal: SecretPrincipal,
|
|
644
|
+
raw: string,
|
|
645
|
+
): Promise<{ itemId: string; fieldId: string | null; version: number | null }> {
|
|
646
|
+
let reference;
|
|
647
|
+
try {
|
|
648
|
+
reference = parseSecretReference(raw.startsWith("{{") ? raw : `{{secret:${raw}}}`);
|
|
649
|
+
} catch {
|
|
650
|
+
throw new SecretReferenceError(
|
|
651
|
+
[{ raw, code: "invalid_reference", message: "not a valid secret reference" }],
|
|
652
|
+
null,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const { meta, candidates } = await resolveSecretReferenceCandidates(
|
|
656
|
+
deps.db,
|
|
657
|
+
principalContext(principal),
|
|
658
|
+
{
|
|
659
|
+
refs: [
|
|
660
|
+
reference.target.form === "id"
|
|
661
|
+
? {
|
|
662
|
+
index: 0,
|
|
663
|
+
form: "id",
|
|
664
|
+
itemId: reference.target.itemId,
|
|
665
|
+
field: reference.target.field,
|
|
666
|
+
otp: reference.otp,
|
|
667
|
+
previous: reference.previous,
|
|
668
|
+
}
|
|
669
|
+
: {
|
|
670
|
+
index: 0,
|
|
671
|
+
form: "path",
|
|
672
|
+
segments: [...reference.target.segments],
|
|
673
|
+
otp: reference.otp,
|
|
674
|
+
previous: reference.previous,
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
const outcome = chooseSecretCandidate(reference, candidates, meta);
|
|
680
|
+
if ("failure" in outcome) throw new SecretReferenceError([outcome.failure], null);
|
|
681
|
+
return {
|
|
682
|
+
itemId: outcome.candidate.itemId,
|
|
683
|
+
fieldId: outcome.candidate.fieldId,
|
|
684
|
+
version: outcome.candidate.version,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function agentSaveVault(
|
|
689
|
+
principal: SecretPrincipal,
|
|
690
|
+
input: SecretSaveInput,
|
|
691
|
+
): Promise<VaultRow> {
|
|
692
|
+
const context = principalContext(principal);
|
|
693
|
+
if (input.vault !== undefined) return await agentVault(principal, input.vault);
|
|
694
|
+
const saveTo = input.saveTo ?? "user";
|
|
695
|
+
if (saveTo === "task") {
|
|
696
|
+
if (principal.kind !== "attempt") {
|
|
697
|
+
throw new VaultError("secret_save_forbidden", "only agent runs have a task vault");
|
|
698
|
+
}
|
|
699
|
+
return await ensureTaskVault(deps.db, context, {
|
|
700
|
+
workspaceId: principal.workspaceId,
|
|
701
|
+
sessionId: principal.sessionId,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
return await ensureDefaultVault(deps.db, context, {
|
|
706
|
+
workspaceId: principal.workspaceId,
|
|
707
|
+
ownerKind: saveTo,
|
|
708
|
+
});
|
|
709
|
+
} catch (error) {
|
|
710
|
+
// No acting person: "Only me" falls back to the workspace vault.
|
|
711
|
+
if (input.saveTo === undefined && error instanceof VaultError) {
|
|
712
|
+
return await ensureDefaultVault(deps.db, context, {
|
|
713
|
+
workspaceId: principal.workspaceId,
|
|
714
|
+
ownerKind: "workspace",
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
async list(actor, workspaceId) {
|
|
723
|
+
const rows = await listVaultsForSubject(deps.db, subjectContext(actor, workspaceId), {
|
|
724
|
+
workspaceId,
|
|
725
|
+
});
|
|
726
|
+
const authority = authorityOf(actor);
|
|
727
|
+
return rows.map((row) => toVaultSummary(row, authority));
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
async get(actor, workspaceId, vaultId) {
|
|
731
|
+
const rows = await listVaultsForSubject(deps.db, subjectContext(actor, workspaceId), {
|
|
732
|
+
workspaceId,
|
|
733
|
+
});
|
|
734
|
+
const row = rows.find((candidate) => candidate.id === vaultId);
|
|
735
|
+
if (!row) throw new VaultError("vault_not_found", "vault not found", { vaultId });
|
|
736
|
+
return toVaultSummary(row, authorityOf(actor));
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
async create(actor, workspaceId, req) {
|
|
740
|
+
if (req.ownerKind === "organization" && !actor.accountAdmin) {
|
|
741
|
+
throw new VaultError("vault_permission_denied", "organization vaults need account:admin");
|
|
742
|
+
}
|
|
743
|
+
const row = await createVault(deps.db, subjectContext(actor, workspaceId), {
|
|
744
|
+
name: req.name,
|
|
745
|
+
description: req.description ?? null,
|
|
746
|
+
ownerKind: req.ownerKind,
|
|
747
|
+
...(req.availability ? { availability: req.availability } : {}),
|
|
748
|
+
allowOrganization: actor.accountAdmin,
|
|
749
|
+
});
|
|
750
|
+
return toVaultSummary(row, authorityOf(actor));
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
async update(actor, workspaceId, vaultId, req) {
|
|
754
|
+
const row = await updateVault(deps.db, subjectContext(actor, workspaceId), {
|
|
755
|
+
vaultId,
|
|
756
|
+
expectedGeneration: req.expectedGeneration,
|
|
757
|
+
...(req.name !== undefined ? { name: req.name } : {}),
|
|
758
|
+
...(req.description !== undefined ? { description: req.description } : {}),
|
|
759
|
+
...(req.availability !== undefined ? { availability: req.availability } : {}),
|
|
760
|
+
allowOrganization: actor.accountAdmin,
|
|
761
|
+
});
|
|
762
|
+
return toVaultSummary(row, authorityOf(actor));
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
async remove(actor, workspaceId, vaultId, confirmName) {
|
|
766
|
+
await deleteVault(deps.db, subjectContext(actor, workspaceId), {
|
|
767
|
+
vaultId,
|
|
768
|
+
confirmName,
|
|
769
|
+
allowOrganization: actor.accountAdmin,
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
async listItems(actor, workspaceId, vaultId, q) {
|
|
774
|
+
const context = subjectContext(actor, workspaceId);
|
|
775
|
+
// Opening a linked vault shows items added straight in the provider
|
|
776
|
+
// (1Password) without anyone pressing Sync: the first page refreshes a
|
|
777
|
+
// stale mirror, on the same budget as agent search.
|
|
778
|
+
if (deps.providers && q.cursor === undefined) {
|
|
779
|
+
const vault = await getVault(deps.db, context, { vaultId });
|
|
780
|
+
await refreshStaleLinkedVaults({
|
|
781
|
+
db: deps.db,
|
|
782
|
+
context,
|
|
783
|
+
vaults: [vault],
|
|
784
|
+
providers: deps.providers,
|
|
785
|
+
maxAgeMs: LINKED_VAULT_SEARCH_MAX_AGE_MS,
|
|
786
|
+
waitMs: LINKED_VAULT_OPEN_WAIT_MS,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const result = await listVaultItems(deps.db, context, {
|
|
790
|
+
vaultId,
|
|
791
|
+
...(q.q !== undefined ? { q: q.q } : {}),
|
|
792
|
+
...(q.type !== undefined ? { type: q.type } : {}),
|
|
793
|
+
...(q.includeArchived !== undefined ? { includeArchived: q.includeArchived } : {}),
|
|
794
|
+
...(q.cursor !== undefined ? { cursor: q.cursor } : {}),
|
|
795
|
+
...(q.limit !== undefined ? { limit: q.limit } : {}),
|
|
796
|
+
});
|
|
797
|
+
return {
|
|
798
|
+
items: result.items.map((item) => toVaultItemSummary(item, key())),
|
|
799
|
+
nextCursor: result.nextCursor,
|
|
800
|
+
};
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
async getItem(actor, workspaceId, itemId) {
|
|
804
|
+
return toItem(await getVaultItem(deps.db, subjectContext(actor, workspaceId), { itemId }));
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
async createItem(actor, workspaceId, vaultId, req) {
|
|
808
|
+
const context = subjectContext(actor, workspaceId);
|
|
809
|
+
const vault = await getVault(deps.db, context, { vaultId });
|
|
810
|
+
if (vault.source !== "opengeni") {
|
|
811
|
+
// W5: connected read-write vaults change through their provider.
|
|
812
|
+
const itemId = await writeProviderVaultItem(providerWriteDeps(actor, workspaceId), {
|
|
813
|
+
vault,
|
|
814
|
+
existing: null,
|
|
815
|
+
change: req,
|
|
816
|
+
});
|
|
817
|
+
return toItem(await getVaultItem(deps.db, context, { itemId }));
|
|
818
|
+
}
|
|
819
|
+
const write = await itemWrite(
|
|
820
|
+
req,
|
|
821
|
+
null,
|
|
822
|
+
humanReferenceResolver(actor, workspaceId),
|
|
823
|
+
createdByKind(actor),
|
|
824
|
+
);
|
|
825
|
+
return toItem(
|
|
826
|
+
await upsertVaultItem(deps.db, subjectContext(actor, workspaceId), {
|
|
827
|
+
...write,
|
|
828
|
+
vaultId,
|
|
829
|
+
mode: "create",
|
|
830
|
+
}),
|
|
831
|
+
);
|
|
832
|
+
},
|
|
833
|
+
|
|
834
|
+
async updateItem(actor, workspaceId, itemId, req) {
|
|
835
|
+
const context = subjectContext(actor, workspaceId);
|
|
836
|
+
const existing = await getVaultItem(deps.db, context, { itemId });
|
|
837
|
+
const { expectedGeneration, ...changes } = req;
|
|
838
|
+
if (existing.vaultSource !== "opengeni") {
|
|
839
|
+
const vault = await getVault(deps.db, context, { vaultId: existing.vaultId });
|
|
840
|
+
const updatedId = await writeProviderVaultItem(providerWriteDeps(actor, workspaceId), {
|
|
841
|
+
vault,
|
|
842
|
+
existing: {
|
|
843
|
+
itemId,
|
|
844
|
+
name: existing.name,
|
|
845
|
+
type: existing.type,
|
|
846
|
+
websites: existing.websites,
|
|
847
|
+
fields: existing.fields,
|
|
848
|
+
expectedGeneration,
|
|
849
|
+
},
|
|
850
|
+
change: changes,
|
|
851
|
+
});
|
|
852
|
+
return toItem(await getVaultItem(deps.db, context, { itemId: updatedId }));
|
|
853
|
+
}
|
|
854
|
+
const write = await itemWrite(
|
|
855
|
+
changes,
|
|
856
|
+
existing,
|
|
857
|
+
humanReferenceResolver(actor, workspaceId),
|
|
858
|
+
createdByKind(actor),
|
|
859
|
+
);
|
|
860
|
+
return toItem(
|
|
861
|
+
await upsertVaultItem(deps.db, context, { ...write, itemId, expectedGeneration }),
|
|
862
|
+
);
|
|
863
|
+
},
|
|
864
|
+
|
|
865
|
+
async archiveItem(actor, workspaceId, itemId) {
|
|
866
|
+
const context = subjectContext(actor, workspaceId);
|
|
867
|
+
const existing = await getVaultItem(deps.db, context, { itemId });
|
|
868
|
+
if (existing.vaultSource !== "opengeni") {
|
|
869
|
+
const vault = await getVault(deps.db, context, { vaultId: existing.vaultId });
|
|
870
|
+
await archiveProviderVaultItem(providerWriteDeps(actor, workspaceId), { vault, itemId });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
await archiveVaultItem(deps.db, context, { itemId });
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
async restoreItem(actor, workspaceId, itemId) {
|
|
877
|
+
return toItem(
|
|
878
|
+
await restoreVaultItem(deps.db, subjectContext(actor, workspaceId), { itemId }),
|
|
879
|
+
);
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
async deleteItemPermanently(actor, workspaceId, itemId) {
|
|
883
|
+
await deleteVaultItemPermanently(deps.db, subjectContext(actor, workspaceId), { itemId });
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
async moveItem(actor, workspaceId, itemId, req) {
|
|
887
|
+
const crossed = await moveAcrossProviders({
|
|
888
|
+
context: subjectContext(actor, workspaceId),
|
|
889
|
+
providerWrites: providerWriteDeps(actor, workspaceId),
|
|
890
|
+
itemId,
|
|
891
|
+
targetVaultId: req.targetVaultId,
|
|
892
|
+
mode: req.mode,
|
|
893
|
+
newName: req.newName,
|
|
894
|
+
createdBy: createdByKind(actor),
|
|
895
|
+
});
|
|
896
|
+
if (crossed) return toItem(crossed);
|
|
897
|
+
return toItem(
|
|
898
|
+
await moveVaultItem(deps.db, subjectContext(actor, workspaceId), {
|
|
899
|
+
itemId,
|
|
900
|
+
targetVaultId: req.targetVaultId,
|
|
901
|
+
mode: req.mode,
|
|
902
|
+
...(req.newName !== undefined ? { newName: req.newName } : {}),
|
|
903
|
+
}),
|
|
904
|
+
);
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
async revealField(actor, workspaceId, fieldId, req) {
|
|
908
|
+
const read = await readVaultFieldCiphertext(deps.db, subjectContext(actor, workspaceId), {
|
|
909
|
+
fieldId,
|
|
910
|
+
...(req.version !== undefined ? { version: req.version } : {}),
|
|
911
|
+
purpose: "reveal",
|
|
912
|
+
metadata: { viaApiKey: vaultActorIsApiKey(actor) },
|
|
913
|
+
});
|
|
914
|
+
if (req.itemId !== undefined && read.itemId !== req.itemId) {
|
|
915
|
+
throw new VaultError("vault_field_not_found", "field not found", { fieldId });
|
|
916
|
+
}
|
|
917
|
+
return await revealFromCiphertext(read);
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
async listFieldVersions(actor, workspaceId, fieldId) {
|
|
921
|
+
return await listVaultFieldVersions(deps.db, subjectContext(actor, workspaceId), { fieldId });
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
async restoreFieldVersion(actor, workspaceId, itemId, fieldId, version) {
|
|
925
|
+
const context = subjectContext(actor, workspaceId);
|
|
926
|
+
const existing = await getVaultItem(deps.db, context, { itemId });
|
|
927
|
+
const field = existing.fields.find((candidate) => candidate.id === fieldId);
|
|
928
|
+
if (!field) throw new VaultError("vault_field_not_found", "field not found", { fieldId });
|
|
929
|
+
return toItem(
|
|
930
|
+
await upsertVaultItem(deps.db, context, {
|
|
931
|
+
itemId,
|
|
932
|
+
createdByKind: createdByKind(actor),
|
|
933
|
+
fields: [{ fieldId, name: field.name, restoreVersion: version }],
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
async sessionSecrets(actor, workspaceId, sessionId) {
|
|
939
|
+
const row = await getSessionSecretsView(deps.db, subjectContext(actor, workspaceId), {
|
|
940
|
+
sessionId,
|
|
941
|
+
});
|
|
942
|
+
return toSessionView(
|
|
943
|
+
row,
|
|
944
|
+
authorityOf(actor),
|
|
945
|
+
await sessionCapabilityEnabled(workspaceId, sessionId),
|
|
946
|
+
);
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
async setSessionPolicy(actor, workspaceId, sessionId, policy) {
|
|
950
|
+
const row = await setSessionVaultPolicy(deps.db, subjectContext(actor, workspaceId), {
|
|
951
|
+
sessionId,
|
|
952
|
+
policy,
|
|
953
|
+
});
|
|
954
|
+
return toSessionView(
|
|
955
|
+
row,
|
|
956
|
+
authorityOf(actor),
|
|
957
|
+
await sessionCapabilityEnabled(workspaceId, sessionId),
|
|
958
|
+
);
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
async searchForAgent(principal, input) {
|
|
962
|
+
const context = principalContext(principal);
|
|
963
|
+
let reachable = await listVaultsForSubject(deps.db, context, {
|
|
964
|
+
workspaceId: principal.workspaceId,
|
|
965
|
+
});
|
|
966
|
+
// Items added straight in a connected provider (1Password) appear here
|
|
967
|
+
// without anyone pressing Sync: stale mirrors refresh before the scan.
|
|
968
|
+
if (deps.providers) {
|
|
969
|
+
const changed = await refreshStaleLinkedVaults({
|
|
970
|
+
db: deps.db,
|
|
971
|
+
context,
|
|
972
|
+
vaults: reachable,
|
|
973
|
+
providers: deps.providers,
|
|
974
|
+
maxAgeMs: LINKED_VAULT_SEARCH_MAX_AGE_MS,
|
|
975
|
+
});
|
|
976
|
+
if (changed.length > 0) {
|
|
977
|
+
reachable = await listVaultsForSubject(deps.db, context, {
|
|
978
|
+
workspaceId: principal.workspaceId,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const byTier = (vaults: VaultRow[]) =>
|
|
983
|
+
[...vaults].sort(
|
|
984
|
+
(left, right) => left.tier - right.tier || left.name.localeCompare(right.name),
|
|
985
|
+
);
|
|
986
|
+
const vaults = byTier(
|
|
987
|
+
input.vault !== undefined ? [await agentVault(principal, input.vault)] : reachable,
|
|
988
|
+
);
|
|
989
|
+
const limit = input.limit ?? 20;
|
|
990
|
+
const filters = agentSearchFilterKey(input);
|
|
991
|
+
// Resume exactly after the last item the previous page examined.
|
|
992
|
+
let startIndex = 0;
|
|
993
|
+
let startAfter: string | null = null;
|
|
994
|
+
if (input.cursor !== undefined) {
|
|
995
|
+
const cursor = decodeAgentSearchCursor(input.cursor);
|
|
996
|
+
const index =
|
|
997
|
+
cursor && cursor.f === filters ? vaults.findIndex((vault) => vault.id === cursor.v) : -1;
|
|
998
|
+
if (!cursor || index < 0) {
|
|
999
|
+
throw new VaultError(
|
|
1000
|
+
"secret_search_cursor_invalid",
|
|
1001
|
+
"the cursor belongs to a different search or a vault you can no longer reach; search again without it",
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
startIndex = index;
|
|
1005
|
+
startAfter = cursor.c;
|
|
1006
|
+
}
|
|
1007
|
+
const website = input.website?.trim().toLowerCase();
|
|
1008
|
+
const hostOf = (value: string) => {
|
|
1009
|
+
try {
|
|
1010
|
+
return new URL(value.includes("://") ? value : `https://${value}`).hostname.replace(
|
|
1011
|
+
/^www\./,
|
|
1012
|
+
"",
|
|
1013
|
+
);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return value.toLowerCase();
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
const wantedHost = website ? hostOf(website) : null;
|
|
1019
|
+
const matchesWebsite = (row: VaultItemRow) =>
|
|
1020
|
+
wantedHost === null ||
|
|
1021
|
+
row.websites.some((site) => {
|
|
1022
|
+
const host = hostOf(site);
|
|
1023
|
+
return (
|
|
1024
|
+
host === wantedHost ||
|
|
1025
|
+
wantedHost.endsWith(`.${host}`) ||
|
|
1026
|
+
host.endsWith(`.${wantedHost}`)
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
const items: SecretSearchOutput["items"] = [];
|
|
1030
|
+
let scanned = 0;
|
|
1031
|
+
let next: AgentSearchCursor | null = null;
|
|
1032
|
+
search: for (let index = startIndex; index < vaults.length; index++) {
|
|
1033
|
+
const vault = vaults[index]!;
|
|
1034
|
+
let after = index === startIndex ? startAfter : null;
|
|
1035
|
+
for (;;) {
|
|
1036
|
+
const page = await listVaultItems(deps.db, context, {
|
|
1037
|
+
vaultId: vault.id,
|
|
1038
|
+
...(input.query !== undefined ? { q: input.query } : {}),
|
|
1039
|
+
...(input.type !== undefined ? { type: input.type } : {}),
|
|
1040
|
+
...(after !== null ? { cursor: after } : {}),
|
|
1041
|
+
limit: Math.min(200, VAULT_LIMITS.searchScanMax - scanned),
|
|
1042
|
+
});
|
|
1043
|
+
for (const [position, row] of page.items.entries()) {
|
|
1044
|
+
scanned++;
|
|
1045
|
+
after = vaultItemCursor(row);
|
|
1046
|
+
if (matchesWebsite(row)) items.push(agentSearchResult(vault, row));
|
|
1047
|
+
if (items.length < limit && scanned < VAULT_LIMITS.searchScanMax) continue;
|
|
1048
|
+
const pageHasMore = position < page.items.length - 1 || page.nextCursor !== null;
|
|
1049
|
+
if (pageHasMore) next = { f: filters, v: vault.id, c: after };
|
|
1050
|
+
else if (index < vaults.length - 1)
|
|
1051
|
+
next = { f: filters, v: vaults[index + 1]!.id, c: null };
|
|
1052
|
+
break search;
|
|
1053
|
+
}
|
|
1054
|
+
if (page.nextCursor === null) break;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
items,
|
|
1059
|
+
truncated: next !== null,
|
|
1060
|
+
nextCursor: next === null ? null : encodeAgentSearchCursor(next),
|
|
1061
|
+
...(input.cursor === undefined
|
|
1062
|
+
? {
|
|
1063
|
+
vaults: byTier(reachable).map((vault) => ({
|
|
1064
|
+
id: vault.id,
|
|
1065
|
+
name: vault.name,
|
|
1066
|
+
ownerKind: vault.ownerKind,
|
|
1067
|
+
source: vault.source,
|
|
1068
|
+
mode: vault.mode,
|
|
1069
|
+
itemCount: vault.itemCount,
|
|
1070
|
+
})),
|
|
1071
|
+
}
|
|
1072
|
+
: {}),
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
function agentSearchResult(vault: VaultRow, row: VaultItemRow) {
|
|
1076
|
+
const summary = toVaultItemSummary(row, key());
|
|
1077
|
+
return {
|
|
1078
|
+
vault: {
|
|
1079
|
+
id: vault.id,
|
|
1080
|
+
name: vault.name,
|
|
1081
|
+
ownerKind: vault.ownerKind,
|
|
1082
|
+
source: vault.source,
|
|
1083
|
+
mode: vault.mode,
|
|
1084
|
+
},
|
|
1085
|
+
item: {
|
|
1086
|
+
id: row.id,
|
|
1087
|
+
name: row.name,
|
|
1088
|
+
type: row.type,
|
|
1089
|
+
websites: row.websites,
|
|
1090
|
+
username: summary.username,
|
|
1091
|
+
hasTotp: summary.hasTotp,
|
|
1092
|
+
hasPasskey: summary.hasPasskey,
|
|
1093
|
+
expiresAt: row.expiresAt,
|
|
1094
|
+
},
|
|
1095
|
+
fields: row.fields.map((field) => ({ name: field.name, kind: field.kind })),
|
|
1096
|
+
references: summary.references,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
|
|
1101
|
+
async saveForAgent(principal, input) {
|
|
1102
|
+
const context = principalContext(principal);
|
|
1103
|
+
const vault = await agentSaveVault(principal, input);
|
|
1104
|
+
const existingPage = await listVaultItems(deps.db, context, {
|
|
1105
|
+
vaultId: vault.id,
|
|
1106
|
+
q: input.item,
|
|
1107
|
+
limit: VAULT_LIMITS.searchResultsMax,
|
|
1108
|
+
});
|
|
1109
|
+
const existing =
|
|
1110
|
+
existingPage.items.find((item) => secretNameKey(item.name) === secretNameKey(input.item)) ??
|
|
1111
|
+
null;
|
|
1112
|
+
if (input.mode === "create" && existing) {
|
|
1113
|
+
throw new VaultError(
|
|
1114
|
+
"vault_item_name_conflict",
|
|
1115
|
+
`${vault.name} already has an item named ${input.item}`,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
if (input.mode === "update" && !existing) {
|
|
1119
|
+
throw new VaultError(
|
|
1120
|
+
"vault_item_not_found",
|
|
1121
|
+
`${vault.name} has no item named ${input.item}`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
const fullExisting = existing
|
|
1125
|
+
? await getVaultItem(deps.db, context, { itemId: existing.id })
|
|
1126
|
+
: null;
|
|
1127
|
+
const fields: FieldSource[] = [...(input.fields ?? [])];
|
|
1128
|
+
if (input.username !== undefined) {
|
|
1129
|
+
fields.unshift({ name: "username", kind: "text", value: input.username });
|
|
1130
|
+
}
|
|
1131
|
+
const generated: string[] = [];
|
|
1132
|
+
if (vault.source !== "opengeni") {
|
|
1133
|
+
// W5: secret_save into a read-write connected vault writes through its provider.
|
|
1134
|
+
const itemId = await writeProviderVaultItem(
|
|
1135
|
+
{
|
|
1136
|
+
db: deps.db,
|
|
1137
|
+
context,
|
|
1138
|
+
providers: deps.providers ?? EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
1139
|
+
resolveReference: (raw) => resolveTextReference(principal, raw, "secret_save"),
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
vault,
|
|
1143
|
+
existing: fullExisting
|
|
1144
|
+
? {
|
|
1145
|
+
itemId: fullExisting.id,
|
|
1146
|
+
name: fullExisting.name,
|
|
1147
|
+
type: fullExisting.type,
|
|
1148
|
+
websites: fullExisting.websites,
|
|
1149
|
+
fields: fullExisting.fields,
|
|
1150
|
+
}
|
|
1151
|
+
: null,
|
|
1152
|
+
change: {
|
|
1153
|
+
...(existing ? {} : { type: input.type, name: input.item }),
|
|
1154
|
+
...(input.websites !== undefined ? { websites: input.websites } : {}),
|
|
1155
|
+
fields,
|
|
1156
|
+
},
|
|
1157
|
+
generated,
|
|
1158
|
+
},
|
|
1159
|
+
);
|
|
1160
|
+
const saved = await getVaultItem(deps.db, context, { itemId });
|
|
1161
|
+
return {
|
|
1162
|
+
item: toVaultItemSummary(saved, key()),
|
|
1163
|
+
references: vaultItemReferences(saved),
|
|
1164
|
+
generatedFields: generated,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
const write = await itemWrite(
|
|
1168
|
+
{
|
|
1169
|
+
...(existing ? {} : { type: input.type, name: input.item }),
|
|
1170
|
+
...(input.websites !== undefined ? { websites: input.websites } : {}),
|
|
1171
|
+
fields,
|
|
1172
|
+
},
|
|
1173
|
+
fullExisting,
|
|
1174
|
+
(raw) => resolveTextReference(principal, raw, "secret_save"),
|
|
1175
|
+
"agent",
|
|
1176
|
+
generated,
|
|
1177
|
+
);
|
|
1178
|
+
const row = await upsertVaultItem(
|
|
1179
|
+
deps.db,
|
|
1180
|
+
context,
|
|
1181
|
+
fullExisting
|
|
1182
|
+
? { ...write, itemId: fullExisting.id }
|
|
1183
|
+
: { ...write, vaultId: vault.id, mode: "create" },
|
|
1184
|
+
);
|
|
1185
|
+
const summary = toVaultItemSummary(row, key());
|
|
1186
|
+
return { item: summary, references: vaultItemReferences(row), generatedFields: generated };
|
|
1187
|
+
},
|
|
1188
|
+
|
|
1189
|
+
async moveForAgent(principal, input) {
|
|
1190
|
+
const context = principalContext(principal);
|
|
1191
|
+
const target = await agentItemByReference(principal, input.reference);
|
|
1192
|
+
const vault = await agentVault(principal, input.toVault);
|
|
1193
|
+
const crossed = await moveAcrossProviders({
|
|
1194
|
+
context,
|
|
1195
|
+
providerWrites: {
|
|
1196
|
+
db: deps.db,
|
|
1197
|
+
context,
|
|
1198
|
+
providers: deps.providers ?? EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
1199
|
+
resolveReference: (raw) => resolveTextReference(principal, raw, "secret_move"),
|
|
1200
|
+
},
|
|
1201
|
+
itemId: target.itemId,
|
|
1202
|
+
targetVaultId: vault.id,
|
|
1203
|
+
mode: input.mode ?? "move",
|
|
1204
|
+
newName: input.newName,
|
|
1205
|
+
createdBy: "agent",
|
|
1206
|
+
});
|
|
1207
|
+
if (crossed) {
|
|
1208
|
+
return {
|
|
1209
|
+
item: toVaultItemSummary(crossed, key()),
|
|
1210
|
+
references: vaultItemReferences(crossed),
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
const row = await moveVaultItem(deps.db, context, {
|
|
1214
|
+
itemId: target.itemId,
|
|
1215
|
+
targetVaultId: vault.id,
|
|
1216
|
+
mode: input.mode ?? "move",
|
|
1217
|
+
...(input.newName !== undefined ? { newName: input.newName } : {}),
|
|
1218
|
+
});
|
|
1219
|
+
return { item: toVaultItemSummary(row, key()), references: vaultItemReferences(row) };
|
|
1220
|
+
},
|
|
1221
|
+
|
|
1222
|
+
async archiveForAgent(principal, input) {
|
|
1223
|
+
const target = await agentItemByReference(principal, input.reference);
|
|
1224
|
+
const context = principalContext(principal);
|
|
1225
|
+
const current = await getVaultItem(deps.db, context, { itemId: target.itemId });
|
|
1226
|
+
if (current.vaultSource !== "opengeni") {
|
|
1227
|
+
const vault = await getVault(deps.db, context, { vaultId: current.vaultId });
|
|
1228
|
+
await archiveProviderVaultItem(
|
|
1229
|
+
{
|
|
1230
|
+
db: deps.db,
|
|
1231
|
+
context,
|
|
1232
|
+
providers: deps.providers ?? EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
1233
|
+
resolveReference: (raw) => resolveTextReference(principal, raw, "secret_delete"),
|
|
1234
|
+
},
|
|
1235
|
+
{ vault, itemId: target.itemId },
|
|
1236
|
+
);
|
|
1237
|
+
return {
|
|
1238
|
+
archived: true,
|
|
1239
|
+
restoreHint: `A person can restore ${current.name} from the provider's archive.`,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const row = await archiveVaultItem(deps.db, context, {
|
|
1243
|
+
itemId: target.itemId,
|
|
1244
|
+
});
|
|
1245
|
+
return {
|
|
1246
|
+
archived: true,
|
|
1247
|
+
restoreHint: `A person can restore ${row.name} from the vault's archived items.`,
|
|
1248
|
+
};
|
|
1249
|
+
},
|
|
1250
|
+
|
|
1251
|
+
async readForAgent(principal, input, permissions) {
|
|
1252
|
+
if (!permissions.includes("secrets:read")) {
|
|
1253
|
+
throw new VaultError(
|
|
1254
|
+
"vault_permission_denied",
|
|
1255
|
+
"reading plaintext needs the secrets:read permission; use references instead",
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
const target = await agentItemByReference(principal, input.reference);
|
|
1259
|
+
if (target.fieldId === null) {
|
|
1260
|
+
throw new VaultError("vault_field_not_found", "the reference names no field");
|
|
1261
|
+
}
|
|
1262
|
+
const read = await readVaultFieldCiphertext(deps.db, principalContext(principal), {
|
|
1263
|
+
fieldId: target.fieldId,
|
|
1264
|
+
...(input.version !== undefined
|
|
1265
|
+
? { version: input.version }
|
|
1266
|
+
: target.version !== null
|
|
1267
|
+
? { version: target.version }
|
|
1268
|
+
: {}),
|
|
1269
|
+
purpose: "agent_read",
|
|
1270
|
+
metadata: { reference: input.reference },
|
|
1271
|
+
});
|
|
1272
|
+
return await revealFromCiphertext(read);
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/** Position of an agent search: the vault it stopped in, the item it examined
|
|
1278
|
+
* last there (exclusive), and the filters it was made for. */
|
|
1279
|
+
type AgentSearchCursor = { f: string; v: string; c: string | null };
|
|
1280
|
+
|
|
1281
|
+
function agentSearchFilterKey(input: SecretSearchInput): string {
|
|
1282
|
+
return createHash("sha256")
|
|
1283
|
+
.update(
|
|
1284
|
+
JSON.stringify([
|
|
1285
|
+
input.query ?? null,
|
|
1286
|
+
input.website ?? null,
|
|
1287
|
+
input.type ?? null,
|
|
1288
|
+
input.vault ?? null,
|
|
1289
|
+
]),
|
|
1290
|
+
)
|
|
1291
|
+
.digest("base64url")
|
|
1292
|
+
.slice(0, 16);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function encodeAgentSearchCursor(cursor: AgentSearchCursor): string {
|
|
1296
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function decodeAgentSearchCursor(raw: string): AgentSearchCursor | null {
|
|
1300
|
+
try {
|
|
1301
|
+
const value = JSON.parse(Buffer.from(raw, "base64url").toString("utf8")) as unknown;
|
|
1302
|
+
if (
|
|
1303
|
+
typeof value === "object" &&
|
|
1304
|
+
value !== null &&
|
|
1305
|
+
typeof (value as AgentSearchCursor).f === "string" &&
|
|
1306
|
+
typeof (value as AgentSearchCursor).v === "string" &&
|
|
1307
|
+
((value as AgentSearchCursor).c === null ||
|
|
1308
|
+
typeof (value as AgentSearchCursor).c === "string")
|
|
1309
|
+
) {
|
|
1310
|
+
return value as AgentSearchCursor;
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
// Not a cursor this service issued.
|
|
1314
|
+
}
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|