@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.
- package/CHANGELOG.md +58 -0
- package/README.md +39 -27
- package/dist/admin-openapi.json +307 -7
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +10 -12
- package/dist/api/components.d.ts +54 -116
- package/dist/api/components.js +8 -11
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/define.d.ts +9 -12
- package/dist/api/http/define.js +11 -26
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +12 -172
- package/dist/api/http/routes/actions.js +0 -1
- package/dist/api/http/routes/artifacts.js +1 -3
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +45 -0
- package/dist/api/http/routes/sandboxes.d.ts +2 -2
- package/dist/api/http/routes/sandboxes.js +1 -3
- package/dist/api/http/routes/sessions.js +6 -12
- package/dist/api/http/routes/vaults.d.ts +12 -3
- package/dist/api/http/routes/vaults.js +122 -54
- package/dist/api/openapi.js +4 -10
- package/dist/artifacts/links.js +6 -6
- package/dist/artifacts/service.js +1 -1
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/host/admin-api.d.ts +12 -5
- package/dist/host/admin-api.js +72 -6
- package/dist/host/app.d.ts +0 -1
- package/dist/host/app.js +13 -23
- package/dist/host/config-for.d.ts +4 -0
- package/dist/host/config-for.js +2 -0
- package/dist/host/config.d.ts +0 -6
- package/dist/host/create-host.d.ts +3 -10
- package/dist/host/create-host.js +4 -1
- package/dist/host/gates.js +6 -1
- package/dist/host/main.js +32 -10
- package/dist/host/stack-config.d.ts +11 -8
- package/dist/host/stack-config.js +23 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +8 -0
- package/dist/mcp/connect.js +2 -1
- package/dist/mcp/pool.d.ts +1 -0
- package/dist/mcp/pool.js +1 -0
- package/dist/openapi.json +2359 -4096
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
- package/dist/store/postgres/schema.d.ts +194 -170
- package/dist/store/postgres/schema.js +46 -28
- package/dist/store/postgres/store.js +51 -100
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +24 -30
- package/dist/streams/control.d.ts +0 -5
- package/dist/streams/control.js +0 -8
- package/dist/streams/types.d.ts +2 -11
- package/dist/streams/types.js +1 -1
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +3 -7
- package/dist/tenant/context.d.ts +20 -22
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +5 -1
- package/dist/tenant/effects.js +5 -16
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/host-token.js +5 -5
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/operator-keys.d.ts +18 -0
- package/dist/tenant/operator-keys.js +55 -0
- package/dist/tenant/outbound.d.ts +30 -0
- package/dist/tenant/outbound.js +142 -10
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/run-token.js +3 -3
- package/dist/tenant/runtime.d.ts +4 -0
- package/dist/tenant/runtime.js +14 -1
- package/dist/tenant/sandboxes.d.ts +5 -5
- package/dist/tenant/sandboxes.js +5 -5
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/sessions.d.ts +2 -2
- package/dist/tenant/sessions.js +1 -1
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/streams.d.ts +2 -3
- package/dist/tenant/streams.js +1 -5
- package/dist/tenant/types.d.ts +21 -0
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +54 -0
- package/dist/vault/sources.js +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -187
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { CreateCredentialRequestSchema, CreateVaultRequestSchema, RotateCredentialRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
-
import { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, DeletedResponse, ListCredentialsResponse, ListVaultsResponse, RotateCredentialRequest, VaultInfo, } from "../../components.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { CreateCredentialRequestSchema, CreateVaultRequestSchema, RotateCredentialRequestSchema, StartOAuthRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, DeletedResponse, ListCredentialsResponse, ListVaultsResponse, RotateCredentialRequest, StartOAuthRequest, StartOAuthResponse, VaultInfo, } from "../../components.js";
|
|
4
|
+
import { HttpError } from "../../../tenant/http.js";
|
|
5
|
+
import { VaultError } from "../../../vault/error.js";
|
|
6
6
|
import { readJson } from "../body.js";
|
|
7
|
-
import {
|
|
7
|
+
import { tenantRoute } from "../define.js";
|
|
8
8
|
import { jsonResponse } from "../respond.js";
|
|
9
|
-
|
|
9
|
+
/** Every vault route, and starting an MCP OAuth connect: an application key acting for no one. */
|
|
10
|
+
const APPLICATION = { credentials: ["application"], scopes: "never" };
|
|
11
|
+
/**
|
|
12
|
+
* The OAuth callback: a browser sent back by the authorization server, with no credential and no
|
|
13
|
+
* protocol header. The `state` it carries is the grant.
|
|
14
|
+
*/
|
|
15
|
+
const CALLBACK = {
|
|
10
16
|
credentials: ["application", "subject", "token"],
|
|
11
|
-
scopes:
|
|
12
|
-
|
|
17
|
+
scopes: "any",
|
|
18
|
+
anonymous: true,
|
|
19
|
+
unversioned: true,
|
|
13
20
|
};
|
|
21
|
+
/** Where the authorization server sends the browser back. */
|
|
22
|
+
export const OAUTH_CALLBACK_PATH = "/v1/oauth/callback";
|
|
14
23
|
const json = (schema, description) => ({
|
|
15
24
|
description,
|
|
16
25
|
content: { "application/json": { schema } },
|
|
@@ -21,58 +30,45 @@ const body = (schema) => ({
|
|
|
21
30
|
});
|
|
22
31
|
const vaultId = z.object({ vaultId: z.string() });
|
|
23
32
|
const credentialId = vaultId.extend({ credentialId: z.string() });
|
|
24
|
-
/**
|
|
25
|
-
* Who a vault route acts for: the subject, or no one in particular (an application key). Below
|
|
26
|
-
* a vault, the vault must be the subject's before anything else.
|
|
27
|
-
*/
|
|
28
|
-
async function ownerFor(c) {
|
|
29
|
-
const { vault } = c.env.tenant;
|
|
30
|
-
const owner = ownerOf(c.get("scope"));
|
|
31
|
-
const id = c.req.param("vaultId");
|
|
32
|
-
if (id !== undefined && owner !== undefined)
|
|
33
|
-
await vault.assertOwner(id, owner);
|
|
34
|
-
return owner;
|
|
35
|
-
}
|
|
36
33
|
export function vaultRoutes(api) {
|
|
37
|
-
tenantRoute(api,
|
|
34
|
+
tenantRoute(api, APPLICATION, {
|
|
38
35
|
method: "post",
|
|
39
36
|
path: "/v1/vaults",
|
|
40
37
|
tags: ["Vaults"],
|
|
41
38
|
summary: "Create a vault",
|
|
39
|
+
description: "With `scope: \"installation\"` the installation's own vault, owned by `installation`, which any session may attach; or a vault of one person (`ownerUserId`), which only that person's sessions attach.",
|
|
42
40
|
request: { body: body(CreateVaultRequest) },
|
|
43
41
|
responses: {
|
|
44
42
|
200: json(VaultInfo, "The vault"),
|
|
45
43
|
409: { description: "The idempotency key was used for another vault" },
|
|
46
44
|
},
|
|
47
45
|
}, async (c) => {
|
|
48
|
-
const owner = await ownerFor(c);
|
|
49
46
|
const request = CreateVaultRequestSchema.parse(await readJson(c.req.raw));
|
|
50
|
-
if (owner !== undefined && request.ownerUserId !== owner)
|
|
51
|
-
fail(403, "ownerUserId must be the subject");
|
|
52
47
|
return jsonResponse(200, await c.env.tenant.vault.createVault(request));
|
|
53
48
|
});
|
|
54
|
-
tenantRoute(api,
|
|
49
|
+
tenantRoute(api, APPLICATION, {
|
|
55
50
|
method: "get",
|
|
56
51
|
path: "/v1/vaults",
|
|
57
52
|
tags: ["Vaults"],
|
|
58
|
-
summary: "List
|
|
53
|
+
summary: "List vaults",
|
|
54
|
+
description: "The installation vaults, after the vaults of `ownerUserId` when it names a person.",
|
|
59
55
|
request: {
|
|
60
56
|
query: z.object({
|
|
61
57
|
ownerUserId: z
|
|
62
58
|
.string()
|
|
63
59
|
.optional()
|
|
64
|
-
.meta({ description: "
|
|
60
|
+
.meta({ description: "Also list this person's vaults" }),
|
|
65
61
|
}),
|
|
66
62
|
},
|
|
67
63
|
responses: { 200: json(ListVaultsResponse, "The vaults") },
|
|
68
64
|
}, async (c) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
return jsonResponse(200, {
|
|
66
|
+
vaults: await c.env.tenant.vault.listVaults(c.req.query("ownerUserId"), {
|
|
67
|
+
installation: true,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
74
70
|
});
|
|
75
|
-
tenantRoute(api,
|
|
71
|
+
tenantRoute(api, APPLICATION, {
|
|
76
72
|
method: "get",
|
|
77
73
|
path: "/v1/vaults/{vaultId}",
|
|
78
74
|
tags: ["Vaults"],
|
|
@@ -80,10 +76,9 @@ export function vaultRoutes(api) {
|
|
|
80
76
|
request: { params: vaultId },
|
|
81
77
|
responses: { 200: json(VaultInfo, "The vault") },
|
|
82
78
|
}, async (c) => {
|
|
83
|
-
await ownerFor(c);
|
|
84
79
|
return jsonResponse(200, await c.env.tenant.vault.getVault(c.req.param("vaultId")));
|
|
85
80
|
});
|
|
86
|
-
tenantRoute(api,
|
|
81
|
+
tenantRoute(api, APPLICATION, {
|
|
87
82
|
method: "delete",
|
|
88
83
|
path: "/v1/vaults/{vaultId}",
|
|
89
84
|
tags: ["Vaults"],
|
|
@@ -91,30 +86,21 @@ export function vaultRoutes(api) {
|
|
|
91
86
|
request: { params: vaultId },
|
|
92
87
|
responses: { 200: json(DeletedResponse, "Deleted") },
|
|
93
88
|
}, async (c) => {
|
|
94
|
-
await ownerFor(c);
|
|
95
89
|
return jsonResponse(200, await c.env.tenant.vault.deleteVault(c.req.param("vaultId")));
|
|
96
90
|
});
|
|
97
|
-
tenantRoute(api,
|
|
91
|
+
tenantRoute(api, APPLICATION, {
|
|
98
92
|
method: "post",
|
|
99
93
|
path: "/v1/vaults/{vaultId}/credentials",
|
|
100
94
|
tags: ["Vaults"],
|
|
101
95
|
summary: "Add a credential",
|
|
102
|
-
description: "A bearer token or OAuth tokens, bound to the URLs a tool may send them to.
|
|
96
|
+
description: "A bearer token or OAuth tokens, bound to the URLs a tool may send them to.",
|
|
103
97
|
request: { params: vaultId, body: body(CreateCredentialRequest) },
|
|
104
98
|
responses: { 200: json(CredentialInfo, "The credential, without its secret") },
|
|
105
99
|
}, async (c) => {
|
|
106
|
-
await ownerFor(c);
|
|
107
100
|
const request = CreateCredentialRequestSchema.parse(await readJson(c.req.raw));
|
|
108
|
-
// The Runtime calls a refresh credential's token endpoint itself: never for a browser.
|
|
109
|
-
if (c.get("scope").kind === "token" &&
|
|
110
|
-
request.auth.type === "oauth" &&
|
|
111
|
-
request.auth.refresh)
|
|
112
|
-
fail(403, "A subject token cannot store OAuth refresh credentials", {
|
|
113
|
-
code: "scope_required",
|
|
114
|
-
});
|
|
115
101
|
return jsonResponse(200, await c.env.tenant.keys.createCredential(c.req.param("vaultId"), request));
|
|
116
102
|
});
|
|
117
|
-
tenantRoute(api,
|
|
103
|
+
tenantRoute(api, APPLICATION, {
|
|
118
104
|
method: "get",
|
|
119
105
|
path: "/v1/vaults/{vaultId}/credentials",
|
|
120
106
|
tags: ["Vaults"],
|
|
@@ -122,12 +108,11 @@ export function vaultRoutes(api) {
|
|
|
122
108
|
request: { params: vaultId },
|
|
123
109
|
responses: { 200: json(ListCredentialsResponse, "The credentials, without their secrets") },
|
|
124
110
|
}, async (c) => {
|
|
125
|
-
await ownerFor(c);
|
|
126
111
|
return jsonResponse(200, {
|
|
127
112
|
credentials: await c.env.tenant.vault.listCredentials(c.req.param("vaultId")),
|
|
128
113
|
});
|
|
129
114
|
});
|
|
130
|
-
tenantRoute(api,
|
|
115
|
+
tenantRoute(api, APPLICATION, {
|
|
131
116
|
method: "get",
|
|
132
117
|
path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
|
|
133
118
|
tags: ["Vaults"],
|
|
@@ -135,10 +120,9 @@ export function vaultRoutes(api) {
|
|
|
135
120
|
request: { params: credentialId },
|
|
136
121
|
responses: { 200: json(CredentialInfo, "The credential, without its secret") },
|
|
137
122
|
}, async (c) => {
|
|
138
|
-
await ownerFor(c);
|
|
139
123
|
return jsonResponse(200, await c.env.tenant.vault.getCredential(c.req.param("vaultId"), c.req.param("credentialId")));
|
|
140
124
|
});
|
|
141
|
-
tenantRoute(api,
|
|
125
|
+
tenantRoute(api, APPLICATION, {
|
|
142
126
|
method: "post",
|
|
143
127
|
path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
|
|
144
128
|
tags: ["Vaults"],
|
|
@@ -149,11 +133,10 @@ export function vaultRoutes(api) {
|
|
|
149
133
|
409: { description: "A credential's type cannot change" },
|
|
150
134
|
},
|
|
151
135
|
}, async (c) => {
|
|
152
|
-
await ownerFor(c);
|
|
153
136
|
const request = RotateCredentialRequestSchema.parse(await readJson(c.req.raw));
|
|
154
137
|
return jsonResponse(200, await c.env.tenant.keys.rotateCredential(c.req.param("vaultId"), c.req.param("credentialId"), request));
|
|
155
138
|
});
|
|
156
|
-
tenantRoute(api,
|
|
139
|
+
tenantRoute(api, APPLICATION, {
|
|
157
140
|
method: "delete",
|
|
158
141
|
path: "/v1/vaults/{vaultId}/credentials/{credentialId}",
|
|
159
142
|
tags: ["Vaults"],
|
|
@@ -161,7 +144,92 @@ export function vaultRoutes(api) {
|
|
|
161
144
|
request: { params: credentialId },
|
|
162
145
|
responses: { 200: json(DeletedResponse, "Deleted") },
|
|
163
146
|
}, async (c) => {
|
|
164
|
-
await ownerFor(c);
|
|
165
147
|
return jsonResponse(200, await c.env.tenant.vault.deleteCredential(c.req.param("vaultId"), c.req.param("credentialId")));
|
|
166
148
|
});
|
|
149
|
+
tenantRoute(api, APPLICATION, {
|
|
150
|
+
method: "post",
|
|
151
|
+
path: "/v1/vaults/{vaultId}/oauth/start",
|
|
152
|
+
tags: ["Vaults"],
|
|
153
|
+
summary: "Start an MCP OAuth connect",
|
|
154
|
+
description: "Signs the installation in to the remote MCP server at `url` (declared as `server`) and stores its OAuth credential, bound to `url`, in this installation vault. The Runtime discovers the server's authorization server (RFC 9728, RFC 8414), registers itself (RFC 7591) unless `clientId` names a registered client, and answers the URL to open in a browser; the sign-in returns to `GET /v1/oauth/callback` within `expiresAt`. The callback URL is `NYLORUN_PUBLIC_URL` + `/v1/oauth/callback`, or this request's own origin when the Host has no public URL. Application keys acting for no one only. `oauth_client_required` when the server offers no registration and no `clientId` was given.",
|
|
155
|
+
request: { params: vaultId, body: body(StartOAuthRequest) },
|
|
156
|
+
responses: {
|
|
157
|
+
200: json(StartOAuthResponse, "Where to send the browser"),
|
|
158
|
+
502: {
|
|
159
|
+
description: "The authorization server, or its discovery, failed or refused (`oauth_failed`)",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}, async (c) => {
|
|
163
|
+
const request = StartOAuthRequestSchema.parse(await readJson(c.req.raw));
|
|
164
|
+
const tenant = c.env.tenant;
|
|
165
|
+
const base = tenant.config.publicUrl ?? new URL(c.req.url).origin;
|
|
166
|
+
return jsonResponse(200, await tenant.keys.startOAuth({
|
|
167
|
+
vaultId: c.req.param("vaultId"),
|
|
168
|
+
server: request.server,
|
|
169
|
+
url: request.url,
|
|
170
|
+
...(request.clientId === undefined ? {} : { clientId: request.clientId }),
|
|
171
|
+
redirectUri: `${base}${OAUTH_CALLBACK_PATH}`,
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
tenantRoute(api, CALLBACK, {
|
|
175
|
+
method: "get",
|
|
176
|
+
path: OAUTH_CALLBACK_PATH,
|
|
177
|
+
tags: ["Vaults"],
|
|
178
|
+
summary: "Finish an MCP OAuth connect",
|
|
179
|
+
description: "Where the authorization server sends the browser back with `code` and `state` (or `error`). Exchanges the code once and stores the credential; answers a small HTML page. Needs no credential and no `Nylorun-Protocol`. A `state` is used once, for ten minutes (`oauth_state_invalid`).",
|
|
180
|
+
request: {
|
|
181
|
+
query: z.object({
|
|
182
|
+
code: z.string().optional(),
|
|
183
|
+
state: z.string().optional(),
|
|
184
|
+
error: z.string().optional(),
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
responses: {
|
|
188
|
+
200: { description: "Connected", content: { "text/html": { schema: z.string() } } },
|
|
189
|
+
502: { description: "The authorization server refused the code (`oauth_failed`)" },
|
|
190
|
+
},
|
|
191
|
+
}, async (c) => {
|
|
192
|
+
const state = c.req.query("state");
|
|
193
|
+
const code = c.req.query("code");
|
|
194
|
+
const error = c.req.query("error");
|
|
195
|
+
if (!state)
|
|
196
|
+
return page(400, "Sign-in failed", "The authorization server sent no state. Start the connect again.");
|
|
197
|
+
try {
|
|
198
|
+
await c.env.tenant.keys.finishOAuth({
|
|
199
|
+
state,
|
|
200
|
+
...(code === undefined ? {} : { code }),
|
|
201
|
+
...(error === undefined ? {} : { error }),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (failure) {
|
|
205
|
+
if (failure instanceof HttpError || failure instanceof VaultError)
|
|
206
|
+
return page(failure.status, "Sign-in failed", failure.message);
|
|
207
|
+
c.env.tenant.config.logger.error("oauth_callback_failed", {
|
|
208
|
+
error: failure instanceof Error ? failure.name : "unknown",
|
|
209
|
+
});
|
|
210
|
+
return page(500, "Sign-in failed", "The connect failed. Start it again.");
|
|
211
|
+
}
|
|
212
|
+
return page(200, "Connected", "Connected. You can close this tab.");
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
216
|
+
const escape = (text) => text.replace(/[&<>"']/g, (char) => ESCAPES[char]);
|
|
217
|
+
/** The callback's answer: a small page that loads nothing, is never cached and leaks no referrer. */
|
|
218
|
+
function page(status, title, message) {
|
|
219
|
+
const html = `<!doctype html>
|
|
220
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
221
|
+
<title>${escape(title)} · Nylorun</title>
|
|
222
|
+
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1rem;color:#1a1a1a;background:#fff}@media (prefers-color-scheme:dark){body{color:#eee;background:#111}}h1{font-size:1.25rem}</style>
|
|
223
|
+
</head><body><h1>${escape(title)}</h1><p>${escape(message)}</p></body></html>
|
|
224
|
+
`;
|
|
225
|
+
return new Response(html, {
|
|
226
|
+
status,
|
|
227
|
+
headers: {
|
|
228
|
+
"content-type": "text/html; charset=utf-8",
|
|
229
|
+
"cache-control": "no-store",
|
|
230
|
+
"referrer-policy": "no-referrer",
|
|
231
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
232
|
+
"x-content-type-options": "nosniff",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
167
235
|
}
|
package/dist/api/openapi.js
CHANGED
|
@@ -68,15 +68,9 @@ function hostRoutes() {
|
|
|
68
68
|
return registry;
|
|
69
69
|
}
|
|
70
70
|
function tenantSchemes(registry) {
|
|
71
|
-
registry.registerComponent("securitySchemes", "applicationKey", bearer("An application key of the Tenant, or
|
|
72
|
-
registry.registerComponent("securitySchemes", "
|
|
71
|
+
registry.registerComponent("securitySchemes", "applicationKey", bearer("An application key of the Tenant: an operator key (`PUT /v1/admin/keys/{id}`), or Studio's key, derived from the admin key. With `Nylorun-Subject` and `Nylorun-Scopes`, it acts for that person, narrowed to those scopes. Never accepted from a browser (`Origin`)."));
|
|
72
|
+
registry.registerComponent("securitySchemes", "issuerToken", bearer("A JWT from a trusted issuer of the Host's identity file (Host feature `trusted-issuers`): one person, with the issuer's scopes, agents and sandbox grants, until it expires. Accepted from servers and browsers alike; CORS is the operator's proxy's.", "JWT"));
|
|
73
73
|
registry.registerComponent("securitySchemes", "deliveryToken", bearer("The delivery token the Runtime signs each Action delivery with (`Nylorun-Signature`), sent back by the Action endpoint. It reaches only that Action's heartbeat, result and sandbox routes, while that delivery is current, for at most 15 minutes.", "JWT"));
|
|
74
|
-
registry.registerComponent("securitySchemes", "publishableKey", {
|
|
75
|
-
type: "apiKey",
|
|
76
|
-
in: "header",
|
|
77
|
-
name: "Nylorun-Key",
|
|
78
|
-
description: "A publishable key, sent by a browser page on one of its origins. Public by design: it grants the access policy's anonymous scopes.",
|
|
79
|
-
});
|
|
80
74
|
}
|
|
81
75
|
let tenant;
|
|
82
76
|
let admin;
|
|
@@ -94,7 +88,7 @@ export function tenantDocument() {
|
|
|
94
88
|
info: {
|
|
95
89
|
title: "Nylorun Runtime: Tenant API",
|
|
96
90
|
version: RUNTIME_VERSION,
|
|
97
|
-
description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials
|
|
91
|
+
description: "Agents, sessions and their events, vaults, access and the Tenant's settings. A Runtime serves one Tenant: its URL is the Tenant's, and no request names it. Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials` and `x-nylorun-scopes` (the subject scopes that reach it) fields.",
|
|
98
92
|
"x-nylorun-protocol": PROTOCOL,
|
|
99
93
|
},
|
|
100
94
|
servers: [
|
|
@@ -122,7 +116,7 @@ export function adminDocument() {
|
|
|
122
116
|
info: {
|
|
123
117
|
title: "Nylorun Runtime: Admin API",
|
|
124
118
|
version: RUNTIME_VERSION,
|
|
125
|
-
description: "The Host's status, its Tenant's state and
|
|
119
|
+
description: "The Host's status, its Tenant's state, shutdown and the Tenant's operator keys, with the admin key. Served on the operator listener when the Host has one; the public listener answers these routes with the opaque 404.",
|
|
126
120
|
"x-nylorun-protocol": PROTOCOL,
|
|
127
121
|
},
|
|
128
122
|
servers: [
|
package/dist/artifacts/links.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Capability links (protocol 6): a short-lived URL that opens one artifact version with no
|
|
3
3
|
* other credential, for UIs, `<img>` tags and sharing. The token in the path is an ES256 JWT
|
|
4
4
|
* (`typ: nylorun-artifact+jwt`) signed with the Tenant's current signing key by the keys
|
|
5
|
-
* service, like
|
|
6
|
-
* `nylorun-artifact`, and it lives at most `ARTIFACT_LINK_MAX_TTL_SECONDS` (
|
|
7
|
-
*
|
|
5
|
+
* service, like delivery tokens: `sub` is the artifact, `ver` the version, `aud`
|
|
6
|
+
* `nylorun-artifact`, and it lives at most `ARTIFACT_LINK_MAX_TTL_SECONDS` (`TOKEN_TTL_MAX_SECONDS`,
|
|
7
|
+
* so a key rotation never revokes a live link). A link opens nothing once its artifact
|
|
8
8
|
* is deleted. A folder's link (F8.2) opens its zip, or with a `path` claim one of its files.
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { errors, jwtVerify } from "jose";
|
|
12
|
-
import { ARTIFACT_LINK_DEFAULT_TTL_SECONDS, ARTIFACT_LINK_MAX_TTL_SECONDS, ARTIFACT_LINK_TOKEN_TYPE,
|
|
12
|
+
import { ARTIFACT_LINK_DEFAULT_TTL_SECONDS, ARTIFACT_LINK_MAX_TTL_SECONDS, ARTIFACT_LINK_TOKEN_TYPE, tenantTokenIssuer, } from "@nylorun/core/contracts";
|
|
13
13
|
import { isArtifactId } from "@nylorun/core/compatibility";
|
|
14
14
|
import { failOpaque, HttpError } from "../tenant/http.js";
|
|
15
15
|
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "../tenant/jwt.js";
|
|
@@ -24,7 +24,7 @@ export async function mintArtifactLink(ctx, link) {
|
|
|
24
24
|
const { token } = await ctx.keys.sign({
|
|
25
25
|
typ: ARTIFACT_LINK_TOKEN_TYPE,
|
|
26
26
|
claims: {
|
|
27
|
-
iss:
|
|
27
|
+
iss: tenantTokenIssuer(ctx.config.tenantId),
|
|
28
28
|
aud: ARTIFACT_LINK_AUDIENCE,
|
|
29
29
|
sub: link.artifactId,
|
|
30
30
|
ver: link.version,
|
|
@@ -57,7 +57,7 @@ export async function verifyArtifactLink(ctx, raw) {
|
|
|
57
57
|
try {
|
|
58
58
|
const verified = await jwtVerify(raw, await ctx.signingKeys.publicKey(row), {
|
|
59
59
|
algorithms: ["ES256"],
|
|
60
|
-
issuer:
|
|
60
|
+
issuer: tenantTokenIssuer(ctx.config.tenantId),
|
|
61
61
|
audience: ARTIFACT_LINK_AUDIENCE,
|
|
62
62
|
typ: ARTIFACT_LINK_TOKEN_TYPE,
|
|
63
63
|
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* module, after re-checking the Tenant total under the quota lock. Until it commits the object
|
|
12
12
|
* is garbage, and a failed commit deletes it.
|
|
13
13
|
* - **Access mirrors sessions.** An application principal reaches every artifact. A request acting
|
|
14
|
-
* for a person (a
|
|
14
|
+
* for a person (a trusted issuer's token or subject headers) reaches only artifacts of that person's
|
|
15
15
|
* sessions; another person's, or a Tenant-wide one, is the same 404 as a missing artifact.
|
|
16
16
|
*
|
|
17
17
|
* Lock order, the same in every write: the session, then the quota lock, then the artifact row.
|
|
@@ -38,7 +38,11 @@ export function createMcpHandler(options) {
|
|
|
38
38
|
const manifest = await declared(vault, server);
|
|
39
39
|
const connection = await open({
|
|
40
40
|
server: manifest,
|
|
41
|
-
authorize: (url) => vault.authorizeMcp(server.sessionId, {
|
|
41
|
+
authorize: (url) => vault.authorizeMcp(server.sessionId, {
|
|
42
|
+
url,
|
|
43
|
+
serverName: server.serverName,
|
|
44
|
+
...(server.agentId === undefined ? {} : { agentId: server.agentId }),
|
|
45
|
+
}),
|
|
42
46
|
pluginData: "",
|
|
43
47
|
});
|
|
44
48
|
const entry = { connection, lastUsedAt: now(), active: 0 };
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
import type { Credential } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { ModelFailureOutcome } from "@nylorun/core/define";
|
|
11
11
|
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
12
|
-
import {
|
|
12
|
+
import { type AuthorizeResult, type HostModelSecret } from "../vault/service.js";
|
|
13
|
+
import { CredentialSources, type McpCredentialRequest, type ResolverConfig } from "../vault/sources.js";
|
|
13
14
|
import type { SessionStore } from "../store/types.js";
|
|
14
15
|
import type { Session } from "../tenant/context.js";
|
|
15
16
|
import { type Keys } from "../keys/keys.js";
|
|
17
|
+
import { type OutboundPolicy } from "../tenant/outbound.js";
|
|
16
18
|
/** The Tenant, as a model call needs it. */
|
|
17
19
|
export interface TenantVault {
|
|
18
20
|
readonly tenantId: string;
|
|
@@ -26,12 +28,14 @@ export interface TenantVault {
|
|
|
26
28
|
/** A session, for the remote MCP servers its pinned manifest declares (F4.1). */
|
|
27
29
|
session(sessionId: string): Promise<Session | undefined>;
|
|
28
30
|
/**
|
|
29
|
-
* The
|
|
30
|
-
*
|
|
31
|
+
* The authorization of one request to a session's remote MCP server: its credential from the
|
|
32
|
+
* session's attached vaults, refreshed when due (F4.1), else from the operator's credential
|
|
33
|
+
* resolver (F9 C1).
|
|
31
34
|
*/
|
|
32
35
|
authorizeMcp(sessionId: string, request: {
|
|
33
36
|
url: string;
|
|
34
37
|
serverName: string;
|
|
38
|
+
agentId?: string;
|
|
35
39
|
}): Promise<AuthorizeResult>;
|
|
36
40
|
/** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
|
|
37
41
|
keys(): Keys;
|
|
@@ -52,13 +56,18 @@ export interface TenantVaultsOptions {
|
|
|
52
56
|
readonly sql: PostgresClient;
|
|
53
57
|
/** The Host root; the gate reads `keys/vault-kek` and `tenant/home` under it. */
|
|
54
58
|
readonly hostRoot: string;
|
|
59
|
+
/** The operator's credential resolver (`NYLORUN_RESOLVER_*`, F9 C1). */
|
|
60
|
+
readonly resolver?: ResolverConfig;
|
|
61
|
+
/**
|
|
62
|
+
* The Host's address policy (`NYLORUN_ENDPOINT_*`): OAuth refresh and MCP OAuth connect call
|
|
63
|
+
* authorization servers through `guardedFetch` under it (F9 C2).
|
|
64
|
+
*/
|
|
65
|
+
readonly delivery?: OutboundPolicy;
|
|
55
66
|
}
|
|
56
67
|
export declare function createTenantVaults(options: TenantVaultsOptions): TenantVaults;
|
|
57
68
|
/**
|
|
58
|
-
* A request to a session's remote MCP server, authorized from the session's attached vaults
|
|
59
|
-
* what the loop
|
|
69
|
+
* A request to a session's remote MCP server, authorized from the session's attached vaults,
|
|
70
|
+
* else the operator's credential resolver: what the loop does in its own process
|
|
71
|
+
* (`tenant/effects.ts` `authorize`). The owner and turn come from the session row.
|
|
60
72
|
*/
|
|
61
|
-
export declare function authorizeSessionMcp(
|
|
62
|
-
url: string;
|
|
63
|
-
serverName: string;
|
|
64
|
-
}): Promise<AuthorizeResult>;
|
|
73
|
+
export declare function authorizeSessionMcp(sources: CredentialSources, session: (sessionId: string) => Promise<Session | undefined>, sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
@@ -8,8 +8,10 @@ import { tenantPaths } from "../tenant/paths.js";
|
|
|
8
8
|
import { readVaultKek } from "../vault/kek.js";
|
|
9
9
|
import { HostModelVault } from "../vault/host-model.js";
|
|
10
10
|
import { VaultService } from "../vault/service.js";
|
|
11
|
+
import { CredentialSources } from "../vault/sources.js";
|
|
11
12
|
import { inProcessKeys } from "../keys/keys.js";
|
|
12
13
|
import { SigningKeys } from "../tenant/signing-keys.js";
|
|
14
|
+
import { guardedFetch } from "../tenant/outbound.js";
|
|
13
15
|
/** A call the gate refuses before the provider is called; it answers with `outcome`. */
|
|
14
16
|
export class GateRefusal extends Error {
|
|
15
17
|
outcome;
|
|
@@ -33,7 +35,15 @@ export function createTenantVaults(options) {
|
|
|
33
35
|
return kek;
|
|
34
36
|
};
|
|
35
37
|
const vault = new HostModelVault({ store, kek: readKek });
|
|
36
|
-
const credentials = new VaultService({
|
|
38
|
+
const credentials = new VaultService({
|
|
39
|
+
store,
|
|
40
|
+
kek: readKek,
|
|
41
|
+
fetch: guardedFetch(options.delivery ?? {}),
|
|
42
|
+
});
|
|
43
|
+
const sources = new CredentialSources({
|
|
44
|
+
vault: credentials,
|
|
45
|
+
...(options.resolver ? { resolver: options.resolver } : {}),
|
|
46
|
+
});
|
|
37
47
|
const keys = inProcessKeys({
|
|
38
48
|
store,
|
|
39
49
|
vault: credentials,
|
|
@@ -48,7 +58,7 @@ export function createTenantVaults(options) {
|
|
|
48
58
|
readHostModel: () => vault.readHostModel(),
|
|
49
59
|
writeHostCredential: (credential) => vault.updateHostCredential(credential),
|
|
50
60
|
session,
|
|
51
|
-
authorizeMcp: (sessionId, request) => authorizeSessionMcp(
|
|
61
|
+
authorizeMcp: (sessionId, request) => authorizeSessionMcp(sources, session, sessionId, request),
|
|
52
62
|
keys: () => keys,
|
|
53
63
|
};
|
|
54
64
|
}
|
|
@@ -77,18 +87,13 @@ export function createTenantVaults(options) {
|
|
|
77
87
|
};
|
|
78
88
|
}
|
|
79
89
|
/**
|
|
80
|
-
* A request to a session's remote MCP server, authorized from the session's attached vaults
|
|
81
|
-
* what the loop
|
|
90
|
+
* A request to a session's remote MCP server, authorized from the session's attached vaults,
|
|
91
|
+
* else the operator's credential resolver: what the loop does in its own process
|
|
92
|
+
* (`tenant/effects.ts` `authorize`). The owner and turn come from the session row.
|
|
82
93
|
*/
|
|
83
|
-
export async function authorizeSessionMcp(
|
|
94
|
+
export async function authorizeSessionMcp(sources, session, sessionId, request) {
|
|
84
95
|
const found = await session(sessionId);
|
|
85
96
|
if (!found)
|
|
86
97
|
throw new Error(`Session ${sessionId} not found`);
|
|
87
|
-
return
|
|
88
|
-
sessionId,
|
|
89
|
-
vaultIds: found.vaultIds ?? [],
|
|
90
|
-
credentialSelections: found.credentialSelections ?? [],
|
|
91
|
-
url: request.url,
|
|
92
|
-
serverName: request.serverName,
|
|
93
|
-
});
|
|
98
|
+
return sources.authorize({ ...found, id: sessionId }, request);
|
|
94
99
|
}
|
package/dist/host/admin-api.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Admin API (`/v1/admin/**`): the Host's status and shutdown,
|
|
3
|
-
* key. A Host serves one Tenant, so there are no
|
|
4
|
-
* and, when it could not be opened, why
|
|
5
|
-
*
|
|
6
|
-
* declared once, for serving and for the
|
|
2
|
+
* The Admin API (`/v1/admin/**`): the Host's status and shutdown, and the Tenant's operator
|
|
3
|
+
* keys (F9 I1), for a caller with the admin key. A Host serves one Tenant, so there are no
|
|
4
|
+
* Tenant routes: the status names the Tenant and, when it could not be opened, why
|
|
5
|
+
* (tenancy.md §9). The Host checks the listener, the protocol and the admin key first
|
|
6
|
+
* (`app.ts`); what gets here is authorized. Routes are declared once, for serving and for the
|
|
7
|
+
* OpenAPI document.
|
|
7
8
|
*/
|
|
8
9
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
9
10
|
import type { AdminStatus } from "@nylorun/core/contracts";
|
|
11
|
+
import type { OperatorKeys } from "../tenant/operator-keys.js";
|
|
10
12
|
import type { NodeBindings } from "../tenant/types.js";
|
|
11
13
|
export interface AdminApiOptions {
|
|
12
14
|
/** `GET /v1/admin/status`. */
|
|
@@ -15,6 +17,11 @@ export interface AdminApiOptions {
|
|
|
15
17
|
shutdown(): void;
|
|
16
18
|
/** `GET /v1/admin/openapi.json`: this API's OpenAPI document (`api/openapi.ts`). */
|
|
17
19
|
document?(): unknown;
|
|
20
|
+
/**
|
|
21
|
+
* The open Tenant's operator keys (`/v1/admin/keys`); undefined while the Tenant is not open.
|
|
22
|
+
* Rejects with `TenantUnavailableError` (503) when it cannot be reached.
|
|
23
|
+
*/
|
|
24
|
+
operatorKeys?(): Promise<OperatorKeys | undefined>;
|
|
18
25
|
}
|
|
19
26
|
type AdminEnv = {
|
|
20
27
|
Bindings: NodeBindings;
|
package/dist/host/admin-api.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Admin API (`/v1/admin/**`): the Host's status and shutdown,
|
|
3
|
-
* key. A Host serves one Tenant, so there are no
|
|
4
|
-
* and, when it could not be opened, why
|
|
5
|
-
*
|
|
6
|
-
* declared once, for serving and for the
|
|
2
|
+
* The Admin API (`/v1/admin/**`): the Host's status and shutdown, and the Tenant's operator
|
|
3
|
+
* keys (F9 I1), for a caller with the admin key. A Host serves one Tenant, so there are no
|
|
4
|
+
* Tenant routes: the status names the Tenant and, when it could not be opened, why
|
|
5
|
+
* (tenancy.md §9). The Host checks the listener, the protocol and the admin key first
|
|
6
|
+
* (`app.ts`); what gets here is authorized. Routes are declared once, for serving and for the
|
|
7
|
+
* OpenAPI document.
|
|
7
8
|
*/
|
|
8
9
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
9
10
|
import { z } from "zod";
|
|
10
|
-
import { AdminStatus as AdminStatusBody, HostShutdownResponse, ProtocolRejected, Rejected, } from "../api/components.js";
|
|
11
|
+
import { AdminStatus as AdminStatusBody, DeleteOperatorKeyResponse, HostShutdownResponse, ListOperatorKeysResponse, PutOperatorKeyResponse, ProtocolRejected, Rejected, } from "../api/components.js";
|
|
11
12
|
import { serveRoute } from "../api/route.js";
|
|
12
13
|
import { jsonResponse, rejectedResponse } from "./http.js";
|
|
13
14
|
const json = (schema, description) => ({
|
|
@@ -54,6 +55,68 @@ export function createAdminApi(options) {
|
|
|
54
55
|
c.env.outgoing.once("finish", () => options.shutdown());
|
|
55
56
|
return jsonResponse(200, { status: "shutting_down" });
|
|
56
57
|
});
|
|
58
|
+
const keyId = z.object({
|
|
59
|
+
id: z.string().meta({ description: "The key's name: `^[a-z][a-z0-9-]{0,31}$`, never `studio`" }),
|
|
60
|
+
});
|
|
61
|
+
const keysOf = async () => (await options.operatorKeys?.()) ??
|
|
62
|
+
rejectedResponse(503, "request_rejected", "The Tenant is not open: see /v1/admin/status");
|
|
63
|
+
serveRoute(api, adminRoute({
|
|
64
|
+
method: "get",
|
|
65
|
+
path: "/v1/admin/keys",
|
|
66
|
+
summary: "List the Tenant's keys",
|
|
67
|
+
description: "Every application key of the Tenant by name, with its role and when it was issued. Keys themselves are never shown again.",
|
|
68
|
+
responses: {
|
|
69
|
+
200: json(ListOperatorKeysResponse, "The keys, by id"),
|
|
70
|
+
503: rejected("The Tenant is not open"),
|
|
71
|
+
},
|
|
72
|
+
}), async () => {
|
|
73
|
+
const keys = await keysOf();
|
|
74
|
+
if (keys instanceof Response)
|
|
75
|
+
return keys;
|
|
76
|
+
return jsonResponse(200, { keys: await keys.list() });
|
|
77
|
+
});
|
|
78
|
+
serveRoute(api, adminRoute({
|
|
79
|
+
method: "put",
|
|
80
|
+
path: "/v1/admin/keys/{id}",
|
|
81
|
+
summary: "Create or rotate a key",
|
|
82
|
+
description: "Issues a new key named `id` and returns it this once. A key of that name is replaced: the old one stops authenticating at once. `studio` is refused.",
|
|
83
|
+
request: { params: keyId },
|
|
84
|
+
responses: {
|
|
85
|
+
200: json(PutOperatorKeyResponse, "The new key, shown once"),
|
|
86
|
+
400: rejected("Not a key name, or `studio`"),
|
|
87
|
+
503: rejected("The Tenant is not open"),
|
|
88
|
+
},
|
|
89
|
+
}), async (c) => {
|
|
90
|
+
const keys = await keysOf();
|
|
91
|
+
if (keys instanceof Response)
|
|
92
|
+
return keys;
|
|
93
|
+
const put = await keys.put(c.req.param("id") ?? "");
|
|
94
|
+
return "reason" in put ? refusedResponse(put) : jsonResponse(200, put);
|
|
95
|
+
});
|
|
96
|
+
serveRoute(api, adminRoute({
|
|
97
|
+
method: "delete",
|
|
98
|
+
path: "/v1/admin/keys/{id}",
|
|
99
|
+
summary: "Delete a key",
|
|
100
|
+
description: "The key stops authenticating at once. `studio` is refused.",
|
|
101
|
+
request: { params: keyId },
|
|
102
|
+
responses: {
|
|
103
|
+
200: json(DeleteOperatorKeyResponse, "The key is deleted"),
|
|
104
|
+
400: rejected("Not a key name, or `studio`"),
|
|
105
|
+
404: rejected("No key of that name; or an unknown admin key, or the public listener"),
|
|
106
|
+
503: rejected("The Tenant is not open"),
|
|
107
|
+
},
|
|
108
|
+
}), async (c) => {
|
|
109
|
+
const keys = await keysOf();
|
|
110
|
+
if (keys instanceof Response)
|
|
111
|
+
return keys;
|
|
112
|
+
const id = c.req.param("id") ?? "";
|
|
113
|
+
const deleted = await keys.delete(id);
|
|
114
|
+
if (typeof deleted !== "boolean")
|
|
115
|
+
return refusedResponse(deleted);
|
|
116
|
+
return deleted
|
|
117
|
+
? jsonResponse(200, { id, deleted: true })
|
|
118
|
+
: rejectedResponse(404, "not_found", `No key ${id}`);
|
|
119
|
+
});
|
|
57
120
|
serveRoute(api, adminRoute({
|
|
58
121
|
method: "get",
|
|
59
122
|
path: "/v1/admin/openapi.json",
|
|
@@ -72,3 +135,6 @@ export function createAdminApi(options) {
|
|
|
72
135
|
function notFound() {
|
|
73
136
|
return rejectedResponse(404, "not_found", "Route not found");
|
|
74
137
|
}
|
|
138
|
+
function refusedResponse(refusal) {
|
|
139
|
+
return rejectedResponse(400, refusal.reason === "invalid" ? "invalid_request" : "request_rejected", refusal.message);
|
|
140
|
+
}
|