@opengeni/api-router 3.1.3-canary.38890816741001 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type { ExternalVaultProviderFactories } from "@opengeni/core";
3
+ import { type PinnedFetchOptions } from "@opengeni/network";
4
+ import type { KeeperRuntimeOptions } from "@opengeni/vaults-keeper";
5
+ /** Test seams only; production passes nothing. */
6
+ export type NativeExternalVaultOptions = {
7
+ keeper?: Pick<KeeperRuntimeOptions, "queryFunction" | "fetchFile" | "sdk">;
8
+ /** Applies when this call creates the pool for its CLI path. */
9
+ protonpass?: {
10
+ stateRoot?: string;
11
+ hostEnv?: Readonly<Record<string, string | undefined>>;
12
+ };
13
+ };
14
+ /**
15
+ * Native external vault adapters that run inside this server: Keeper Secrets
16
+ * Manager always, Proton Pass only when the operator installed `pass-cli`
17
+ * (`OPENGENI_PROTON_PASS_CLI_PATH`). Both packages load through `import()`
18
+ * on first use (packages/vaults-keeper/test/import-guard.test.ts). Keep this
19
+ * file identical to apps/worker/src/external-vault-providers.ts.
20
+ */
21
+ export declare function createNativeExternalVaultFactories(settings: Settings, options?: NativeExternalVaultOptions): ExternalVaultProviderFactories;
22
+ /** Logs out every pooled Proton Pass CLI session and wipes its state (graceful shutdown). */
23
+ export declare function closeNativeExternalVaultProviders(): Promise<void>;
24
+ /**
25
+ * The fetch every Keeper request and file download uses: DNS-pinned and
26
+ * policy-checked (no private or special-use targets). The adapter adds
27
+ * HTTPS-only, no redirects, deadlines, size bounds and the caller's abort.
28
+ */
29
+ export declare function nativeKeeperFetch(settings: Settings, pinned?: Pick<PinnedFetchOptions, "dnsLookup" | "fetchImpl" | "agentFactory">): (input: string | URL, init: RequestInit) => Promise<Response>;
@@ -7,7 +7,7 @@ import type { VaultProviderRegistry } from "@opengeni/vaults";
7
7
  import { type BrowserPasskeyCredentialMaterial } from "@opengeni/contracts";
8
8
  import { type VaultService } from "@opengeni/core";
9
9
  import { type Database } from "@opengeni/db";
10
- import type { InteractionSecretsPort } from "./interaction-secrets.js";
10
+ import type { AttemptSecretPrincipal, InteractionSecretsPort } from "./interaction-secrets.js";
11
11
  export declare function passkeyItemName(credential: BrowserPasskeyCredentialMaterial): string;
