strapi-oauth-mcp-manager 1.0.0 → 1.1.0
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 +23 -0
- package/README.md +130 -0
- package/dist/_chunks/{App-Ddu3EpO8.mjs → App-BLcSXikb.mjs} +48 -20
- package/dist/_chunks/{App-WKeHpRl7.js → App-CxPrn10p.js} +46 -18
- package/dist/_chunks/{index-BrZfQfFg.mjs → index-B36PlzpY.mjs} +1 -1
- package/dist/_chunks/{index-BPOlIxN_.js → index-dzfso1OG.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +362 -51
- package/dist/server/index.mjs +362 -51
- package/dist/server/src/config/index.d.ts +15 -0
- package/dist/server/src/content-types/index.d.ts +9 -2
- package/dist/server/src/identity/index.d.ts +8 -0
- package/dist/server/src/identity/line.d.ts +8 -0
- package/dist/server/src/identity/types.d.ts +15 -0
- package/dist/server/src/index.d.ts +27 -4
- package/dist/server/src/services/index.d.ts +16 -2
- package/dist/server/src/services/oauth.d.ts +36 -12
- package/dist/server/src/utils/bearer.d.ts +2 -0
- package/dist/server/src/utils/end-user.d.ts +25 -0
- package/dist/server/src/utils/oauth-error.d.ts +11 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
Customer sign-in with LINE, for apps that expose MCP tools to their own users.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Token exchange (RFC 8693) at the token endpoint: a LINE client posts a customer's LINE ID token (`liff.getIDToken()`) and gets a short-lived MCP session. The session runs with the client's mapped admin token.
|
|
10
|
+
- `resolveSubject(authorization)` on the `oauth` service. Tool plugins pass the `Authorization` header their handler received and get the customer's `line:U…` subject, or `null` when there's no verified customer (staff sessions, plain admin tokens and anything invalid). Call it only from MCP tool handlers: it identifies the customer but doesn't authenticate the request.
|
|
11
|
+
- Configuration: `identityProviders.line.channelId` (digits only: the LINE Login channel ID, not the LIFF ID), the test-only `identityProviders.line.verifyUrl` (Strapi warns at startup while it's set), and `endUserAccessTokenTtl` (default 3600 seconds).
|
|
12
|
+
- "Customer sign-in" when adding a client on the **MCP OAuth** page, a Customer column in the sessions list (masked), and token exchange details under Connection details.
|
|
13
|
+
- Discovery lists the token exchange grant when LINE sign-in is configured.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- New fields: `endUserProvider` on clients (existing clients are staff clients), and `subject` on grants.
|
|
18
|
+
- LINE clients are always public, must keep a mapped admin token, and may have no redirect URI. Only one LINE client can be active at a time, and LINE clients can't use the consent page (`/authorize`).
|
|
19
|
+
- Customer sessions have no refresh token. They expire after `endUserAccessTokenTtl`, and the app exchanges a fresh ID token.
|
|
20
|
+
- Token exchange answers `invalid_grant` only when LINE rejects the ID token. When LINE can't be reached or the LINE client's admin token can't be used, it answers `temporarily_unavailable` (503) with `Retry-After`, and the Strapi log says why.
|
|
21
|
+
|
|
22
|
+
### Breaking changes
|
|
23
|
+
|
|
24
|
+
None. Staff sign-in, dynamic client registration, refresh rotation and revocation work as before.
|
|
25
|
+
|
|
3
26
|
## 1.0.0
|
|
4
27
|
|
|
5
28
|
A rewrite that adds OAuth sign-in to the MCP server built into Strapi 5.47+ (`/mcp`).
|
package/README.md
CHANGED
|
@@ -174,6 +174,108 @@ These sign-in methods aren't supported:
|
|
|
174
174
|
|
|
175
175
|
---
|
|
176
176
|
|
|
177
|
+
## Customer sign-in with LINE
|
|
178
|
+
|
|
179
|
+
Staff connect with their Strapi admin account. Your **customers** can use MCP tools too, from an app inside LINE (a LIFF app or LINE MINI App). The app exchanges the customer's LINE ID token for a short-lived session, and your tools find out who the customer is.
|
|
180
|
+
|
|
181
|
+
### Set it up
|
|
182
|
+
|
|
183
|
+
1. **Configure the LINE channel** whose ID tokens you accept. That's the LINE Login or LINE MINI App channel that hosts your app. Use its channel ID (digits only, such as `1657000000`), not the LIFF ID.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// config/plugins.ts
|
|
187
|
+
'strapi-oauth-mcp-manager': {
|
|
188
|
+
config: {
|
|
189
|
+
identityProviders: env('LINE_LOGIN_CHANNEL_ID')
|
|
190
|
+
? { line: { channelId: env('LINE_LOGIN_CHANNEL_ID') } }
|
|
191
|
+
: {},
|
|
192
|
+
endUserAccessTokenTtl: 3600,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
2. **Create an admin token for customers** under Settings → Admin Tokens. Every customer session runs with its permissions, so grant only what customers may do.
|
|
198
|
+
|
|
199
|
+
> **Content-type permissions aren't scoped to the customer.** The content tools Strapi generates (such as `list_article`) don't call `resolveSubject`, so any content-type permission on this token is available to every customer, unscoped. Grant only the tool-plugin actions customers need.
|
|
200
|
+
|
|
201
|
+
3. **Add a client** on the **MCP OAuth** page. Choose **Customer sign-in: LINE** and map that token. LINE clients are public, and don't need a redirect URI.
|
|
202
|
+
4. **Allow your app's origin** in `config/middlewares.ts`, because the app calls Strapi from the browser:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
{
|
|
206
|
+
name: 'strapi::cors',
|
|
207
|
+
config: {
|
|
208
|
+
origin: ['https://your-app.example.com'],
|
|
209
|
+
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
210
|
+
headers: ['Content-Type', 'Authorization', 'Accept', 'mcp-session-id', 'mcp-protocol-version', 'Last-Event-ID'],
|
|
211
|
+
expose: ['WWW-Authenticate', 'mcp-session-id', 'mcp-protocol-version'],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Exchange the ID token in your app
|
|
217
|
+
|
|
218
|
+
The LIFF app needs the `openid` scope for `liff.getIDToken()`.
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
async function startCustomerSession() {
|
|
222
|
+
await liff.init({ liffId });
|
|
223
|
+
if (!liff.isLoggedIn()) {
|
|
224
|
+
liff.login(); // LINE sends the customer back to your app, signed in
|
|
225
|
+
return null; // without this, the request below would send subject_token=null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const response = await fetch(`${STRAPI_URL}/api/strapi-oauth-mcp-manager/oauth/token`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
231
|
+
body: new URLSearchParams({
|
|
232
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
233
|
+
client_id: LINE_CLIENT_ID,
|
|
234
|
+
subject_token: liff.getIDToken(),
|
|
235
|
+
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
|
|
236
|
+
resource: `${STRAPI_URL}/mcp`,
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
const { access_token, expires_in } = await response.json();
|
|
240
|
+
// Call ${STRAPI_URL}/mcp with "Authorization: Bearer <access_token>". There is no refresh token:
|
|
241
|
+
// exchange a fresh ID token when the session expires.
|
|
242
|
+
return { access_token, expires_in };
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
If the token endpoint answers `invalid_grant`, LINE rejected the ID token: sign the customer in again. If it answers `temporarily_unavailable` (HTTP 503), LINE couldn't be reached or the LINE client needs an admin's attention: try again after the `Retry-After` seconds. The Strapi log says which.
|
|
247
|
+
|
|
248
|
+
### Use the customer's identity in your tools
|
|
249
|
+
|
|
250
|
+
Tool handlers see the caller's original `Authorization` header, so ask this plugin who holds the session:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
createHandler: (strapi) => async ({ args, extra }) => {
|
|
254
|
+
const subject = await strapi
|
|
255
|
+
.plugin('strapi-oauth-mcp-manager')
|
|
256
|
+
.service('oauth')
|
|
257
|
+
.resolveSubject(extra.requestInfo?.headers?.authorization); // "line:U…" or null
|
|
258
|
+
if (!subject) {
|
|
259
|
+
// No verified customer: a staff session, a plain admin token, or no valid session.
|
|
260
|
+
}
|
|
261
|
+
// ...scope the tool to this customer
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`resolveSubject` identifies the customer but doesn't authenticate the request, so call it only from MCP tool handlers, where the middleware has already validated the session. `null` means there's no verified customer, not that the caller is staff.
|
|
266
|
+
|
|
267
|
+
Never take the customer from a tool argument or a custom header. A header set by a middleware doesn't reach tools, and one sent by the caller could be forged. The session token can't be.
|
|
268
|
+
|
|
269
|
+
### Limits
|
|
270
|
+
|
|
271
|
+
- **No rate limit on the token endpoint.** Put one in front of it (proxy or WAF) for public apps.
|
|
272
|
+
- **A captured ID token can be exchanged until it expires,** like any bearer token. Sessions last at most `endUserAccessTokenTtl`.
|
|
273
|
+
- **To end customer sessions,** deactivate or delete the LINE client, delete or regenerate its admin token, or revoke single sessions on the **MCP OAuth** page. Customer sessions have no "revoke every session approved by" button: their approver is the token's owner, so it would end every customer's session.
|
|
274
|
+
- **`identityProviders.line.verifyUrl`** is only for tests and local development, for example with the mock server in this plugin's repository (`node test/e2e/mock-line-verify.mjs`; it isn't in the npm package). Strapi logs a warning at startup while it's set. Never set it in production.
|
|
275
|
+
- **One LINE client can be active at a time.** Every LINE client accepts ID tokens from the one configured channel, so LINE clients are interchangeable: a customer could use any active one and get its admin token. The plugin refuses to create or re-activate a LINE client while another one is active. Deactivate or delete the other one first.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
177
279
|
## How it works
|
|
178
280
|
|
|
179
281
|
```mermaid
|
|
@@ -228,11 +330,18 @@ export default () => ({
|
|
|
228
330
|
dynamicClientRegistration: true, // false = only clients added in the admin panel
|
|
229
331
|
allowUserPermissions: false, // true = also offer "All of my permissions" on the consent page
|
|
230
332
|
cleanupIntervalMs: 3600000, // how often expired data is removed; 0 turns it off
|
|
333
|
+
identityProviders: {}, // customer sign-in, off by default; e.g. { line: { channelId: '1657000000' } }
|
|
334
|
+
endUserAccessTokenTtl: 3600, // seconds a customer session lasts (there is no refresh token)
|
|
231
335
|
},
|
|
232
336
|
},
|
|
233
337
|
});
|
|
234
338
|
```
|
|
235
339
|
|
|
340
|
+
`identityProviders.line` turns on [customer sign-in with LINE](#customer-sign-in-with-line):
|
|
341
|
+
|
|
342
|
+
- `channelId` (required): the channel ID of your LINE Login or LINE MINI App channel, digits only. Not the LIFF ID.
|
|
343
|
+
- `verifyUrl` (tests only): where to verify ID tokens instead of LINE's endpoint, such as a local mock server. Strapi logs a warning at startup while it's set.
|
|
344
|
+
|
|
236
345
|
The **MCP OAuth** admin page requires the **Manage MCP OAuth clients and grants** permission, on the **Plugins** tab of each role under **Settings → Administration Panel → Roles**. Super Admins have it by default.
|
|
237
346
|
|
|
238
347
|
---
|
|
@@ -262,6 +371,7 @@ The **MCP OAuth** admin page requires the **Manage MCP OAuth clients and grants*
|
|
|
262
371
|
| Sign-in page links use `http://` or the wrong host | Set `url` in `config/server.ts`, or `proxy: true` behind a reverse proxy. |
|
|
263
372
|
| "The redirect URI is not registered for this client" | For clients added in the admin panel, the redirect URI must match exactly. `*` matches any run of characters except `/`. |
|
|
264
373
|
| A client suddenly gets `401` | Its session was revoked, its token was deleted or regenerated, or the user was deactivated. Reconnect. |
|
|
374
|
+
| Customer sign-in answers `temporarily_unavailable` | The Strapi log says why: LINE couldn't be reached, or the LINE client needs a working admin token (the log names the client and the fix). |
|
|
265
375
|
|
|
266
376
|
---
|
|
267
377
|
|
|
@@ -289,6 +399,26 @@ End-to-end tests run against a development Strapi app that has the MCP server en
|
|
|
289
399
|
BASE=http://localhost:1337 ADMIN_EMAIL=you@example.com ADMIN_PASSWORD=... npm run test:e2e
|
|
290
400
|
```
|
|
291
401
|
|
|
402
|
+
The suites use the test app's Article type with the fields in `FIELDS` (default `title,body`); set `FIELDS=title,description`, for example, if your Article type has different fields. `SUBJECT` (default `api::article.article`) sets the content type those permissions are for.
|
|
403
|
+
|
|
404
|
+
The LINE suite (`test/e2e/line-exchange.mjs`) starts a mock LINE verify server on port 4545, so keep that port free. It creates its own LINE client, so no other LINE client may be active in the test app. The test app needs customer sign-in pointed at the mock. Have its `config/plugins.ts` read two variables:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
'strapi-oauth-mcp-manager': {
|
|
408
|
+
config: {
|
|
409
|
+
identityProviders: env('LINE_LOGIN_CHANNEL_ID')
|
|
410
|
+
? { line: { channelId: env('LINE_LOGIN_CHANNEL_ID'), verifyUrl: env('LINE_VERIFY_URL') } }
|
|
411
|
+
: {},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
and start the app with them set:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
LINE_LOGIN_CHANNEL_ID=1234567890 LINE_VERIFY_URL=http://localhost:4545/verify npm run develop
|
|
420
|
+
```
|
|
421
|
+
|
|
292
422
|
The tests sign in many times, so turn off the admin login rate limit in that app (`rateLimit: { enabled: false }` in `config/admin.ts`). They also create users, tokens and sessions and change the Editor and Author roles temporarily, so don't point them at production.
|
|
293
423
|
|
|
294
424
|
---
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useFetchClient, useNotification, Page, Layouts, ConfirmDialog } from "@strapi/strapi/admin";
|
|
3
3
|
import { Routes, Route } from "react-router-dom";
|
|
4
4
|
import { useState, useCallback, useEffect } from "react";
|
|
5
|
-
import { Alert, Button, Flex, Typography, EmptyStateLayout, Table, Thead, Tr, Th, Tbody, Td, Badge, IconButton, Box, Switch, Dialog, SingleSelect, SingleSelectOption, Modal, Field, TextInput,
|
|
5
|
+
import { Alert, Button, Flex, Typography, EmptyStateLayout, Table, Thead, Tr, Th, Tbody, Td, Badge, IconButton, Box, Switch, Dialog, SingleSelect, SingleSelectOption, Modal, Field, TextInput, Radio, Textarea } from "@strapi/design-system";
|
|
6
6
|
import { Trash, User, WarningCircle, Duplicate, Plus } from "@strapi/icons";
|
|
7
|
-
import { P as PLUGIN_ID } from "./index-
|
|
7
|
+
import { P as PLUGIN_ID } from "./index-B36PlzpY.mjs";
|
|
8
8
|
const PICK_ON_CONNECT = "pick";
|
|
9
9
|
const formatDate = (value) => value ? new Date(value).toLocaleString() : "—";
|
|
10
10
|
const Section = ({ title, subtitle, action, children }) => /* @__PURE__ */ jsxs(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: [
|
|
@@ -41,18 +41,20 @@ const TokenSelect = ({
|
|
|
41
41
|
value,
|
|
42
42
|
current,
|
|
43
43
|
onChange,
|
|
44
|
-
disabled
|
|
44
|
+
disabled,
|
|
45
|
+
allowPickOnConnect = true
|
|
45
46
|
}) => {
|
|
46
47
|
const ownsCurrent = current && tokens.some((t) => t.id === current.id);
|
|
47
48
|
return /* @__PURE__ */ jsxs(
|
|
48
49
|
SingleSelect,
|
|
49
50
|
{
|
|
50
51
|
"aria-label": "Admin token",
|
|
51
|
-
|
|
52
|
+
placeholder: "Choose one of your admin tokens",
|
|
53
|
+
value: !allowPickOnConnect && value === PICK_ON_CONNECT ? null : value,
|
|
52
54
|
disabled,
|
|
53
55
|
onChange: (next) => onChange(String(next)),
|
|
54
56
|
children: [
|
|
55
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: PICK_ON_CONNECT, children: "Picked when connecting" }),
|
|
57
|
+
allowPickOnConnect && /* @__PURE__ */ jsx(SingleSelectOption, { value: PICK_ON_CONNECT, children: "Picked when connecting" }),
|
|
56
58
|
current && !ownsCurrent && /* @__PURE__ */ jsx(SingleSelectOption, { value: String(current.id), disabled: true, children: current.missing ? "Deleted token" : `${current.name} (owned by ${current.ownerEmail ?? "another admin"})` }),
|
|
57
59
|
tokens.map((token) => /* @__PURE__ */ jsx(SingleSelectOption, { value: String(token.id), children: token.name }, token.id))
|
|
58
60
|
]
|
|
@@ -67,6 +69,7 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
67
69
|
const [redirectUris, setRedirectUris] = useState("");
|
|
68
70
|
const [confidential, setConfidential] = useState("confidential");
|
|
69
71
|
const [adminTokenId, setAdminTokenId] = useState(PICK_ON_CONNECT);
|
|
72
|
+
const [signIn, setSignIn] = useState("none");
|
|
70
73
|
const [created, setCreated] = useState(null);
|
|
71
74
|
const [submitting, setSubmitting] = useState(false);
|
|
72
75
|
const reset = () => {
|
|
@@ -74,15 +77,18 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
74
77
|
setRedirectUris("");
|
|
75
78
|
setConfidential("confidential");
|
|
76
79
|
setAdminTokenId(PICK_ON_CONNECT);
|
|
80
|
+
setSignIn("none");
|
|
77
81
|
setCreated(null);
|
|
78
82
|
};
|
|
79
83
|
const submit = async () => {
|
|
80
84
|
setSubmitting(true);
|
|
81
85
|
try {
|
|
86
|
+
const line = signIn === "line";
|
|
82
87
|
const { data } = await post(`/${PLUGIN_ID}/clients`, {
|
|
83
88
|
name,
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
endUserProvider: signIn,
|
|
90
|
+
redirectUris: line ? [] : redirectUris.split("\n").map((uri) => uri.trim()).filter(Boolean),
|
|
91
|
+
confidential: line ? false : confidential === "confidential",
|
|
86
92
|
adminTokenId: adminTokenId === PICK_ON_CONNECT ? null : Number(adminTokenId)
|
|
87
93
|
});
|
|
88
94
|
setCreated(data.data);
|
|
@@ -110,12 +116,20 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
110
116
|
/* @__PURE__ */ jsx(CopyValue, { label: "Client ID", value: created.clientId }),
|
|
111
117
|
created.clientSecret && /* @__PURE__ */ jsx(CopyValue, { label: "Client secret", value: created.clientSecret })
|
|
112
118
|
] }) : /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
113
|
-
/* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "Only needed for clients that ask for a client ID and secret, like ChatGPT connectors. Claude and most MCP clients register themselves automatically." }),
|
|
119
|
+
/* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "Only needed for clients that ask for a client ID and secret, like ChatGPT connectors, or for a customer app that signs people in with LINE. Claude and most MCP clients register themselves automatically." }),
|
|
114
120
|
/* @__PURE__ */ jsxs(Field.Root, { name: "name", required: true, children: [
|
|
115
121
|
/* @__PURE__ */ jsx(Field.Label, { children: "Name" }),
|
|
116
|
-
/* @__PURE__ */ jsx(TextInput, { value: name, onChange: (e) => setName(e.target.value), placeholder: "ChatGPT" })
|
|
122
|
+
/* @__PURE__ */ jsx(TextInput, { value: name, onChange: (e) => setName(e.target.value), placeholder: signIn === "line" ? "Customer app" : "ChatGPT" })
|
|
117
123
|
] }),
|
|
118
|
-
/* @__PURE__ */ jsxs(Field.Root, { name: "
|
|
124
|
+
/* @__PURE__ */ jsxs(Field.Root, { name: "signIn", hint: "LINE: customers sign in to your LINE app, which exchanges their LINE ID token for a session. No consent page is shown.", children: [
|
|
125
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Customer sign-in" }),
|
|
126
|
+
/* @__PURE__ */ jsxs(Radio.Group, { value: signIn, onValueChange: (value) => setSignIn(value), "aria-label": "Customer sign-in", children: [
|
|
127
|
+
/* @__PURE__ */ jsx(Radio.Item, { value: "none", children: "None: people connect with their Strapi admin account" }),
|
|
128
|
+
/* @__PURE__ */ jsx(Radio.Item, { value: "line", children: "LINE: customers sign in with LINE" })
|
|
129
|
+
] }),
|
|
130
|
+
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
131
|
+
] }),
|
|
132
|
+
signIn === "none" && /* @__PURE__ */ jsxs(Field.Root, { name: "redirectUris", required: true, hint: "One per line. * matches any run of characters except /.", children: [
|
|
119
133
|
/* @__PURE__ */ jsx(Field.Label, { children: "Redirect URIs" }),
|
|
120
134
|
/* @__PURE__ */ jsx(
|
|
121
135
|
Textarea,
|
|
@@ -131,15 +145,16 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
131
145
|
Field.Root,
|
|
132
146
|
{
|
|
133
147
|
name: "adminToken",
|
|
134
|
-
|
|
148
|
+
required: signIn === "line",
|
|
149
|
+
hint: signIn === "line" ? "Required. Every customer session runs with this token's permissions, so grant it only what customers need." : "Map one of your admin tokens so every connection uses it. Only you can then approve this client. Leave it to let each person pick one of their own tokens.",
|
|
135
150
|
children: [
|
|
136
151
|
/* @__PURE__ */ jsx(Field.Label, { children: "Admin token" }),
|
|
137
|
-
/* @__PURE__ */ jsx(TokenSelect, { tokens, value: adminTokenId, onChange: setAdminTokenId }),
|
|
152
|
+
/* @__PURE__ */ jsx(TokenSelect, { tokens, value: adminTokenId, onChange: setAdminTokenId, allowPickOnConnect: signIn === "none" }),
|
|
138
153
|
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
139
154
|
]
|
|
140
155
|
}
|
|
141
156
|
),
|
|
142
|
-
/* @__PURE__ */ jsxs(Field.Root, { name: "type", children: [
|
|
157
|
+
signIn === "none" && /* @__PURE__ */ jsxs(Field.Root, { name: "type", children: [
|
|
143
158
|
/* @__PURE__ */ jsx(Field.Label, { children: "Client type" }),
|
|
144
159
|
/* @__PURE__ */ jsxs(Radio.Group, { value: confidential, onValueChange: setConfidential, "aria-label": "Client type", children: [
|
|
145
160
|
/* @__PURE__ */ jsx(Radio.Item, { value: "confidential", children: "Confidential (client ID + secret)" }),
|
|
@@ -149,7 +164,7 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
149
164
|
] }) }),
|
|
150
165
|
/* @__PURE__ */ jsxs(Modal.Footer, { children: [
|
|
151
166
|
/* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: created ? "Done" : "Cancel" }) }),
|
|
152
|
-
!created && /* @__PURE__ */ jsx(Button, { onClick: submit, loading: submitting, disabled: !name.trim() || !redirectUris.trim(), children: "Create" })
|
|
167
|
+
!created && /* @__PURE__ */ jsx(Button, { onClick: submit, loading: submitting, disabled: !name.trim() || (signIn === "line" ? adminTokenId === PICK_ON_CONNECT : !redirectUris.trim()), children: "Create" })
|
|
153
168
|
] })
|
|
154
169
|
] })
|
|
155
170
|
]
|
|
@@ -243,7 +258,16 @@ const HomePage = () => {
|
|
|
243
258
|
/* @__PURE__ */ jsx(CopyValue, { label: "Authorization server metadata", value: overview.endpoints.authorizationServerMetadata }),
|
|
244
259
|
/* @__PURE__ */ jsx(CopyValue, { label: "Authorization endpoint", value: overview.endpoints.authorization }),
|
|
245
260
|
/* @__PURE__ */ jsx(CopyValue, { label: "Token endpoint", value: overview.endpoints.token }),
|
|
246
|
-
/* @__PURE__ */ jsxs(
|
|
261
|
+
overview.lineSignIn.configured ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
262
|
+
/* @__PURE__ */ jsx(CopyValue, { label: "Token exchange grant type", value: overview.tokenExchange.grantType }),
|
|
263
|
+
/* @__PURE__ */ jsx(CopyValue, { label: "Subject token type (LINE ID token)", value: overview.tokenExchange.subjectTokenType }),
|
|
264
|
+
/* @__PURE__ */ jsxs(Typography, { tag: "p", variant: "pi", textColor: "neutral600", children: [
|
|
265
|
+
"LINE sign-in is on for channel ",
|
|
266
|
+
overview.lineSignIn.channelId,
|
|
267
|
+
". Customer apps post liff.getIDToken() to the token endpoint with a LINE client's ID."
|
|
268
|
+
] })
|
|
269
|
+
] }) : clients.some((client) => client.endUserProvider === "line") && /* @__PURE__ */ jsx(Alert, { variant: "warning", title: "LINE sign-in is off", closeLabel: "Close", children: "A LINE client exists, but identityProviders.line.channelId isn't set in this plugin's config." }),
|
|
270
|
+
/* @__PURE__ */ jsxs(Typography, { tag: "p", variant: "pi", textColor: "neutral600", children: [
|
|
247
271
|
"Dynamic client registration is ",
|
|
248
272
|
overview.dynamicClientRegistration ? "on" : "off",
|
|
249
273
|
"."
|
|
@@ -254,9 +278,10 @@ const HomePage = () => {
|
|
|
254
278
|
{
|
|
255
279
|
title: "Connected sessions",
|
|
256
280
|
subtitle: "Each session uses an admin token owned by the person who approved it. Deactivating that person, or deleting or regenerating the token in Settings → Admin Tokens, also ends the session.",
|
|
257
|
-
children: grants.length === 0 ? /* @__PURE__ */ jsx(EmptyStateLayout, { content: "No MCP clients are connected yet." }) : /* @__PURE__ */ jsxs(Table, { colCount:
|
|
281
|
+
children: grants.length === 0 ? /* @__PURE__ */ jsx(EmptyStateLayout, { content: "No MCP clients are connected yet." }) : /* @__PURE__ */ jsxs(Table, { colCount: 7, rowCount: grants.length + 1, children: [
|
|
258
282
|
/* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
|
|
259
283
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Client" }) }),
|
|
284
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Customer" }) }),
|
|
260
285
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Approved by" }) }),
|
|
261
286
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Access" }) }),
|
|
262
287
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Last used" }) }),
|
|
@@ -265,6 +290,7 @@ const HomePage = () => {
|
|
|
265
290
|
] }) }),
|
|
266
291
|
/* @__PURE__ */ jsx(Tbody, { children: grants.map((grant) => /* @__PURE__ */ jsxs(Tr, { children: [
|
|
267
292
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: grant.clientName }) }),
|
|
293
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: grant.subject ?? "—" }) }),
|
|
268
294
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
269
295
|
/* @__PURE__ */ jsx(Typography, { children: grant.userEmail ?? "—" }),
|
|
270
296
|
!grant.userActive && /* @__PURE__ */ jsx(Badge, { variant: "danger", children: "Inactive" })
|
|
@@ -282,7 +308,7 @@ const HomePage = () => {
|
|
|
282
308
|
children: /* @__PURE__ */ jsx(Trash, {})
|
|
283
309
|
}
|
|
284
310
|
),
|
|
285
|
-
/* @__PURE__ */ jsx(
|
|
311
|
+
!grant.subject && /* @__PURE__ */ jsx(
|
|
286
312
|
IconButton,
|
|
287
313
|
{
|
|
288
314
|
label: `Revoke every session approved by ${grant.userEmail ?? "this user"}`,
|
|
@@ -323,15 +349,17 @@ const HomePage = () => {
|
|
|
323
349
|
] }) }),
|
|
324
350
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
|
|
325
351
|
/* @__PURE__ */ jsx(Badge, { variant: client.registrationType === "dynamic" ? "secondary" : "primary", children: client.registrationType === "dynamic" ? "Self-registered" : "Manual" }),
|
|
326
|
-
/* @__PURE__ */ jsx(Badge, { children: client.tokenEndpointAuthMethod === "none" ? "Public" : "Confidential" })
|
|
352
|
+
/* @__PURE__ */ jsx(Badge, { children: client.tokenEndpointAuthMethod === "none" ? "Public" : "Confidential" }),
|
|
353
|
+
client.endUserProvider === "line" && /* @__PURE__ */ jsx(Badge, { variant: "success", children: "LINE sign-in" })
|
|
327
354
|
] }) }),
|
|
328
|
-
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", style: { wordBreak: "break-all" }, children: client.redirectUris.join(", ") }) }),
|
|
355
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", style: { wordBreak: "break-all" }, children: client.redirectUris.length ? client.redirectUris.join(", ") : "—" }) }),
|
|
329
356
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Box, { minWidth: "220px", children: /* @__PURE__ */ jsx(
|
|
330
357
|
TokenSelect,
|
|
331
358
|
{
|
|
332
359
|
tokens,
|
|
333
360
|
current: client.adminToken,
|
|
334
361
|
value: client.adminToken ? String(client.adminToken.id) : PICK_ON_CONNECT,
|
|
362
|
+
allowPickOnConnect: client.endUserProvider !== "line",
|
|
335
363
|
onChange: (next) => run(
|
|
336
364
|
() => put(`/${PLUGIN_ID}/clients/${client.id}`, {
|
|
337
365
|
adminTokenId: next === PICK_ON_CONNECT ? null : Number(next)
|
|
@@ -6,7 +6,7 @@ const reactRouterDom = require("react-router-dom");
|
|
|
6
6
|
const react = require("react");
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const icons = require("@strapi/icons");
|
|
9
|
-
const index = require("./index-
|
|
9
|
+
const index = require("./index-dzfso1OG.js");
|
|
10
10
|
const PICK_ON_CONNECT = "pick";
|
|
11
11
|
const formatDate = (value) => value ? new Date(value).toLocaleString() : "—";
|
|
12
12
|
const Section = ({ title, subtitle, action, children }) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 6, children: [
|
|
@@ -43,18 +43,20 @@ const TokenSelect = ({
|
|
|
43
43
|
value,
|
|
44
44
|
current,
|
|
45
45
|
onChange,
|
|
46
|
-
disabled
|
|
46
|
+
disabled,
|
|
47
|
+
allowPickOnConnect = true
|
|
47
48
|
}) => {
|
|
48
49
|
const ownsCurrent = current && tokens.some((t) => t.id === current.id);
|
|
49
50
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
50
51
|
designSystem.SingleSelect,
|
|
51
52
|
{
|
|
52
53
|
"aria-label": "Admin token",
|
|
53
|
-
|
|
54
|
+
placeholder: "Choose one of your admin tokens",
|
|
55
|
+
value: !allowPickOnConnect && value === PICK_ON_CONNECT ? null : value,
|
|
54
56
|
disabled,
|
|
55
57
|
onChange: (next) => onChange(String(next)),
|
|
56
58
|
children: [
|
|
57
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: PICK_ON_CONNECT, children: "Picked when connecting" }),
|
|
59
|
+
allowPickOnConnect && /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: PICK_ON_CONNECT, children: "Picked when connecting" }),
|
|
58
60
|
current && !ownsCurrent && /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: String(current.id), disabled: true, children: current.missing ? "Deleted token" : `${current.name} (owned by ${current.ownerEmail ?? "another admin"})` }),
|
|
59
61
|
tokens.map((token) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: String(token.id), children: token.name }, token.id))
|
|
60
62
|
]
|
|
@@ -69,6 +71,7 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
69
71
|
const [redirectUris, setRedirectUris] = react.useState("");
|
|
70
72
|
const [confidential, setConfidential] = react.useState("confidential");
|
|
71
73
|
const [adminTokenId, setAdminTokenId] = react.useState(PICK_ON_CONNECT);
|
|
74
|
+
const [signIn, setSignIn] = react.useState("none");
|
|
72
75
|
const [created, setCreated] = react.useState(null);
|
|
73
76
|
const [submitting, setSubmitting] = react.useState(false);
|
|
74
77
|
const reset = () => {
|
|
@@ -76,15 +79,18 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
76
79
|
setRedirectUris("");
|
|
77
80
|
setConfidential("confidential");
|
|
78
81
|
setAdminTokenId(PICK_ON_CONNECT);
|
|
82
|
+
setSignIn("none");
|
|
79
83
|
setCreated(null);
|
|
80
84
|
};
|
|
81
85
|
const submit = async () => {
|
|
82
86
|
setSubmitting(true);
|
|
83
87
|
try {
|
|
88
|
+
const line = signIn === "line";
|
|
84
89
|
const { data } = await post(`/${index.PLUGIN_ID}/clients`, {
|
|
85
90
|
name,
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
endUserProvider: signIn,
|
|
92
|
+
redirectUris: line ? [] : redirectUris.split("\n").map((uri) => uri.trim()).filter(Boolean),
|
|
93
|
+
confidential: line ? false : confidential === "confidential",
|
|
88
94
|
adminTokenId: adminTokenId === PICK_ON_CONNECT ? null : Number(adminTokenId)
|
|
89
95
|
});
|
|
90
96
|
setCreated(data.data);
|
|
@@ -112,12 +118,20 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
112
118
|
/* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Client ID", value: created.clientId }),
|
|
113
119
|
created.clientSecret && /* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Client secret", value: created.clientSecret })
|
|
114
120
|
] }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
115
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Only needed for clients that ask for a client ID and secret, like ChatGPT connectors. Claude and most MCP clients register themselves automatically." }),
|
|
121
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Only needed for clients that ask for a client ID and secret, like ChatGPT connectors, or for a customer app that signs people in with LINE. Claude and most MCP clients register themselves automatically." }),
|
|
116
122
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "name", required: true, children: [
|
|
117
123
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Name" }),
|
|
118
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.TextInput, { value: name, onChange: (e) => setName(e.target.value), placeholder: "ChatGPT" })
|
|
124
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.TextInput, { value: name, onChange: (e) => setName(e.target.value), placeholder: signIn === "line" ? "Customer app" : "ChatGPT" })
|
|
119
125
|
] }),
|
|
120
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "
|
|
126
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "signIn", hint: "LINE: customers sign in to your LINE app, which exchanges their LINE ID token for a session. No consent page is shown.", children: [
|
|
127
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Customer sign-in" }),
|
|
128
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Radio.Group, { value: signIn, onValueChange: (value) => setSignIn(value), "aria-label": "Customer sign-in", children: [
|
|
129
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Radio.Item, { value: "none", children: "None: people connect with their Strapi admin account" }),
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Radio.Item, { value: "line", children: "LINE: customers sign in with LINE" })
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
133
|
+
] }),
|
|
134
|
+
signIn === "none" && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "redirectUris", required: true, hint: "One per line. * matches any run of characters except /.", children: [
|
|
121
135
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Redirect URIs" }),
|
|
122
136
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
123
137
|
designSystem.Textarea,
|
|
@@ -133,15 +147,16 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
133
147
|
designSystem.Field.Root,
|
|
134
148
|
{
|
|
135
149
|
name: "adminToken",
|
|
136
|
-
|
|
150
|
+
required: signIn === "line",
|
|
151
|
+
hint: signIn === "line" ? "Required. Every customer session runs with this token's permissions, so grant it only what customers need." : "Map one of your admin tokens so every connection uses it. Only you can then approve this client. Leave it to let each person pick one of their own tokens.",
|
|
137
152
|
children: [
|
|
138
153
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Admin token" }),
|
|
139
|
-
/* @__PURE__ */ jsxRuntime.jsx(TokenSelect, { tokens, value: adminTokenId, onChange: setAdminTokenId }),
|
|
154
|
+
/* @__PURE__ */ jsxRuntime.jsx(TokenSelect, { tokens, value: adminTokenId, onChange: setAdminTokenId, allowPickOnConnect: signIn === "none" }),
|
|
140
155
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
141
156
|
]
|
|
142
157
|
}
|
|
143
158
|
),
|
|
144
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "type", children: [
|
|
159
|
+
signIn === "none" && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "type", children: [
|
|
145
160
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Client type" }),
|
|
146
161
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Radio.Group, { value: confidential, onValueChange: setConfidential, "aria-label": "Client type", children: [
|
|
147
162
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Radio.Item, { value: "confidential", children: "Confidential (client ID + secret)" }),
|
|
@@ -151,7 +166,7 @@ const CreateClientModal = ({ onCreated, tokens }) => {
|
|
|
151
166
|
] }) }),
|
|
152
167
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Footer, { children: [
|
|
153
168
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Close, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: created ? "Done" : "Cancel" }) }),
|
|
154
|
-
!created && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: submit, loading: submitting, disabled: !name.trim() || !redirectUris.trim(), children: "Create" })
|
|
169
|
+
!created && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: submit, loading: submitting, disabled: !name.trim() || (signIn === "line" ? adminTokenId === PICK_ON_CONNECT : !redirectUris.trim()), children: "Create" })
|
|
155
170
|
] })
|
|
156
171
|
] })
|
|
157
172
|
]
|
|
@@ -245,7 +260,16 @@ const HomePage = () => {
|
|
|
245
260
|
/* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Authorization server metadata", value: overview.endpoints.authorizationServerMetadata }),
|
|
246
261
|
/* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Authorization endpoint", value: overview.endpoints.authorization }),
|
|
247
262
|
/* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Token endpoint", value: overview.endpoints.token }),
|
|
248
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
263
|
+
overview.lineSignIn.configured ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
264
|
+
/* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Token exchange grant type", value: overview.tokenExchange.grantType }),
|
|
265
|
+
/* @__PURE__ */ jsxRuntime.jsx(CopyValue, { label: "Subject token type (LINE ID token)", value: overview.tokenExchange.subjectTokenType }),
|
|
266
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { tag: "p", variant: "pi", textColor: "neutral600", children: [
|
|
267
|
+
"LINE sign-in is on for channel ",
|
|
268
|
+
overview.lineSignIn.channelId,
|
|
269
|
+
". Customer apps post liff.getIDToken() to the token endpoint with a LINE client's ID."
|
|
270
|
+
] })
|
|
271
|
+
] }) : clients.some((client) => client.endUserProvider === "line") && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Alert, { variant: "warning", title: "LINE sign-in is off", closeLabel: "Close", children: "A LINE client exists, but identityProviders.line.channelId isn't set in this plugin's config." }),
|
|
272
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { tag: "p", variant: "pi", textColor: "neutral600", children: [
|
|
249
273
|
"Dynamic client registration is ",
|
|
250
274
|
overview.dynamicClientRegistration ? "on" : "off",
|
|
251
275
|
"."
|
|
@@ -256,9 +280,10 @@ const HomePage = () => {
|
|
|
256
280
|
{
|
|
257
281
|
title: "Connected sessions",
|
|
258
282
|
subtitle: "Each session uses an admin token owned by the person who approved it. Deactivating that person, or deleting or regenerating the token in Settings → Admin Tokens, also ends the session.",
|
|
259
|
-
children: grants.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.EmptyStateLayout, { content: "No MCP clients are connected yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount:
|
|
283
|
+
children: grants.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.EmptyStateLayout, { content: "No MCP clients are connected yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount: 7, rowCount: grants.length + 1, children: [
|
|
260
284
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Thead, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
|
|
261
285
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Client" }) }),
|
|
286
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Customer" }) }),
|
|
262
287
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Approved by" }) }),
|
|
263
288
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Access" }) }),
|
|
264
289
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Last used" }) }),
|
|
@@ -267,6 +292,7 @@ const HomePage = () => {
|
|
|
267
292
|
] }) }),
|
|
268
293
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: grants.map((grant) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
|
|
269
294
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "semiBold", children: grant.clientName }) }),
|
|
295
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: grant.subject ?? "—" }) }),
|
|
270
296
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
271
297
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: grant.userEmail ?? "—" }),
|
|
272
298
|
!grant.userActive && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: "danger", children: "Inactive" })
|
|
@@ -284,7 +310,7 @@ const HomePage = () => {
|
|
|
284
310
|
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
|
|
285
311
|
}
|
|
286
312
|
),
|
|
287
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
313
|
+
!grant.subject && /* @__PURE__ */ jsxRuntime.jsx(
|
|
288
314
|
designSystem.IconButton,
|
|
289
315
|
{
|
|
290
316
|
label: `Revoke every session approved by ${grant.userEmail ?? "this user"}`,
|
|
@@ -325,15 +351,17 @@ const HomePage = () => {
|
|
|
325
351
|
] }) }),
|
|
326
352
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
|
|
327
353
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: client.registrationType === "dynamic" ? "secondary" : "primary", children: client.registrationType === "dynamic" ? "Self-registered" : "Manual" }),
|
|
328
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { children: client.tokenEndpointAuthMethod === "none" ? "Public" : "Confidential" })
|
|
354
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { children: client.tokenEndpointAuthMethod === "none" ? "Public" : "Confidential" }),
|
|
355
|
+
client.endUserProvider === "line" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: "success", children: "LINE sign-in" })
|
|
329
356
|
] }) }),
|
|
330
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", style: { wordBreak: "break-all" }, children: client.redirectUris.join(", ") }) }),
|
|
357
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", style: { wordBreak: "break-all" }, children: client.redirectUris.length ? client.redirectUris.join(", ") : "—" }) }),
|
|
331
358
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { minWidth: "220px", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
332
359
|
TokenSelect,
|
|
333
360
|
{
|
|
334
361
|
tokens,
|
|
335
362
|
current: client.adminToken,
|
|
336
363
|
value: client.adminToken ? String(client.adminToken.id) : PICK_ON_CONNECT,
|
|
364
|
+
allowPickOnConnect: client.endUserProvider !== "line",
|
|
337
365
|
onChange: (next) => run(
|
|
338
366
|
() => put(`/${index.PLUGIN_ID}/clients/${client.id}`, {
|
|
339
367
|
adminTokenId: next === PICK_ON_CONNECT ? null : Number(next)
|
|
@@ -37,7 +37,7 @@ const index = {
|
|
|
37
37
|
},
|
|
38
38
|
permissions: [{ action: `plugin::${PLUGIN_ID}.manage`, subject: null }],
|
|
39
39
|
Component: async () => {
|
|
40
|
-
const { App } = await import("./App-
|
|
40
|
+
const { App } = await import("./App-BLcSXikb.mjs");
|
|
41
41
|
return App;
|
|
42
42
|
}
|
|
43
43
|
});
|
|
@@ -38,7 +38,7 @@ const index = {
|
|
|
38
38
|
},
|
|
39
39
|
permissions: [{ action: `plugin::${PLUGIN_ID}.manage`, subject: null }],
|
|
40
40
|
Component: async () => {
|
|
41
|
-
const { App } = await Promise.resolve().then(() => require("./App-
|
|
41
|
+
const { App } = await Promise.resolve().then(() => require("./App-CxPrn10p.js"));
|
|
42
42
|
return App;
|
|
43
43
|
}
|
|
44
44
|
});
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED