@nylorun/runtime 0.16.0-beta → 0.17.0-beta

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +39 -27
  3. package/dist/admin-openapi.json +307 -7
  4. package/dist/api/ag-ui/endpoint.js +1 -2
  5. package/dist/api/ag-ui/routes.d.ts +1 -1
  6. package/dist/api/ag-ui/routes.js +10 -12
  7. package/dist/api/components.d.ts +54 -116
  8. package/dist/api/components.js +8 -11
  9. package/dist/api/http/app.js +2 -0
  10. package/dist/api/http/define.d.ts +9 -12
  11. package/dist/api/http/define.js +11 -26
  12. package/dist/api/http/routes/access.d.ts +3 -19
  13. package/dist/api/http/routes/access.js +12 -172
  14. package/dist/api/http/routes/actions.js +0 -1
  15. package/dist/api/http/routes/artifacts.js +1 -3
  16. package/dist/api/http/routes/me.d.ts +13 -0
  17. package/dist/api/http/routes/me.js +45 -0
  18. package/dist/api/http/routes/sandboxes.d.ts +2 -2
  19. package/dist/api/http/routes/sandboxes.js +1 -3
  20. package/dist/api/http/routes/sessions.js +6 -12
  21. package/dist/api/http/routes/vaults.d.ts +12 -3
  22. package/dist/api/http/routes/vaults.js +122 -54
  23. package/dist/api/openapi.js +4 -10
  24. package/dist/artifacts/links.js +6 -6
  25. package/dist/artifacts/service.js +1 -1
  26. package/dist/gates/mcp-handler.js +5 -1
  27. package/dist/gates/tenant-vaults.d.ts +18 -9
  28. package/dist/gates/tenant-vaults.js +17 -12
  29. package/dist/host/admin-api.d.ts +12 -5
  30. package/dist/host/admin-api.js +72 -6
  31. package/dist/host/app.d.ts +0 -1
  32. package/dist/host/app.js +13 -23
  33. package/dist/host/config-for.d.ts +4 -0
  34. package/dist/host/config-for.js +2 -0
  35. package/dist/host/config.d.ts +0 -6
  36. package/dist/host/create-host.d.ts +3 -10
  37. package/dist/host/create-host.js +4 -1
  38. package/dist/host/gates.js +6 -1
  39. package/dist/host/main.js +32 -10
  40. package/dist/host/stack-config.d.ts +11 -8
  41. package/dist/host/stack-config.js +23 -12
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +2 -0
  44. package/dist/keys/client.js +9 -1
  45. package/dist/keys/keys.d.ts +12 -2
  46. package/dist/keys/keys.js +8 -0
  47. package/dist/mcp/connect.js +2 -1
  48. package/dist/mcp/pool.d.ts +1 -0
  49. package/dist/mcp/pool.js +1 -0
  50. package/dist/openapi.json +2359 -4096
  51. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  52. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  53. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  54. package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
  55. package/dist/store/postgres/schema.d.ts +194 -170
  56. package/dist/store/postgres/schema.js +46 -28
  57. package/dist/store/postgres/store.js +51 -100
  58. package/dist/store/postgres/tenant.d.ts +1 -1
  59. package/dist/store/postgres/tenant.js +2 -2
  60. package/dist/store/types.d.ts +24 -30
  61. package/dist/streams/control.d.ts +0 -5
  62. package/dist/streams/control.js +0 -8
  63. package/dist/streams/types.d.ts +2 -11
  64. package/dist/streams/types.js +1 -1
  65. package/dist/tenant/auth.d.ts +13 -9
  66. package/dist/tenant/auth.js +25 -32
  67. package/dist/tenant/commands.js +3 -7
  68. package/dist/tenant/context.d.ts +20 -22
  69. package/dist/tenant/delivery-token.js +4 -4
  70. package/dist/tenant/effects.d.ts +5 -1
  71. package/dist/tenant/effects.js +5 -16
  72. package/dist/tenant/ephemeral.d.ts +14 -2
  73. package/dist/tenant/ephemeral.js +8 -1
  74. package/dist/tenant/host-token.js +5 -5
  75. package/dist/tenant/identity-file.d.ts +75 -0
  76. package/dist/tenant/identity-file.js +202 -0
  77. package/dist/tenant/issuers.d.ts +48 -0
  78. package/dist/tenant/issuers.js +311 -0
  79. package/dist/tenant/jwt.js +3 -3
  80. package/dist/tenant/operator-keys.d.ts +18 -0
  81. package/dist/tenant/operator-keys.js +55 -0
  82. package/dist/tenant/outbound.d.ts +30 -0
  83. package/dist/tenant/outbound.js +142 -10
  84. package/dist/tenant/principals.d.ts +2 -11
  85. package/dist/tenant/principals.js +10 -30
  86. package/dist/tenant/run-token.js +3 -3
  87. package/dist/tenant/runtime.d.ts +4 -0
  88. package/dist/tenant/runtime.js +14 -1
  89. package/dist/tenant/sandboxes.d.ts +5 -5
  90. package/dist/tenant/sandboxes.js +5 -5
  91. package/dist/tenant/session-streams.d.ts +6 -10
  92. package/dist/tenant/session-streams.js +9 -27
  93. package/dist/tenant/sessions.d.ts +2 -2
  94. package/dist/tenant/sessions.js +1 -1
  95. package/dist/tenant/signing-keys.d.ts +2 -1
  96. package/dist/tenant/signing-keys.js +2 -2
  97. package/dist/tenant/streams.d.ts +2 -3
  98. package/dist/tenant/streams.js +1 -5
  99. package/dist/tenant/types.d.ts +21 -0
  100. package/dist/vault/crypto.d.ts +7 -0
  101. package/dist/vault/crypto.js +16 -0
  102. package/dist/vault/oauth.d.ts +55 -0
  103. package/dist/vault/oauth.js +186 -0
  104. package/dist/vault/service.d.ts +54 -7
  105. package/dist/vault/service.js +261 -17
  106. package/dist/vault/sources.d.ts +54 -0
  107. package/dist/vault/sources.js +185 -0
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/package.json +4 -3
  111. package/dist/host/cors.d.ts +0 -27
  112. package/dist/host/cors.js +0 -64
  113. package/dist/tenant/access-policy.d.ts +0 -30
  114. package/dist/tenant/access-policy.js +0 -52
  115. package/dist/tenant/browser.d.ts +0 -15
  116. package/dist/tenant/browser.js +0 -39
  117. package/dist/tenant/subject-limits.d.ts +0 -16
  118. package/dist/tenant/subject-limits.js +0 -45
  119. package/dist/tenant/tokens.d.ts +0 -14
  120. package/dist/tenant/tokens.js +0 -187