12
12
  export declare function createVaultInteractionSecretsPort(deps: {
13
13
  db: Database;
@@ -16,5 +16,5 @@ export declare function createVaultInteractionSecretsPort(deps: {
16
16
  /** Defaults to a service over the same database and key. */
17
17
  vaultService?: VaultService;
18
18
  /** Connected vaults (1Password, host) for a workspace's acting subject. */
19
- providersFor?: (workspaceId: string, subjectId: string | null) => VaultProviderRegistry;
19
+ providersFor?: (workspaceId: string, subjectId: string | null, principal?: AttemptSecretPrincipal) => VaultProviderRegistry;
20
20
  }): InteractionSecretsPort;
@@ -1,4 +1,4 @@
1
1
  import type { ActionCatalogEntry } from "./action-catalog-types.js";
2
- /** Actions that must carry a description (subscription and model settings). */
2
+ /** Actions that must carry a description (subscriptions, models and vault connections). */
3
3
  export declare const DESCRIBED_ACTION_PATH: RegExp;
4
4
  export declare function actionDescription(entry: Pick<ActionCatalogEntry, "method" | "path">): string | undefined;
@@ -1,7 +1,7 @@
1
1
  import type { Hono } from "hono";
2
- import { type ApiRouteDeps, type OnePasswordConnectionMetadata, type OnePasswordVaultProviderFactory } from "@opengeni/core";
2
+ import { type ApiRouteDeps, type OnePasswordConnectionMetadata, type OnePasswordVaultProviderFactory, type ExternalVaultProviderFactories } from "@opengeni/core";
3
3
  import { type VaultDbContext } from "@opengeni/db";
4
- import { type VaultProvider, type VaultProviderRegistry } from "@opengeni/vaults";
4
+ import { type VaultProvider, type VaultProviderRegistry, type VaultProviderAuthority } from "@opengeni/vaults";
5
5
  export declare function onePasswordFactoryFor(deps: ApiRouteDeps): OnePasswordVaultProviderFactory;
6
6
  /**
7
7
  * The 1Password provider for a stored connection the caller can see, plus its
@@ -23,7 +23,9 @@ export declare function onePasswordProviderForConnection(deps: ApiRouteDeps, inp
23
23
  export declare function vaultProviderRegistryFor(deps: ApiRouteDeps, input: {
24
24
  workspaceId: string;
25
25
  subjectId: string | null;
26
+ executionContext?: Pick<VaultProviderAuthority, "sessionId" | "turnId" | "attemptId" | "siteId">;
26
27
  }): VaultProviderRegistry;
28
+ export declare function externalVaultFactoriesFor(deps: ApiRouteDeps): ExternalVaultProviderFactories;
27
29
  export type LinkedVaultLookup = (input: {
28
30
  workspaceId: string;
29
31
  connectionId: string;
@@ -0,0 +1,14 @@
1
+ type ShutdownSignal = "SIGINT" | "SIGTERM";
2
+ type ShutdownProcess = {
3
+ on(signal: ShutdownSignal, handler: () => void): unknown;
4
+ off(signal: ShutdownSignal, handler: () => void): unknown;
5
+ exit(code: number): void;
6
+ };
7
+ /** Standalone process boundary; embedded hosts keep owning their explicit close(). */
8
+ export declare function installApiShutdownHandlers(input: {
9
+ close: () => Promise<void>;
10
+ process?: ShutdownProcess;
11
+ timeoutMs?: number;
12
+ onFailure?: () => void;
13
+ }): () => void;
14
+ export {};
@@ -23,5 +23,5 @@ export declare function workspaceGatewaySecretArguments(input: {
23
23
  settings: Settings;
24
24
  grant: AccessGrant;
25
25
  /** Connected vaults (1Password, host) for the resolved acting subject. */
26
- providersForSubject?: ((subjectId: string | null) => VaultProviderRegistry) | undefined;
26
+ providersForSubject?: ((subjectId: string | null, site?: SiteSecretContext | null) => VaultProviderRegistry) | undefined;
27
27
  }): ToolGatewayArgumentResolver;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "3.1.3-canary.38890816741001",
3
+ "version": "3.2.2",
4
4
  "description": "Opengeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -42,29 +42,32 @@
42
42
  "@aws-sdk/client-s3": "^3.1044.0",
43
43
  "@aws-sdk/s3-request-presigner": "^3.1044.0",
44
44
  "@hono/zod-validator": "^0.7.6",
45
+ "@keeper-security/secrets-manager-core": "17.6.0",
45
46
  "@llamaindex/liteparse": "2.14.2",
46
47
  "@modelcontextprotocol/sdk": "^1.29.0",
47
- "@opengeni/agent-proto": "3.1.3-canary.38890816741001",
48
- "@opengeni/artifact-tool": "3.1.3-canary.38890816741001",
49
- "@opengeni/capabilities": "3.1.3-canary.38890816741001",
50
- "@opengeni/codemode": "3.1.3-canary.38890816741001",
51
- "@opengeni/codex": "3.1.3-canary.38890816741001",
52
- "@opengeni/config": "3.1.3-canary.38890816741001",
53
- "@opengeni/contracts": "3.1.3-canary.38890816741001",
54
- "@opengeni/core": "3.1.3-canary.38890816741001",
55
- "@opengeni/db": "3.1.3-canary.38890816741001",
56
- "@opengeni/documents": "3.1.3-canary.38890816741001",
57
- "@opengeni/events": "3.1.3-canary.38890816741001",
58
- "@opengeni/github": "3.1.3-canary.38890816741001",
59
- "@opengeni/interaction": "3.1.3-canary.38890816741001",
60
- "@opengeni/network": "3.1.3-canary.38890816741001",
61
- "@opengeni/observability": "3.1.3-canary.38890816741001",
62
- "@opengeni/runtime": "3.1.3-canary.38890816741001",
63
- "@opengeni/storage": "3.1.3-canary.38890816741001",
64
- "@opengeni/tool-gateway": "3.1.3-canary.38890816741001",
65
- "@opengeni/vaults": "3.1.3-canary.38890816741001",
66
- "@opengeni/vaults-onepassword": "3.1.3-canary.38890816741001",
67
- "@opengeni/xai-subscription": "3.1.3-canary.38890816741001",
48
+ "@opengeni/agent-proto": "3.2.2",
49
+ "@opengeni/artifact-tool": "3.2.2",
50
+ "@opengeni/capabilities": "3.2.2",
51
+ "@opengeni/codemode": "3.2.2",
52
+ "@opengeni/codex": "3.2.2",
53
+ "@opengeni/config": "3.2.2",
54
+ "@opengeni/contracts": "3.2.2",
55
+ "@opengeni/core": "3.2.2",
56
+ "@opengeni/db": "3.2.2",
57
+ "@opengeni/documents": "3.2.2",
58
+ "@opengeni/events": "3.2.2",
59
+ "@opengeni/github": "3.2.2",
60
+ "@opengeni/interaction": "3.2.2",
61
+ "@opengeni/network": "3.2.2",
62
+ "@opengeni/observability": "3.2.2",
63
+ "@opengeni/runtime": "3.2.2",
64
+ "@opengeni/storage": "3.2.2",
65
+ "@opengeni/tool-gateway": "3.2.2",
66
+ "@opengeni/vaults": "3.2.2",
67
+ "@opengeni/vaults-keeper": "3.2.2",
68
+ "@opengeni/vaults-onepassword": "3.2.2",
69
+ "@opengeni/vaults-protonpass": "3.2.2",
70
+ "@opengeni/xai-subscription": "3.2.2",
68
71
  "@temporalio/client": "^1.17.0",
69
72
  "ajv": "^8.20.0",
70
73
  "better-auth": "^1.6.14",
package/src/app.ts CHANGED
@@ -2200,8 +2200,20 @@ export function createAppComposition(deps: AppDependencies): {
2200
2200
  const interactionSecrets = createVaultInteractionSecretsPort({
2201
2201
  db: routeDeps.db,
2202
2202
  encryptionKey: environmentsEncryptionKeyBytes(routeDeps.settings) ?? null,
2203
- providersFor: (workspaceId, subjectId) =>
2204
- vaultProviderRegistryFor(routeDeps, { workspaceId, subjectId }),
2203
+ providersFor: (workspaceId, subjectId, principal) =>
2204
+ vaultProviderRegistryFor(routeDeps, {
2205
+ workspaceId,
2206
+ subjectId,
2207
+ ...(principal
2208
+ ? {
2209
+ executionContext: {
2210
+ sessionId: principal.sessionId,
2211
+ turnId: principal.turnId,
2212
+ attemptId: principal.attemptId,
2213
+ },
2214
+ }
2215
+ : {}),
2216
+ }),
2205
2217
  });
2206
2218
  registerBrowserSessionRoutes(app, routeDeps, { interactionSecrets });
2207
2219
  registerComputerSessionRoutes(app, routeDeps, { interactionSecrets });
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ import { createAutomaticAgentUpdater } from "./sandbox/agent-update";
52
52
  import { startSlackInteractionPump } from "./integrations/slack-interactions";
53
53
  import { startMemorySlackPublicationPump } from "./memory-slack-delivery";
54
54
  import { startTemporalScheduleCleanupPump } from "./temporal-schedule-cleanup";
55
+ import { closeNativeExternalVaultProviders } from "./integrations/external-vault-providers";
55
56
  import { startScheduledTaskLifecyclePump } from "./scheduled-task-lifecycle";
56
57
  import { createTemporalScheduleSynchronizer } from "./temporal-schedule-sync";
57
58
  import { startNativePushDispatchPump } from "./native-push-dispatch";
@@ -73,6 +74,7 @@ import {
73
74
  type StandaloneEditableArtifactApplication,
74
75
  } from "./editable-artifact-production";
75
76
  import { installApiFatalProcessBoundary } from "./fatal-process-boundary";
77
+ import { installApiShutdownHandlers } from "./shutdown";
76
78
 
77
79
  /**
78
80
  * A REJECT_DUPLICATE start collides on the deterministic workflowId when the
@@ -590,6 +592,8 @@ export async function startApi(
590
592
  server,
591
593
  close: async () => {
592
594
  server.stop(true);
595
+ // Start private CLI session cleanup even if another service's drain stalls.
596
+ const vaultProvidersClosed = closeNativeExternalVaultProviders().catch(() => undefined);
593
597
  metricsServer?.stop(true);
594
598
  stopSlackInteractionPump?.();
595
599
  await stopMemorySlackPublicationPump();
@@ -602,6 +606,7 @@ export async function startApi(
602
606
  // Write queued presence before the database pool closes.
603
607
  await routeDeps.userPresence?.close().catch(() => undefined);
604
608
  await Promise.allSettled([
609
+ vaultProvidersClosed,
605
610
  Promise.resolve(editableArtifactComposition?.close()),
606
611
  authCalloutResponder?.close(),
607
612
  bus.close(),
@@ -618,7 +623,14 @@ if (import.meta.main) {
618
623
  const settings = getSettings();
619
624
  const observability = createObservability(settings, { component: "api" });
620
625
  fatalBoundary.attachObservability(observability);
621
- await startApi({ settings, observability });
626
+ const api = await startApi({ settings, observability });
627
+ installApiShutdownHandlers({
628
+ close: api.close,
629
+ onFailure: () =>
630
+ observability.warn("Opengeni API shutdown did not finish cleanly", {
631
+ errorCode: "api_shutdown_failed",
632
+ }),
633
+ });
622
634
  fatalBoundary.markRunning();
623
635
  } catch (error) {
624
636
  await fatalBoundary.reportStartupFailure(error);
@@ -0,0 +1,136 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type {
3
+ ExternalVaultProviderFactories,
4
+ ExternalVaultProviderFactory,
5
+ StoredVaultProviderCredentials,
6
+ } from "@opengeni/core";
7
+ import { pinnedFetch, type PinnedFetchOptions } from "@opengeni/network";
8
+ import { VaultProviderError } from "@opengeni/vaults";
9
+ import type { KeeperRuntimeOptions } from "@opengeni/vaults-keeper";
10
+ import type { ProtonPassProviderPool } from "@opengeni/vaults-protonpass";
11
+
12
+ /** Test seams only; production passes nothing. */
13
+ export type NativeExternalVaultOptions = {
14
+ keeper?: Pick<KeeperRuntimeOptions, "queryFunction" | "fetchFile" | "sdk">;
15
+ /** Applies when this call creates the pool for its CLI path. */
16
+ protonpass?: { stateRoot?: string; hostEnv?: Readonly<Record<string, string | undefined>> };
17
+ };
18
+
19
+ /**
20
+ * Native external vault adapters that run inside this server: Keeper Secrets
21
+ * Manager always, Proton Pass only when the operator installed `pass-cli`
22
+ * (`OPENGENI_PROTON_PASS_CLI_PATH`). Both packages load through `import()`
23
+ * on first use (packages/vaults-keeper/test/import-guard.test.ts). Keep this
24
+ * file identical to apps/worker/src/external-vault-providers.ts.
25
+ */
26
+ export function createNativeExternalVaultFactories(
27
+ settings: Settings,
28
+ options: NativeExternalVaultOptions = {},
29
+ ): ExternalVaultProviderFactories {
30
+ const factories: ExternalVaultProviderFactories = {
31
+ keeper: keeperFactory(settings, options.keeper ?? {}),
32
+ };
33
+ const cliPath = settings.protonPassCliPath;
34
+ if (cliPath) factories.protonpass = protonPassFactory(cliPath, options.protonpass ?? {});
35
+ return factories;
36
+ }
37
+
38
+ /** Logs out every pooled Proton Pass CLI session and wipes its state (graceful shutdown). */
39
+ export async function closeNativeExternalVaultProviders(): Promise<void> {
40
+ const pools = [...protonPools.values()];
41
+ protonPools.clear();
42
+ await Promise.all(pools.map(async (pool) => (await pool.catch(() => null))?.close()));
43
+ }
44
+
45
+ function mismatch(): never {
46
+ throw new VaultProviderError(
47
+ "provider_unavailable",
48
+ "The configured adapter does not match this connection.",
49
+ );
50
+ }
51
+
52
+ /**
53
+ * The fetch every Keeper request and file download uses: DNS-pinned and
54
+ * policy-checked (no private or special-use targets). The adapter adds
55
+ * HTTPS-only, no redirects, deadlines, size bounds and the caller's abort.
56
+ */
57
+ export function nativeKeeperFetch(
58
+ settings: Settings,
59
+ pinned: Pick<PinnedFetchOptions, "dnsLookup" | "fetchImpl" | "agentFactory"> = {},
60
+ ) {
61
+ return (input: string | URL, init: RequestInit) =>
62
+ pinnedFetch(input, init, settings, {
63
+ ...pinned,
64
+ label: "Keeper Secrets Manager",
65
+ requireHttpsOutsideLocalTest: true,
66
+ });
67
+ }
68
+
69
+ function keeperFactory(
70
+ settings: Settings,
71
+ seams: NonNullable<NativeExternalVaultOptions["keeper"]>,
72
+ ): ExternalVaultProviderFactory {
73
+ const fetch = nativeKeeperFetch(settings);
74
+ return async (input) => {
75
+ const credentials = input.credentials;
76
+ if (credentials.provider !== "keeper") mismatch();
77
+ const persist = input.persistCredentials;
78
+ const { openKeeperConnection } = await import("@opengeni/vaults-keeper");
79
+ const opened = await openKeeperConnection({
80
+ credentials:
81
+ "oneTimeToken" in credentials
82
+ ? {
83
+ oneTimeToken: credentials.oneTimeToken,
84
+ ...(credentials.hostname ? { hostname: credentials.hostname } : {}),
85
+ }
86
+ : { configuration: credentials.configuration },
87
+ // Without it openKeeperConnection refuses: a redeemed token or rotated
88
+ // key must never live only in memory.
89
+ ...(persist
90
+ ? {
91
+ persistConfiguration: (configuration: Record<string, string>) =>
92
+ persist({ provider: "keeper", configuration }),
93
+ }
94
+ : {}),
95
+ ...(input.operations ? { operations: input.operations } : {}),
96
+ // Cancels a pending bootstrap; a stored configuration opens without a request.
97
+ ...(input.signal ? { signal: input.signal } : {}),
98
+ fetch,
99
+ ...seams,
100
+ });
101
+ const stored: StoredVaultProviderCredentials = {
102
+ provider: "keeper",
103
+ configuration: opened.configuration,
104
+ };
105
+ return { provider: opened.provider, credentials: stored };
106
+ };
107
+ }
108
+
109
+ /** One pool per CLI path per process, so repeated factory creation never leaks sessions. */
110
+ const protonPools = new Map<string, Promise<ProtonPassProviderPool>>();
111
+
112
+ function protonPassFactory(
113
+ binaryPath: string,
114
+ seams: NonNullable<NativeExternalVaultOptions["protonpass"]>,
115
+ ): ExternalVaultProviderFactory {
116
+ return async (input) => {
117
+ const credentials = input.credentials;
118
+ if (credentials.provider !== "protonpass") mismatch();
119
+ let pool = protonPools.get(binaryPath);
120
+ if (!pool) {
121
+ pool = import("@opengeni/vaults-protonpass").then(({ createProtonPassProviderPool }) =>
122
+ createProtonPassProviderPool({ ...seams, binaryPath }),
123
+ );
124
+ protonPools.set(binaryPath, pool);
125
+ pool.catch(() => protonPools.delete(binaryPath));
126
+ }
127
+ // Sessions are scoped to the connection, so two connections never share
128
+ // CLI state even with the same token. Without a connection id (a setup
129
+ // probe) the provider gets an isolated session of its own.
130
+ const provider = (await pool).provider(credentials.token, {
131
+ ...(input.connectionId === undefined ? {} : { connectionId: input.connectionId }),
132
+ ...(input.operations ? { operations: input.operations } : {}),
133
+ });
134
+ return { provider, credentials: { provider: "protonpass", token: credentials.token } };
135
+ };
136
+ }
@@ -59,7 +59,11 @@ export function createVaultInteractionSecretsPort(deps: {
59
59
  /** Defaults to a service over the same database and key. */
60
60
  vaultService?: VaultService;
61
61
  /** Connected vaults (1Password, host) for a workspace's acting subject. */
62
- providersFor?: (workspaceId: string, subjectId: string | null) => VaultProviderRegistry;
62
+ providersFor?: (
63
+ workspaceId: string,
64
+ subjectId: string | null,
65
+ principal?: AttemptSecretPrincipal,
66
+ ) => VaultProviderRegistry;
63
67
  }): InteractionSecretsPort {
64
68
  const encryptionKey = deps.encryptionKey;
65
69
  let service = deps.vaultService ?? null;
@@ -203,7 +207,7 @@ export function createVaultInteractionSecretsPort(deps: {
203
207
  ...(providersFor
204
208
  ? {
205
209
  providersForSubject: (subjectId: string | null) =>
206
- providersFor(principal.workspaceId, subjectId),
210
+ providersFor(principal.workspaceId, subjectId, principal),
207
211
  }
208
212
  : {}),
209
213
  });