@@ -2,7 +2,7 @@ import { basinOf } from "../streams/basin.js";
2
2
  import { signalActionResolved, signalCancel, signalSessionsReset } from "../streams/control.js";
3
3
  import { createStreamRelay, } from "../streams/relay/core.js";
4
4
  import { CONTROL_STREAM, } from "../streams/types.js";
5
- import { checkSessionStreams, currentBasin, endSubjectStreams, sleep, } from "./session-streams.js";
5
+ import { checkSessionStreams, currentBasin, sleep, } from "./session-streams.js";
6
6
  const RETRY_MIN_MS = 100;
7
7
  const RETRY_MAX_MS = 2000;
8
8
  /** The longest wait between two `ensureTenant` attempts while the basin is being repaired. */
@@ -65,10 +65,6 @@ export async function wireStreams(ctx, options) {
65
65
  void resolveHeld(ctx, signal.sessionId, signal.actionId).catch(report("held run resolution failed"));
66
66
  else if (signal?.type === "sessions.reset")
67
67
  void checkSessionStreams(ctx).catch(report("session stream check failed"));
68
- else if (signal?.type === "subject.revoked" &&
69
- typeof signal.subject === "string" &&
70
- typeof signal.epoch === "number")
71
- endSubjectStreams(ctx.sessionStreams, signal.subject, signal.epoch);
72
68
  }, report("control stream read failed; retrying"));
73
69
  // Signals appended from here on reach this process.
74
70
  await startControl();
@@ -7,7 +7,9 @@ import type { HostAuthority } from "../sandbox/join.js";
7
7
  import type { SessionStore } from "../store/types.js";
8
8
  import type { SandboxBackend } from "../sandbox/types.js";
9
9
  import type { TenantCause } from "./cause.js";
10
+ import type { OperatorKeys } from "./operator-keys.js";
10
11
  import type { OutboundPolicy } from "./outbound.js";
12
+ import type { ResolverConfig } from "../vault/sources.js";
11
13
  import type { TenantWorker } from "./worker.js";
12
14
  export type TenantMode = "shared" | "ephemeral" | "test";
13
15
  /**
@@ -81,6 +83,23 @@ export interface TenantConfig {
81
83
  */
82
84
  flowEnv?: Readonly<Record<string, string | undefined>>;
83
85
  vaultFetch?: typeof fetch;
86
+ /**
87
+ * The trusted issuers whose JWTs the Tenant API accepts (Host feature `trusted-issuers`), from
88
+ * the identity file (`NYLORUN_IDENTITY_FILE`). Absent: none.
89
+ */
90
+ issuers?: import("./issuers.js").TrustedIssuers;
91
+ /**
92
+ * The operator's credential resolver (F9 C1), asked for a person's MCP credential when the
93
+ * session's vaults hold none. Used where this process authorizes MCP calls itself (embedding,
94
+ * the ephemeral Runtime, tests); the gateway takes `NYLORUN_RESOLVER_*` instead.
95
+ */
96
+ resolver?: ResolverConfig;
97
+ /**
98
+ * The URL browsers reach this Tenant API at (`NYLORUN_PUBLIC_URL`, no trailing slash): the
99
+ * base of the MCP OAuth callback, `<publicUrl>/v1/oauth/callback` (F9 C2). Absent (embedding,
100
+ * tests): the origin the start request was sent to.
101
+ */
102
+ publicUrl?: string;
84
103
  logger: Logger;
85
104
  }
86
105
  export interface Logger {
@@ -124,6 +143,8 @@ export interface TenantHandle {
124
143
  attachHarness?(channel: HarnessChannel, peer: HarnessPeer): () => void;
125
144
  /** The Tenant's side of a sandbox pod's join (F7.2); absent without sandbox pods. */
126
145
  hostAuthority?(): HostAuthority | undefined;
146
+ /** The Tenant's operator keys, for the Admin API (`/v1/admin/keys`, F9 I1). */
147
+ operatorKeys?(): OperatorKeys;
127
148
  }
128
149
  /**
129
150
  * The Host's Tenant for a request: open, or unavailable. `cause` says why when opening it
@@ -10,3 +10,10 @@ export declare function encryptSecret(kek: Buffer, aad: Buffer, plaintext: Buffe
10
10
  kekId: string;
11
11
  };
12
12
  export declare function decryptSecret(kek: Buffer, aad: Buffer, nonce: Buffer, ciphertext: Buffer, wrappedDek: Buffer, expectedKekId: string): Buffer;
13
+ /**
14
+ * `encryptSecret` in one column: nonce, wrapped DEK, then ciphertext and tag. For rows that
15
+ * seal several short secrets under one `kekId` (`oauth_pending`, F9 C2).
16
+ */
17
+ export declare function sealBytes(kek: Buffer, aad: Buffer, plaintext: Buffer): Buffer;
18
+ /** Opens what `sealBytes` sealed; `VaultCryptoError` when it cannot. */
19
+ export declare function openBytes(kek: Buffer, aad: Buffer, sealed: Buffer, expectedKekId: string): Buffer;
@@ -78,3 +78,19 @@ function unwrapKey(kek, wrapped) {
78
78
  throw new VaultCryptoError();
79
79
  }
80
80
  }
81
+ /** The wrapped DEK's length: its nonce, the 32-byte key and the tag. */
82
+ const WRAPPED_DEK_BYTES = IV_BYTES + 32 + TAG_BYTES;
83
+ /**
84
+ * `encryptSecret` in one column: nonce, wrapped DEK, then ciphertext and tag. For rows that
85
+ * seal several short secrets under one `kekId` (`oauth_pending`, F9 C2).
86
+ */
87
+ export function sealBytes(kek, aad, plaintext) {
88
+ const sealed = encryptSecret(kek, aad, plaintext);
89
+ return Buffer.concat([sealed.nonce, sealed.wrappedDek, sealed.ciphertext]);
90
+ }
91
+ /** Opens what `sealBytes` sealed; `VaultCryptoError` when it cannot. */
92
+ export function openBytes(kek, aad, sealed, expectedKekId) {
93
+ if (sealed.length < IV_BYTES + WRAPPED_DEK_BYTES + TAG_BYTES)
94
+ throw new VaultCryptoError();
95
+ return decryptSecret(kek, aad, sealed.subarray(0, IV_BYTES), sealed.subarray(IV_BYTES + WRAPPED_DEK_BYTES), sealed.subarray(IV_BYTES, IV_BYTES + WRAPPED_DEK_BYTES), expectedKekId);
96
+ }
@@ -0,0 +1,55 @@
1
+ import type { AuthorizationServerMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
2
+ import { HttpError } from "../tenant/http.js";
3
+ export type TokenEndpointAuth = "none" | "client_secret_basic" | "client_secret_post";
4
+ /** Where and how to sign in to an MCP server. */
5
+ export interface OAuthServer {
6
+ readonly authorizationServerUrl: string;
7
+ readonly metadata: AuthorizationServerMetadata;
8
+ /** The protected resource's own `resource` (RFC 9728), sent as the RFC 8707 indicator. */
9
+ readonly resource?: string;
10
+ /** The scopes the protected resource names, space separated. */
11
+ readonly scope?: string;
12
+ }
13
+ /** The client this installation signs in as. */
14
+ export interface OAuthClient {
15
+ readonly clientId: string;
16
+ readonly clientSecret?: string;
17
+ readonly tokenEndpointAuth: TokenEndpointAuth;
18
+ }
19
+ /** A failed step, as the Tenant API answers it: a refused address is the Host's rule (400). */
20
+ export declare function oauthFailure(step: string, error: unknown): HttpError;
21
+ /**
22
+ * RFC 9728 protected resource metadata, then RFC 8414 (or OpenID) metadata of its first
23
+ * authorization server. A server without the former is its own authorization server (the MCP
24
+ * spec's fallback); a refused address is never a fallback.
25
+ */
26
+ export declare function discoverOAuthServer(url: string, fetchFn: typeof fetch): Promise<OAuthServer>;
27
+ /**
28
+ * The client to sign in as: the operator's `clientId` (a public client) when given, else one
29
+ * registered now (RFC 7591) when the server offers registration, else `oauth_client_required`.
30
+ */
31
+ export declare function oauthClient(server: OAuthServer, options: {
32
+ clientId?: string;
33
+ redirectUri: string;
34
+ fetchFn: typeof fetch;
35
+ }): Promise<OAuthClient>;
36
+ /** The authorization URL, with a fresh S256 PKCE verifier. */
37
+ export declare function authorizationUrl(server: OAuthServer, client: OAuthClient, options: {
38
+ redirectUri: string;
39
+ state: string;
40
+ }): Promise<{
41
+ authorizeUrl: string;
42
+ codeVerifier: string;
43
+ }>;
44
+ /** Adds the client's authentication to a token request, as `tokenEndpointAuth` says. */
45
+ export declare function addClientAuthentication(client: OAuthClient, headers: Headers, params: URLSearchParams): void;
46
+ /** Exchanges the authorization code (with its PKCE verifier) for tokens. */
47
+ export declare function exchangeCode(options: {
48
+ tokenEndpoint: string;
49
+ client: OAuthClient;
50
+ code: string;
51
+ codeVerifier: string;
52
+ redirectUri: string;
53
+ resource?: string;
54
+ fetchFn: typeof fetch;
55
+ }): Promise<OAuthTokens>;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The network half of an MCP OAuth connect (F9 C2): discovery, dynamic client registration,
3
+ * the authorization URL and the code exchange, with the MCP SDK's auth helpers over the fetch
4
+ * it is given (the Host's `guardedFetch` in the gateway). `VaultService.startOAuth` and
5
+ * `finishOAuth` call these outside any transaction and hold the secrets; nothing here stores
6
+ * or logs one.
7
+ */
8
+ import { discoverAuthorizationServerMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, registerClient, startAuthorization, } from "@modelcontextprotocol/sdk/client/auth.js";
9
+ import { OAuthError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
10
+ import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
11
+ import { HttpError } from "../tenant/http.js";
12
+ import { OutboundFailed, OutboundRefused } from "../tenant/outbound.js";
13
+ /** A failed step, as the Tenant API answers it: a refused address is the Host's rule (400). */
14
+ export function oauthFailure(step, error) {
15
+ if (error instanceof HttpError)
16
+ return error;
17
+ if (error instanceof OutboundRefused)
18
+ return new HttpError(400, `${error.message} (OAuth ${step})`, { code: "request_rejected" });
19
+ if (error instanceof OAuthError)
20
+ return new HttpError(502, `The authorization server refused the ${step} (${error.errorCode})`, {
21
+ code: "oauth_failed",
22
+ });
23
+ if (error instanceof OutboundFailed)
24
+ return new HttpError(502, `The OAuth ${step} failed: ${error.message}`, { code: "oauth_failed" });
25
+ return new HttpError(502, `The OAuth ${step} failed: the authorization server's answer was not usable`, {
26
+ code: "oauth_failed",
27
+ });
28
+ }
29
+ function httpUrl(value, what) {
30
+ let url;
31
+ try {
32
+ url = new URL(value);
33
+ }
34
+ catch {
35
+ throw new HttpError(502, `The authorization server's ${what} is not a URL`, { code: "oauth_failed" });
36
+ }
37
+ if (url.protocol !== "https:" && url.protocol !== "http:")
38
+ throw new HttpError(502, `The authorization server's ${what} is not an http(s) URL`, {
39
+ code: "oauth_failed",
40
+ });
41
+ return url;
42
+ }
43
+ /**
44
+ * RFC 9728 protected resource metadata, then RFC 8414 (or OpenID) metadata of its first
45
+ * authorization server. A server without the former is its own authorization server (the MCP
46
+ * spec's fallback); a refused address is never a fallback.
47
+ */
48
+ export async function discoverOAuthServer(url, fetchFn) {
49
+ let authorizationServerUrl;
50
+ let resource;
51
+ let scope;
52
+ try {
53
+ const protectedResource = await discoverOAuthProtectedResourceMetadata(url, {}, fetchFn);
54
+ authorizationServerUrl = protectedResource.authorization_servers?.[0];
55
+ if (protectedResource.resource) {
56
+ if (!checkResourceAllowed({ requestedResource: url, configuredResource: protectedResource.resource }))
57
+ throw new HttpError(502, `The MCP server's protected resource metadata names another resource (${protectedResource.resource})`, { code: "oauth_failed" });
58
+ resource = protectedResource.resource;
59
+ }
60
+ if (protectedResource.scopes_supported?.length)
61
+ scope = protectedResource.scopes_supported.join(" ");
62
+ }
63
+ catch (error) {
64
+ if (error instanceof OutboundRefused || error instanceof HttpError)
65
+ throw oauthFailure("discovery", error);
66
+ // No RFC 9728 metadata: the MCP server is its own authorization server.
67
+ }
68
+ authorizationServerUrl ??= new URL("/", url).href;
69
+ httpUrl(authorizationServerUrl, "address");
70
+ let metadata;
71
+ try {
72
+ metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn });
73
+ }
74
+ catch (error) {
75
+ throw oauthFailure("discovery", error);
76
+ }
77
+ if (!metadata)
78
+ throw new HttpError(502, `No OAuth authorization server metadata at ${new URL(authorizationServerUrl).origin}: the MCP server does not support OAuth sign-in`, { code: "oauth_failed" });
79
+ httpUrl(metadata.authorization_endpoint, "authorization endpoint");
80
+ httpUrl(metadata.token_endpoint, "token endpoint");
81
+ return {
82
+ authorizationServerUrl,
83
+ metadata,
84
+ ...(resource === undefined ? {} : { resource }),
85
+ ...(scope === undefined ? {} : { scope }),
86
+ };
87
+ }
88
+ function authMethodOf(value, secret) {
89
+ if (value === undefined)
90
+ return secret ? "client_secret_basic" : "none";
91
+ if (value === "none" || value === "client_secret_basic" || value === "client_secret_post") {
92
+ if (value !== "none" && !secret)
93
+ throw new HttpError(502, "The authorization server registered a confidential client without a secret", {
94
+ code: "oauth_failed",
95
+ });
96
+ return value;
97
+ }
98
+ throw new HttpError(502, `The authorization server's client authentication ${String(value)} is not supported`, {
99
+ code: "oauth_failed",
100
+ });
101
+ }
102
+ /**
103
+ * The client to sign in as: the operator's `clientId` (a public client) when given, else one
104
+ * registered now (RFC 7591) when the server offers registration, else `oauth_client_required`.
105
+ */
106
+ export async function oauthClient(server, options) {
107
+ if (options.clientId)
108
+ return { clientId: options.clientId, tokenEndpointAuth: "none" };
109
+ if (!server.metadata.registration_endpoint)
110
+ throw new HttpError(400, "The authorization server offers no dynamic client registration: register a client with it and pass its id (--client-id)", { code: "oauth_client_required" });
111
+ httpUrl(server.metadata.registration_endpoint, "registration endpoint");
112
+ try {
113
+ const registered = await registerClient(server.authorizationServerUrl, {
114
+ metadata: server.metadata,
115
+ clientMetadata: {
116
+ client_name: "Nylorun",
117
+ redirect_uris: [options.redirectUri],
118
+ grant_types: ["authorization_code", "refresh_token"],
119
+ response_types: ["code"],
120
+ token_endpoint_auth_method: "none",
121
+ },
122
+ ...(server.scope === undefined ? {} : { scope: server.scope }),
123
+ fetchFn: options.fetchFn,
124
+ });
125
+ return {
126
+ clientId: registered.client_id,
127
+ ...(registered.client_secret ? { clientSecret: registered.client_secret } : {}),
128
+ tokenEndpointAuth: authMethodOf(registered.token_endpoint_auth_method, registered.client_secret),
129
+ };
130
+ }
131
+ catch (error) {
132
+ throw oauthFailure("client registration", error);
133
+ }
134
+ }
135
+ /** The authorization URL, with a fresh S256 PKCE verifier. */
136
+ export async function authorizationUrl(server, client, options) {
137
+ try {
138
+ const started = await startAuthorization(server.authorizationServerUrl, {
139
+ metadata: server.metadata,
140
+ clientInformation: { client_id: client.clientId },
141
+ redirectUrl: options.redirectUri,
142
+ state: options.state,
143
+ ...(server.scope === undefined ? {} : { scope: server.scope }),
144
+ ...(server.resource === undefined ? {} : { resource: server.resource }),
145
+ });
146
+ return { authorizeUrl: started.authorizationUrl.href, codeVerifier: started.codeVerifier };
147
+ }
148
+ catch (error) {
149
+ throw oauthFailure("authorization request", error);
150
+ }
151
+ }
152
+ /** Adds the client's authentication to a token request, as `tokenEndpointAuth` says. */
153
+ export function addClientAuthentication(client, headers, params) {
154
+ if (client.tokenEndpointAuth === "client_secret_basic") {
155
+ const encoded = Buffer.from(`${encodeURIComponent(client.clientId)}:${encodeURIComponent(client.clientSecret ?? "")}`).toString("base64");
156
+ headers.set("authorization", `Basic ${encoded}`);
157
+ return;
158
+ }
159
+ params.set("client_id", client.clientId);
160
+ if (client.tokenEndpointAuth === "client_secret_post")
161
+ params.set("client_secret", client.clientSecret ?? "");
162
+ }
163
+ /** Exchanges the authorization code (with its PKCE verifier) for tokens. */
164
+ export async function exchangeCode(options) {
165
+ const { client } = options;
166
+ const clientInformation = {
167
+ client_id: client.clientId,
168
+ ...(client.clientSecret ? { client_secret: client.clientSecret } : {}),
169
+ };
170
+ try {
171
+ return await exchangeAuthorization(options.tokenEndpoint, {
172
+ // Only the token endpoint is read from it.
173
+ metadata: { token_endpoint: options.tokenEndpoint },
174
+ clientInformation,
175
+ authorizationCode: options.code,
176
+ codeVerifier: options.codeVerifier,
177
+ redirectUri: options.redirectUri,
178
+ ...(options.resource === undefined ? {} : { resource: options.resource }),
179
+ addClientAuthentication: (headers, params) => addClientAuthentication(client, headers, params),
180
+ fetchFn: options.fetchFn,
181
+ });
182
+ }
183
+ catch (error) {
184
+ throw oauthFailure("code exchange", error);
185
+ }
186
+ }
@@ -1,5 +1,7 @@
1
1
  import type { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, CredentialSelection, HostModelProviderInfo, HostModelView, PutHostModelRequest, CustomModelSettings, RotateCredentialRequest, SelectHostModelRequest, VaultInfo } from "@nylorun/core/contracts";
2
2
  import type { SessionStore, Tx, VaultCredentialRow } from "../store/types.js";
3
+ /** How long an MCP OAuth connect may take between its start and its callback (F9 C2). */
4
+ export declare const OAUTH_PENDING_MS: number;
3
5
  export declare const HOST_VAULT_ID = "host";
4
6
  export type HostModelSecret = {
5
7
  provider: string;
@@ -25,20 +27,39 @@ export type AuthorizeResult = {
25
27
  } | {
26
28
  status: "authorized";
27
29
  url: string;
28
- headers: {
29
- authorization: string;
30
- };
30
+ /** A vault credential's `authorization`, or the headers the credential resolver gave. */
31
+ headers: Record<string, string>;
31
32
  } | {
32
33
  status: "refused";
33
34
  url: string;
34
35
  credentialIds: string[];
35
36
  reason: string;
36
37
  };
38
+ /** `startOAuth`'s input: the route's body, the installation vault and the callback URL. */
39
+ export interface StartOAuthInput {
40
+ vaultId: string;
41
+ /** The MCP server's name, the credential's name. */
42
+ server: string;
43
+ url: string;
44
+ clientId?: string;
45
+ /** `<public URL>/v1/oauth/callback`. */
46
+ redirectUri: string;
47
+ }
48
+ /** The callback's query: `code`, or the authorization server's `error`. */
49
+ export interface FinishOAuthInput {
50
+ state: string;
51
+ code?: string;
52
+ error?: string;
53
+ }
37
54
  export interface VaultServiceOptions {
38
55
  store: SessionStore;
39
56
  /** The Tenant key-encryption key. May read or create the key file. */
40
57
  kek: () => Buffer;
41
- /** Used only for OAuth refresh, always outside a transaction. */
58
+ /**
59
+ * OAuth refresh and MCP OAuth connect (discovery, registration, the code exchange), always
60
+ * outside a transaction: the Host's `guardedFetch` (`tenant/outbound.ts`) unless a test or
61
+ * an embedder injects one.
62
+ */
42
63
  fetch: typeof fetch;
43
64
  /** How long a token endpoint may take to answer a refresh. Default 30 s. */
44
65
  refreshTimeoutMs?: number;
@@ -52,10 +73,14 @@ export declare class VaultService {
52
73
  private readonly refreshing;
53
74
  constructor(options: VaultServiceOptions);
54
75
  createVault(body: CreateVaultRequest): Promise<VaultInfo>;
55
- listVaults(ownerUserId: string): Promise<VaultInfo[]>;
76
+ /**
77
+ * One person's vaults, then (with `installation`) the installation vaults. No owner lists
78
+ * the installation vaults only.
79
+ */
80
+ listVaults(ownerUserId: string | undefined, options?: {
81
+ installation?: boolean;
82
+ }): Promise<VaultInfo[]>;
56
83
  getVault(id: string): Promise<VaultInfo>;
57
- /** Another owner's vault, or the host vault, is the same 404 as a missing one. */
58
- assertOwner(id: string, ownerUserId: string): Promise<void>;
59
84
  deleteVault(id: string): Promise<{
60
85
  id: string;
61
86
  }>;
@@ -82,6 +107,28 @@ export declare class VaultService {
82
107
  url: string;
83
108
  serverName?: string;
84
109
  }): Promise<AuthorizeResult>;
110
+ /**
111
+ * Starts signing the installation in to the MCP server at `url`: discovery, a client (the
112
+ * given id, or one registered now), an S256 PKCE verifier and a `state`, kept for ten minutes
113
+ * in a pending row (verifier and secret sealed, state hashed). Returns where to send the
114
+ * browser. Installation vaults only.
115
+ */
116
+ startOAuth(input: StartOAuthInput): Promise<{
117
+ authorizeUrl: string;
118
+ expiresAt: string;
119
+ }>;
120
+ /**
121
+ * Finishes a connect from its callback: takes the pending row of `state` (deleted whatever
122
+ * happens next), exchanges the code, and seals an `oauth` credential bound to the server's
123
+ * URL in the installation vault, rotating the vault's OAuth credential for that URL when it
124
+ * has one.
125
+ */
126
+ finishOAuth(input: FinishOAuthInput): Promise<{
127
+ vaultId: string;
128
+ credentialId: string;
129
+ }>;
130
+ /** An installation vault, else the 404 of a missing one (a user vault is a 400). */
131
+ private installationVault;
85
132
  getHostModel(): Promise<HostModelView>;
86
133
  listHostProviders(): Promise<{
87
134
  providers: HostModelProviderInfo[];