@softure-ai/mcp-access 0.2.4 → 0.2.5
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 +14 -0
- package/README.md +31 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +3 -2
- package/dist/next/index.js.map +1 -1
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +5 -56
- package/dist/next/pages.js.map +1 -1
- package/dist/next/token-manager-props.d.ts +18 -0
- package/dist/next/token-manager-props.d.ts.map +1 -0
- package/dist/next/token-manager-props.js +74 -0
- package/dist/next/token-manager-props.js.map +1 -0
- package/dist/ui/token-manager.d.ts.map +1 -1
- package/dist/ui/token-manager.js +18 -3
- package/dist/ui/token-manager.js.map +1 -1
- package/module.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/next/index.ts +3 -2
- package/src/next/pages.tsx +7 -68
- package/src/next/token-manager-props.ts +92 -0
- package/src/ui/token-manager.tsx +21 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
|
|
|
4
4
|
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
5
|
"Definition of done"). Versions before the first one below are described in their GitHub Releases (`mcp-access@x.y.z`).
|
|
6
6
|
|
|
7
|
+
## 0.2.5
|
|
8
|
+
|
|
9
|
+
- `TokenManager` keeps keyboard focus (#590): when the issued token appears, focus moves to its heading
|
|
10
|
+
(`tabIndex={-1}`), so a screen reader announces the token that is shown only once; "Done, hide the token" returns
|
|
11
|
+
focus to the issue form's name field instead of dropping it to `<body>`. Migrate: nothing to change; an app that
|
|
12
|
+
moved focus itself around `TokenManager` can drop that code.
|
|
13
|
+
- `getTokenManagerProps({ userId, config?, now? })` in `/next` (#589): every data prop of `TokenManager` (`tools`,
|
|
14
|
+
`tokens`, `grants` while OAuth is on, `allowWrites`, `maxTokens`, `messages`, `locale`), built exactly as
|
|
15
|
+
`McpAccessPage` builds them, which now renders through it. For an app that composes its own page around
|
|
16
|
+
`TokenManager` (its own `<h1>`, its own frame). It checks no session: pass the signed-in user's id. Migrate: an app
|
|
17
|
+
that copied the page's builders (`toRow`, `toGrant`, `toTool`, `describeStatus` and the date formatting) deletes
|
|
18
|
+
them and spreads `await getTokenManagerProps({ userId: user.id })` into `TokenManager`, adding only the actions
|
|
19
|
+
(`revokeGrantAction` only when `grants` is present). Its output is unchanged.
|
|
20
|
+
|
|
7
21
|
## 0.2.4
|
|
8
22
|
|
|
9
23
|
- `stdio/register` works for CommonJS (#571): it resolved `server-only` to a `data:` module, which an ES `import`
|
package/README.md
CHANGED
|
@@ -271,7 +271,37 @@ app that renders the expiry itself.
|
|
|
271
271
|
|
|
272
272
|
`TokenManager` (`@softure-ai/mcp-access/ui`) takes the actions `issueTokenAction` and
|
|
273
273
|
`revokeTokenAction` (`/next`) as props for an app that composes its own page, and `grants` with
|
|
274
|
-
`revokeGrantAction` for the connected apps list.
|
|
274
|
+
`revokeGrantAction` for the connected apps list. It manages focus itself: the issued token's heading takes focus
|
|
275
|
+
when it appears, and "Done" returns focus to the name field.
|
|
276
|
+
`getTokenManagerProps({ userId })` (`/next`) builds every other
|
|
277
|
+
prop exactly as `McpAccessPage` does: `tools`, `tokens` (status, scope, created, expiry and last-use lines in the
|
|
278
|
+
config's locale and zone), `grants` only while `oauth.enabled`, `allowWrites`, `maxTokens`, `messages` and `locale`.
|
|
279
|
+
It checks no session: pass the signed-in user's id from the app's own check. For example, a page with its own `<h1>`:
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// app/account/mcp/page.tsx
|
|
283
|
+
import { requireUser } from "@softure-ai/auth/next";
|
|
284
|
+
import { getTokenManagerProps, issueTokenAction, revokeGrantAction, revokeTokenAction } from "@softure-ai/mcp-access/next";
|
|
285
|
+
import { TokenManager } from "@softure-ai/mcp-access/ui";
|
|
286
|
+
|
|
287
|
+
export const dynamic = "force-dynamic";
|
|
288
|
+
|
|
289
|
+
export default async function Page() {
|
|
290
|
+
const user = await requireUser({ next: "/account/mcp" });
|
|
291
|
+
const props = await getTokenManagerProps({ userId: user.id });
|
|
292
|
+
return (
|
|
293
|
+
<main>
|
|
294
|
+
<h1>{t("mcp.title")}</h1>
|
|
295
|
+
<TokenManager
|
|
296
|
+
{...props}
|
|
297
|
+
issueAction={issueTokenAction}
|
|
298
|
+
revokeAction={revokeTokenAction}
|
|
299
|
+
{...(props.grants === undefined ? {} : { revokeGrantAction })}
|
|
300
|
+
/>
|
|
301
|
+
</main>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
275
305
|
|
|
276
306
|
With `oauth.enabled`, mount the OAuth routes and the consent page too (the paths for the default
|
|
277
307
|
`routes`):
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export const MCP_RATE_LIMIT_BUCKETS = {
|
|
|
30
30
|
export const mcpAccess = defineModule({
|
|
31
31
|
manifest: {
|
|
32
32
|
id: MODULE_ID,
|
|
33
|
-
version: "0.2.
|
|
33
|
+
version: "0.2.5",
|
|
34
34
|
dependsOn: { security: "^0.2.0", auth: "^0.2.0", jobs: "^0.1.1?", ops: "^0.2.0?" },
|
|
35
35
|
dbSchema: "mcp",
|
|
36
36
|
tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window"],
|
package/dist/next/index.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export { getMcpAccessContext, getRequestOrigins } from "./context.js";
|
|
|
5
5
|
export { issueToken, refuseIssue, type BeforeIssueGate, type IssueGateInput, type IssueGateResult, type IssueTokenOptions } from "./issue-token.js";
|
|
6
6
|
export { McpAccessPage, type McpAccessPageProps } from "./pages.js";
|
|
7
7
|
export { createMcpRoute } from "./route.js";
|
|
8
|
+
export { getTokenManagerProps, type TokenManagerData, type TokenManagerPropsInput } from "./token-manager-props.js";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/next/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACL,oBAAoB,EACpB,6BAA6B,EAC7B,uBAAuB,EACvB,mCAAmC,EACnC,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACpJ,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACL,oBAAoB,EACpB,6BAA6B,EAC7B,uBAAuB,EACvB,mCAAmC,EACnC,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACpJ,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,KAAK,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/next/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The Next.js adapter of @softure-ai/mcp-access: the MCP route factory, the token page
|
|
2
|
-
// server actions, and the OAuth routes and consent page (docs/02-module-standard.md §8).
|
|
1
|
+
// The Next.js adapter of @softure-ai/mcp-access: the MCP route factory, the token page, its prop
|
|
2
|
+
// builder and its server actions, and the OAuth routes and consent page (docs/02-module-standard.md §8).
|
|
3
3
|
export { issueTokenAction, revokeGrantAction, revokeTokenAction } from "./actions.js";
|
|
4
4
|
export { OAuthConsentPage } from "./oauth-page.js";
|
|
5
5
|
export { answerOAuthPreflight, decideOAuthAuthorizationRoute, exchangeOAuthTokenRoute, getAuthorizationServerMetadataRoute, getProtectedResourceMetadataRoute, registerOAuthClientRoute, } from "./oauth-routes.js";
|
|
@@ -7,4 +7,5 @@ export { getMcpAccessContext, getRequestOrigins } from "./context.js";
|
|
|
7
7
|
export { issueToken, refuseIssue } from "./issue-token.js";
|
|
8
8
|
export { McpAccessPage } from "./pages.js";
|
|
9
9
|
export { createMcpRoute } from "./route.js";
|
|
10
|
+
export { getTokenManagerProps } from "./token-manager-props.js";
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
package/dist/next/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,yGAAyG;AACzG,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAA8B,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACL,oBAAoB,EACpB,6BAA6B,EAC7B,uBAAuB,EACvB,mCAAmC,EACnC,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,WAAW,EAA2F,MAAM,kBAAkB,CAAC;AACpJ,OAAO,EAAE,aAAa,EAA2B,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAsD,MAAM,0BAA0B,CAAC"}
|
package/dist/next/pages.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM7E,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC;CACzC;AAED,wBAAsB,aAAa,CAAC,EAAE,WAA8B,EAAE,GAAE,kBAAuB,wCAiB9F"}
|
package/dist/next/pages.js
CHANGED
|
@@ -2,71 +2,20 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
// The token page, ready to mount with one line:
|
|
3
3
|
// `export { McpAccessPage as default } from "@softure-ai/mcp-access/next"`.
|
|
4
4
|
// A server component for the signed-in user; without a session it redirects to the login page and
|
|
5
|
-
// back.
|
|
6
|
-
// app's locale and time zone, and never differ between the server render and hydration.
|
|
5
|
+
// back. Its props come from `getTokenManagerProps`, the same builder an app's own page uses.
|
|
7
6
|
import { requireUser } from "@softure-ai/auth/next";
|
|
8
|
-
import { formatMessage } from "@softure-ai/core";
|
|
9
7
|
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
10
8
|
import { Card } from "@softure-ai/ui";
|
|
11
|
-
import {
|
|
12
|
-
import { listOAuthGrants } from "../server/oauth.js";
|
|
13
|
-
import { listAccessTokens } from "../server/tokens.js";
|
|
14
|
-
import { getAccessTokenStatus } from "../token-status.js";
|
|
9
|
+
import { getMcpAccessMessages, getMcpAccessRoutes } from "../server/options.js";
|
|
15
10
|
import { TokenManager } from "../ui/token-manager.js";
|
|
16
11
|
import { issueTokenAction, revokeGrantAction, revokeTokenAction } from "./actions.js";
|
|
17
|
-
import {
|
|
18
|
-
import { formatDate, formatDateTime } from "./format.js";
|
|
12
|
+
import { getTokenManagerProps } from "./token-manager-props.js";
|
|
19
13
|
const LAYOUT_CLASS = "sft:mx-auto sft:box-border sft:w-full sft:sm:max-w-3xl sft:px-4 sft:py-4";
|
|
20
|
-
function describeStatus(status, messages) {
|
|
21
|
-
switch (status.kind) {
|
|
22
|
-
case "active":
|
|
23
|
-
return messages.list.active;
|
|
24
|
-
case "expired":
|
|
25
|
-
return messages.list.expired;
|
|
26
|
-
case "expiring":
|
|
27
|
-
if (status.daysLeft === 0)
|
|
28
|
-
return messages.list.expiresToday;
|
|
29
|
-
if (status.daysLeft === 1)
|
|
30
|
-
return messages.list.expiresTomorrow;
|
|
31
|
-
return formatMessage(messages.list.expiresInDays, { days: status.daysLeft });
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function toRow(token, now, config, options, messages) {
|
|
35
|
-
const status = getAccessTokenStatus(token.expiresAt, now, { timezone: config.timezone, warningDays: options.expiryWarningDays });
|
|
36
|
-
const isExpired = status.kind === "expired";
|
|
37
|
-
const expiry = formatMessage(isExpired ? messages.list.expiredOn : messages.list.expiresOn, { date: formatDate(config, token.expiresAt) });
|
|
38
|
-
const lastUse = token.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, token.lastUsedAt) });
|
|
39
|
-
return {
|
|
40
|
-
id: token.id,
|
|
41
|
-
name: token.name,
|
|
42
|
-
scopeText: token.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
43
|
-
statusText: describeStatus(status, messages),
|
|
44
|
-
isExpired,
|
|
45
|
-
details: [formatMessage(messages.list.createdOn, { date: formatDate(config, token.createdAt) }), expiry, lastUse],
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function toGrant(grant, config, options, messages) {
|
|
49
|
-
const lastUse = grant.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, grant.lastUsedAt) });
|
|
50
|
-
return {
|
|
51
|
-
id: grant.id,
|
|
52
|
-
clientName: grant.clientName,
|
|
53
|
-
scopeText: grant.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
54
|
-
details: [formatMessage(messages.grants.connectedOn, { date: formatDate(config, grant.createdAt) }), lastUse],
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function toTool(tool, config, options, messages) {
|
|
58
|
-
const accessText = tool.access === "read" ? messages.tools.read : options.allowWrites ? messages.tools.write : messages.tools.writeUnavailable;
|
|
59
|
-
return { name: tool.name, description: getLocalizedText(tool.description, config.locale), accessText };
|
|
60
|
-
}
|
|
61
14
|
export async function McpAccessPage({ issueAction = issueTokenAction } = {}) {
|
|
62
15
|
const config = getSoftureConfig();
|
|
63
16
|
const user = await requireUser({ next: getMcpAccessRoutes(config).page });
|
|
64
|
-
const options = getMcpAccessOptions(config);
|
|
65
17
|
const messages = getMcpAccessMessages(config);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const tokens = await listAccessTokens(ctx, user.id);
|
|
69
|
-
const grants = options.oauth.enabled ? await listOAuthGrants(ctx, user.id) : null;
|
|
70
|
-
return (_jsx("main", { className: LAYOUT_CLASS, children: _jsx(Card, { title: messages.page.title, subtitle: messages.page.lead, children: _jsx(TokenManager, { tools: options.tools.map((tool) => toTool(tool, config, options, messages)), tokens: tokens.map((token) => toRow(token, now, config, options, messages)), allowWrites: options.allowWrites, maxTokens: options.maxTokensPerUser, issueAction: issueAction, revokeAction: revokeTokenAction, ...(grants === null ? {} : { grants: grants.map((grant) => toGrant(grant, config, options, messages)), revokeGrantAction }), messages: messages, locale: config.locale }) }) }));
|
|
18
|
+
const props = await getTokenManagerProps({ userId: user.id, config });
|
|
19
|
+
return (_jsx("main", { className: LAYOUT_CLASS, children: _jsx(Card, { title: messages.page.title, subtitle: messages.page.lead, children: _jsx(TokenManager, { ...props, issueAction: issueAction, revokeAction: revokeTokenAction, ...(props.grants === undefined ? {} : { revokeGrantAction }) }) }) }));
|
|
71
20
|
}
|
|
72
21
|
//# sourceMappingURL=pages.js.map
|
package/dist/next/pages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":";AAAA,gDAAgD;AAChD,4EAA4E;AAC5E,kGAAkG;AAClG
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":";AAAA,gDAAgD;AAChD,4EAA4E;AAC5E,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,YAAY,EAAyB,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,YAAY,GAAG,0EAA0E,CAAC;AAUhG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAE,WAAW,GAAG,gBAAgB,KAAyB,EAAE;IAC7F,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,EAAE,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IACtE,OAAO,CACL,eAAM,SAAS,EAAE,YAAY,YAC3B,KAAC,IAAI,IAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,YAC5D,KAAC,YAAY,OACP,KAAK,EACT,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,iBAAiB,KAC3B,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,GAC7D,GACG,GACF,CACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { TokenManagerProps } from "../ui/token-manager.js";
|
|
3
|
+
/** Every prop of `TokenManager` but the actions and the styling. */
|
|
4
|
+
export type TokenManagerData = Pick<TokenManagerProps, "tools" | "tokens" | "grants" | "allowWrites" | "maxTokens" | "messages" | "locale">;
|
|
5
|
+
export interface TokenManagerPropsInput {
|
|
6
|
+
/** The signed-in user's id, from the app's session check: only this user's tokens and apps are listed. */
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
/** Default: the registered config (`getSoftureConfig()`). */
|
|
9
|
+
readonly config?: SoftureConfig;
|
|
10
|
+
/** The moment token statuses are computed for. Default: the module clock's now. */
|
|
11
|
+
readonly now?: Date;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The tools, the user's tokens and (while OAuth is on) connected apps, and the page settings, ready to
|
|
15
|
+
* spread into `TokenManager` next to the actions. Reads the database; it checks no session itself.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTokenManagerProps({ userId, config, now }: TokenManagerPropsInput): Promise<TokenManagerData>;
|
|
18
|
+
//# sourceMappingURL=token-manager-props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-manager-props.d.ts","sourceRoot":"","sources":["../../src/next/token-manager-props.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AASrE,OAAO,KAAK,EAAqB,iBAAiB,EAAqC,MAAM,wBAAwB,CAAC;AAItH,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC,CAAC;AAE5I,MAAM,WAAW,sBAAsB;IACrC,0GAA0G;IAC1G,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;CACrB;AA6CD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAA2B,EAAE,GAAG,EAAE,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB1I"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The data `TokenManager` shows, built on the server for one user: what `McpAccessPage` renders, for an
|
|
2
|
+
// app that composes its own page around `TokenManager`. Dates are formatted here, once, in the app's
|
|
3
|
+
// locale and time zone, so the server render and hydration never differ.
|
|
4
|
+
import { formatMessage } from "@softure-ai/core";
|
|
5
|
+
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
6
|
+
import { getLocalizedText, getMcpAccessMessages, getMcpAccessOptions } from "../server/options.js";
|
|
7
|
+
import { listOAuthGrants } from "../server/oauth.js";
|
|
8
|
+
import { listAccessTokens } from "../server/tokens.js";
|
|
9
|
+
import { getAccessTokenStatus } from "../token-status.js";
|
|
10
|
+
import { getMcpAccessContext } from "./context.js";
|
|
11
|
+
import { formatDate, formatDateTime } from "./format.js";
|
|
12
|
+
function describeStatus(status, messages) {
|
|
13
|
+
switch (status.kind) {
|
|
14
|
+
case "active":
|
|
15
|
+
return messages.list.active;
|
|
16
|
+
case "expired":
|
|
17
|
+
return messages.list.expired;
|
|
18
|
+
case "expiring":
|
|
19
|
+
if (status.daysLeft === 0)
|
|
20
|
+
return messages.list.expiresToday;
|
|
21
|
+
if (status.daysLeft === 1)
|
|
22
|
+
return messages.list.expiresTomorrow;
|
|
23
|
+
return formatMessage(messages.list.expiresInDays, { days: status.daysLeft });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function toRow(token, now, config, options, messages) {
|
|
27
|
+
const status = getAccessTokenStatus(token.expiresAt, now, { timezone: config.timezone, warningDays: options.expiryWarningDays });
|
|
28
|
+
const isExpired = status.kind === "expired";
|
|
29
|
+
const expiry = formatMessage(isExpired ? messages.list.expiredOn : messages.list.expiresOn, { date: formatDate(config, token.expiresAt) });
|
|
30
|
+
const lastUse = token.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, token.lastUsedAt) });
|
|
31
|
+
return {
|
|
32
|
+
id: token.id,
|
|
33
|
+
name: token.name,
|
|
34
|
+
scopeText: token.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
35
|
+
statusText: describeStatus(status, messages),
|
|
36
|
+
isExpired,
|
|
37
|
+
details: [formatMessage(messages.list.createdOn, { date: formatDate(config, token.createdAt) }), expiry, lastUse],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toGrant(grant, config, options, messages) {
|
|
41
|
+
const lastUse = grant.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, grant.lastUsedAt) });
|
|
42
|
+
return {
|
|
43
|
+
id: grant.id,
|
|
44
|
+
clientName: grant.clientName,
|
|
45
|
+
scopeText: grant.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
46
|
+
details: [formatMessage(messages.grants.connectedOn, { date: formatDate(config, grant.createdAt) }), lastUse],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function toTool(tool, config, options, messages) {
|
|
50
|
+
const accessText = tool.access === "read" ? messages.tools.read : options.allowWrites ? messages.tools.write : messages.tools.writeUnavailable;
|
|
51
|
+
return { name: tool.name, description: getLocalizedText(tool.description, config.locale), accessText };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The tools, the user's tokens and (while OAuth is on) connected apps, and the page settings, ready to
|
|
55
|
+
* spread into `TokenManager` next to the actions. Reads the database; it checks no session itself.
|
|
56
|
+
*/
|
|
57
|
+
export async function getTokenManagerProps({ userId, config = getSoftureConfig(), now }) {
|
|
58
|
+
const options = getMcpAccessOptions(config);
|
|
59
|
+
const messages = getMcpAccessMessages(config);
|
|
60
|
+
const ctx = await getMcpAccessContext(config);
|
|
61
|
+
const at = now ?? ctx.clock.now();
|
|
62
|
+
const tokens = await listAccessTokens(ctx, userId);
|
|
63
|
+
const grants = options.oauth.enabled ? await listOAuthGrants(ctx, userId) : null;
|
|
64
|
+
return {
|
|
65
|
+
tools: options.tools.map((tool) => toTool(tool, config, options, messages)),
|
|
66
|
+
tokens: tokens.map((token) => toRow(token, at, config, options, messages)),
|
|
67
|
+
...(grants === null ? {} : { grants: grants.map((grant) => toGrant(grant, config, options, messages)) }),
|
|
68
|
+
allowWrites: options.allowWrites,
|
|
69
|
+
maxTokens: options.maxTokensPerUser,
|
|
70
|
+
messages,
|
|
71
|
+
locale: config.locale,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=token-manager-props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-manager-props.js","sourceRoot":"","sources":["../../src/next/token-manager-props.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/ui/token-manager.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAgC,KAAK,UAAU,EAA+C,MAAM,gBAAgB,CAAC;AAE5H,OAAO,KAAK,EAAe,mBAAmB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACnH,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/ui/token-manager.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAgC,KAAK,UAAU,EAA+C,MAAM,gBAAgB,CAAC;AAE5H,OAAO,KAAK,EAAe,mBAAmB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACnH,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAWpF,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AACnH,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AACtH,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEtH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAEnJ,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IAC5C,wDAAwD;IACxD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,iDAAiD;IACjD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AA+MD,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,MAAM,EACN,iBAAiB,EACjB,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,GACT,EAAE,iBAAiB,+BAoDnB"}
|
package/dist/ui/token-manager.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { formatMessage } from "@softure-ai/core";
|
|
4
4
|
import { Button, ButtonLink, Checkbox, createSlotClassGetter, FormError, TextField } from "@softure-ai/ui";
|
|
5
|
-
import { useActionState, useState } from "react";
|
|
5
|
+
import { useActionState, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { getTokenErrorMessage } from "../messages/index.js";
|
|
7
7
|
import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
|
|
8
8
|
const DEFAULT_CLASSES = {
|
|
@@ -30,14 +30,29 @@ function Snippet({ title, value, messages, slot, unstyled }) {
|
|
|
30
30
|
}
|
|
31
31
|
function IssuedTokenPanel({ issued, onDone, messages, slot, unstyled, }) {
|
|
32
32
|
const snippet = (title, value) => _jsx(Snippet, { title: title, value: value, messages: messages, slot: slot, unstyled: unstyled });
|
|
33
|
-
|
|
33
|
+
const headingRef = useRef(null);
|
|
34
|
+
// Once per issued token, so a screen reader announces it; not on re-renders while the owner copies.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
headingRef.current?.focus();
|
|
37
|
+
}, [issued.id]);
|
|
38
|
+
return (_jsxs("section", { className: slot("section"), children: [_jsx("h3", { ref: headingRef, tabIndex: -1, className: slot("heading"), children: formatMessage(messages.issued.title, { name: issued.name }) }), _jsx("p", { className: slot("text"), children: messages.issued.once }), _jsxs("p", { className: slot("text"), children: [issued.expiresText, " ", issued.canWrite ? messages.issued.readWrite : messages.issued.readOnly] }), snippet(messages.issued.promptTitle, issued.setup.assistantPrompt), _jsx("p", { className: slot("text"), children: messages.issued.promptHint }), _jsx(ButtonLink, { href: issued.setup.claudeCodeLink, variant: "secondary", size: "sm", unstyled: unstyled, children: messages.issued.openClaudeCode }), snippet(messages.issued.commandTitle, issued.setup.claudeCodeCommand), snippet(messages.issued.jsonTitle, issued.setup.jsonConfig), snippet(messages.issued.desktopTitle, issued.setup.desktopConfig), snippet(messages.issued.headerTitle, `${issued.setup.endpointUrl}\nAuthorization: ${issued.setup.authorizationHeader}`), _jsx(Button, { variant: "primary", onClick: onDone, unstyled: unstyled, children: messages.issued.done })] }));
|
|
34
39
|
}
|
|
35
40
|
function IssueForm({ action, allowWrites, maxTokens, messages, locale, slot, unstyled, }) {
|
|
36
41
|
const [state, formAction, isPending] = useActionState(action, { status: "idle" });
|
|
37
42
|
// The id of the issued token the owner dismissed; a new one shows again.
|
|
38
43
|
const [dismissedId, setDismissedId] = useState(null);
|
|
39
44
|
const issued = state.status === "ok" && state.issued.id !== dismissedId ? state.issued : null;
|
|
40
|
-
|
|
45
|
+
const formRef = useRef(null);
|
|
46
|
+
function dismiss(id) {
|
|
47
|
+
setDismissedId(id);
|
|
48
|
+
// The focused "Done" button unmounts with the panel; once it is gone, the name field is the next step.
|
|
49
|
+
requestAnimationFrame(() => {
|
|
50
|
+
const field = formRef.current?.elements.namedItem("name");
|
|
51
|
+
if (field instanceof HTMLInputElement)
|
|
52
|
+
field.focus();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return (_jsxs(_Fragment, { children: [_jsxs("section", { className: slot("section"), children: [_jsx("h2", { className: slot("heading"), children: messages.issue.title }), _jsxs("form", { ref: formRef, action: formAction, className: slot("list"), children: [_jsx(TextField, { name: "name", label: messages.issue.name, hint: messages.issue.nameHint, maxLength: MAX_TOKEN_NAME_LENGTH, required: true, autoComplete: "off", locale: locale, unstyled: unstyled }), allowWrites ? _jsx(Checkbox, { name: "canWrite", label: messages.issue.canWrite, description: messages.issue.canWriteHint, unstyled: unstyled }) : null, _jsx("p", { className: slot("text"), children: formatMessage(messages.issue.limit, { max: maxTokens }) }), _jsx(FormError, { message: state.status === "error" ? (state.message ?? getTokenErrorMessage(messages, state.error)) : undefined, unstyled: unstyled }), _jsx(Button, { type: "submit", variant: "primary", pending: isPending, unstyled: unstyled, children: isPending ? messages.issue.pending : messages.issue.submit })] })] }), issued === null ? null : _jsx(IssuedTokenPanel, { issued: issued, onDone: () => dismiss(issued.id), messages: messages, slot: slot, unstyled: unstyled })] }));
|
|
41
56
|
}
|
|
42
57
|
function TokenItem({ row, action, messages, slot, unstyled, }) {
|
|
43
58
|
const [state, formAction, isPending] = useActionState(action, { status: "idle" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/ui/token-manager.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAe,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAmB,qBAAqB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5H,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/ui/token-manager.tsx"],"names":[],"mappings":"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"}
|
package/module.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mcp-access",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"dependsOn": { "security": "^0.2.0", "auth": "^0.2.0", "jobs": "^0.1.1?", "ops": "^0.2.0?" },
|
|
5
5
|
"dbSchema": "mcp",
|
|
6
6
|
"tables": ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/mcp-access",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "SOFTURE AI MCP access module: hashed, scoped, expiring access tokens, a rate-limited Bearer endpoint around the app's MCP server factory, and a token page with client setup instructions for Next.js; tool result helpers with a safe error boundary, a stdio entry, a catalog parity check and the softure-mcp prune command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const MCP_RATE_LIMIT_BUCKETS = {
|
|
|
34
34
|
export const mcpAccess = defineModule({
|
|
35
35
|
manifest: {
|
|
36
36
|
id: MODULE_ID,
|
|
37
|
-
version: "0.2.
|
|
37
|
+
version: "0.2.5",
|
|
38
38
|
dependsOn: { security: "^0.2.0", auth: "^0.2.0", jobs: "^0.1.1?", ops: "^0.2.0?" },
|
|
39
39
|
dbSchema: "mcp",
|
|
40
40
|
tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window"],
|
package/src/next/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The Next.js adapter of @softure-ai/mcp-access: the MCP route factory, the token page
|
|
2
|
-
// server actions, and the OAuth routes and consent page (docs/02-module-standard.md §8).
|
|
1
|
+
// The Next.js adapter of @softure-ai/mcp-access: the MCP route factory, the token page, its prop
|
|
2
|
+
// builder and its server actions, and the OAuth routes and consent page (docs/02-module-standard.md §8).
|
|
3
3
|
export { issueTokenAction, revokeGrantAction, revokeTokenAction } from "./actions.js";
|
|
4
4
|
export { OAuthConsentPage, type OAuthConsentPageProps } from "./oauth-page.js";
|
|
5
5
|
export {
|
|
@@ -14,3 +14,4 @@ export { getMcpAccessContext, getRequestOrigins } from "./context.js";
|
|
|
14
14
|
export { issueToken, refuseIssue, type BeforeIssueGate, type IssueGateInput, type IssueGateResult, type IssueTokenOptions } from "./issue-token.js";
|
|
15
15
|
export { McpAccessPage, type McpAccessPageProps } from "./pages.js";
|
|
16
16
|
export { createMcpRoute } from "./route.js";
|
|
17
|
+
export { getTokenManagerProps, type TokenManagerData, type TokenManagerPropsInput } from "./token-manager-props.js";
|
package/src/next/pages.tsx
CHANGED
|
@@ -1,69 +1,17 @@
|
|
|
1
1
|
// The token page, ready to mount with one line:
|
|
2
2
|
// `export { McpAccessPage as default } from "@softure-ai/mcp-access/next"`.
|
|
3
3
|
// A server component for the signed-in user; without a session it redirects to the login page and
|
|
4
|
-
// back.
|
|
5
|
-
// app's locale and time zone, and never differ between the server render and hydration.
|
|
4
|
+
// back. Its props come from `getTokenManagerProps`, the same builder an app's own page uses.
|
|
6
5
|
import { requireUser } from "@softure-ai/auth/next";
|
|
7
|
-
import { formatMessage, type SoftureConfig } from "@softure-ai/core";
|
|
8
6
|
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
9
7
|
import { Card } from "@softure-ai/ui";
|
|
10
|
-
import
|
|
11
|
-
import type
|
|
12
|
-
import type { McpAccessOptions } from "../options.js";
|
|
13
|
-
import { getLocalizedText, getMcpAccessMessages, getMcpAccessOptions, getMcpAccessRoutes } from "../server/options.js";
|
|
14
|
-
import { listOAuthGrants } from "../server/oauth.js";
|
|
15
|
-
import { listAccessTokens } from "../server/tokens.js";
|
|
16
|
-
import { getAccessTokenStatus } from "../token-status.js";
|
|
17
|
-
import { TokenManager, type IssueTokenAction, type TokenManagerGrant, type TokenManagerRow, type TokenManagerTool } from "../ui/token-manager.js";
|
|
8
|
+
import { getMcpAccessMessages, getMcpAccessRoutes } from "../server/options.js";
|
|
9
|
+
import { TokenManager, type IssueTokenAction } from "../ui/token-manager.js";
|
|
18
10
|
import { issueTokenAction, revokeGrantAction, revokeTokenAction } from "./actions.js";
|
|
19
|
-
import {
|
|
20
|
-
import { formatDate, formatDateTime } from "./format.js";
|
|
11
|
+
import { getTokenManagerProps } from "./token-manager-props.js";
|
|
21
12
|
|
|
22
13
|
const LAYOUT_CLASS = "sft:mx-auto sft:box-border sft:w-full sft:sm:max-w-3xl sft:px-4 sft:py-4";
|
|
23
14
|
|
|
24
|
-
function describeStatus(status: AccessTokenStatus, messages: McpAccessMessages): string {
|
|
25
|
-
switch (status.kind) {
|
|
26
|
-
case "active":
|
|
27
|
-
return messages.list.active;
|
|
28
|
-
case "expired":
|
|
29
|
-
return messages.list.expired;
|
|
30
|
-
case "expiring":
|
|
31
|
-
if (status.daysLeft === 0) return messages.list.expiresToday;
|
|
32
|
-
if (status.daysLeft === 1) return messages.list.expiresTomorrow;
|
|
33
|
-
return formatMessage(messages.list.expiresInDays, { days: status.daysLeft });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function toRow(token: AccessTokenView, now: Date, config: SoftureConfig, options: McpAccessOptions, messages: McpAccessMessages): TokenManagerRow {
|
|
38
|
-
const status = getAccessTokenStatus(token.expiresAt, now, { timezone: config.timezone, warningDays: options.expiryWarningDays });
|
|
39
|
-
const isExpired = status.kind === "expired";
|
|
40
|
-
const expiry = formatMessage(isExpired ? messages.list.expiredOn : messages.list.expiresOn, { date: formatDate(config, token.expiresAt) });
|
|
41
|
-
const lastUse = token.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, token.lastUsedAt) });
|
|
42
|
-
return {
|
|
43
|
-
id: token.id,
|
|
44
|
-
name: token.name,
|
|
45
|
-
scopeText: token.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
46
|
-
statusText: describeStatus(status, messages),
|
|
47
|
-
isExpired,
|
|
48
|
-
details: [formatMessage(messages.list.createdOn, { date: formatDate(config, token.createdAt) }), expiry, lastUse],
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function toGrant(grant: OAuthGrantView, config: SoftureConfig, options: McpAccessOptions, messages: McpAccessMessages): TokenManagerGrant {
|
|
53
|
-
const lastUse = grant.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, grant.lastUsedAt) });
|
|
54
|
-
return {
|
|
55
|
-
id: grant.id,
|
|
56
|
-
clientName: grant.clientName,
|
|
57
|
-
scopeText: grant.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
58
|
-
details: [formatMessage(messages.grants.connectedOn, { date: formatDate(config, grant.createdAt) }), lastUse],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function toTool(tool: McpAccessOptions["tools"][number], config: SoftureConfig, options: McpAccessOptions, messages: McpAccessMessages): TokenManagerTool {
|
|
63
|
-
const accessText = tool.access === "read" ? messages.tools.read : options.allowWrites ? messages.tools.write : messages.tools.writeUnavailable;
|
|
64
|
-
return { name: tool.name, description: getLocalizedText(tool.description, config.locale), accessText };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
15
|
export interface McpAccessPageProps {
|
|
68
16
|
/**
|
|
69
17
|
* The app's own issue action, e.g. one that calls `issueToken` with a `beforeIssue` gate. Default:
|
|
@@ -75,25 +23,16 @@ export interface McpAccessPageProps {
|
|
|
75
23
|
export async function McpAccessPage({ issueAction = issueTokenAction }: McpAccessPageProps = {}) {
|
|
76
24
|
const config = getSoftureConfig();
|
|
77
25
|
const user = await requireUser({ next: getMcpAccessRoutes(config).page });
|
|
78
|
-
const options = getMcpAccessOptions(config);
|
|
79
26
|
const messages = getMcpAccessMessages(config);
|
|
80
|
-
const
|
|
81
|
-
const now = ctx.clock.now();
|
|
82
|
-
const tokens = await listAccessTokens(ctx, user.id);
|
|
83
|
-
const grants = options.oauth.enabled ? await listOAuthGrants(ctx, user.id) : null;
|
|
27
|
+
const props = await getTokenManagerProps({ userId: user.id, config });
|
|
84
28
|
return (
|
|
85
29
|
<main className={LAYOUT_CLASS}>
|
|
86
30
|
<Card title={messages.page.title} subtitle={messages.page.lead}>
|
|
87
31
|
<TokenManager
|
|
88
|
-
|
|
89
|
-
tokens={tokens.map((token) => toRow(token, now, config, options, messages))}
|
|
90
|
-
allowWrites={options.allowWrites}
|
|
91
|
-
maxTokens={options.maxTokensPerUser}
|
|
32
|
+
{...props}
|
|
92
33
|
issueAction={issueAction}
|
|
93
34
|
revokeAction={revokeTokenAction}
|
|
94
|
-
{...(grants ===
|
|
95
|
-
messages={messages}
|
|
96
|
-
locale={config.locale}
|
|
35
|
+
{...(props.grants === undefined ? {} : { revokeGrantAction })}
|
|
97
36
|
/>
|
|
98
37
|
</Card>
|
|
99
38
|
</main>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The data `TokenManager` shows, built on the server for one user: what `McpAccessPage` renders, for an
|
|
2
|
+
// app that composes its own page around `TokenManager`. Dates are formatted here, once, in the app's
|
|
3
|
+
// locale and time zone, so the server render and hydration never differ.
|
|
4
|
+
import { formatMessage, type SoftureConfig } from "@softure-ai/core";
|
|
5
|
+
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
6
|
+
import type { AccessTokenStatus, AccessTokenView, OAuthGrantView } from "../contract.js";
|
|
7
|
+
import type { McpAccessMessages } from "../messages/index.js";
|
|
8
|
+
import type { McpAccessOptions } from "../options.js";
|
|
9
|
+
import { getLocalizedText, getMcpAccessMessages, getMcpAccessOptions } from "../server/options.js";
|
|
10
|
+
import { listOAuthGrants } from "../server/oauth.js";
|
|
11
|
+
import { listAccessTokens } from "../server/tokens.js";
|
|
12
|
+
import { getAccessTokenStatus } from "../token-status.js";
|
|
13
|
+
import type { TokenManagerGrant, TokenManagerProps, TokenManagerRow, TokenManagerTool } from "../ui/token-manager.js";
|
|
14
|
+
import { getMcpAccessContext } from "./context.js";
|
|
15
|
+
import { formatDate, formatDateTime } from "./format.js";
|
|
16
|
+
|
|
17
|
+
/** Every prop of `TokenManager` but the actions and the styling. */
|
|
18
|
+
export type TokenManagerData = Pick<TokenManagerProps, "tools" | "tokens" | "grants" | "allowWrites" | "maxTokens" | "messages" | "locale">;
|
|
19
|
+
|
|
20
|
+
export interface TokenManagerPropsInput {
|
|
21
|
+
/** The signed-in user's id, from the app's session check: only this user's tokens and apps are listed. */
|
|
22
|
+
readonly userId: string;
|
|
23
|
+
/** Default: the registered config (`getSoftureConfig()`). */
|
|
24
|
+
readonly config?: SoftureConfig;
|
|
25
|
+
/** The moment token statuses are computed for. Default: the module clock's now. */
|
|
26
|
+
readonly now?: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function describeStatus(status: AccessTokenStatus, messages: McpAccessMessages): string {
|
|
30
|
+
switch (status.kind) {
|
|
31
|
+
case "active":
|
|
32
|
+
return messages.list.active;
|
|
33
|
+
case "expired":
|
|
34
|
+
return messages.list.expired;
|
|
35
|
+
case "expiring":
|
|
36
|
+
if (status.daysLeft === 0) return messages.list.expiresToday;
|
|
37
|
+
if (status.daysLeft === 1) return messages.list.expiresTomorrow;
|
|
38
|
+
return formatMessage(messages.list.expiresInDays, { days: status.daysLeft });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toRow(token: AccessTokenView, now: Date, config: SoftureConfig, options: McpAccessOptions, messages: McpAccessMessages): TokenManagerRow {
|
|
43
|
+
const status = getAccessTokenStatus(token.expiresAt, now, { timezone: config.timezone, warningDays: options.expiryWarningDays });
|
|
44
|
+
const isExpired = status.kind === "expired";
|
|
45
|
+
const expiry = formatMessage(isExpired ? messages.list.expiredOn : messages.list.expiresOn, { date: formatDate(config, token.expiresAt) });
|
|
46
|
+
const lastUse = token.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, token.lastUsedAt) });
|
|
47
|
+
return {
|
|
48
|
+
id: token.id,
|
|
49
|
+
name: token.name,
|
|
50
|
+
scopeText: token.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
51
|
+
statusText: describeStatus(status, messages),
|
|
52
|
+
isExpired,
|
|
53
|
+
details: [formatMessage(messages.list.createdOn, { date: formatDate(config, token.createdAt) }), expiry, lastUse],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toGrant(grant: OAuthGrantView, config: SoftureConfig, options: McpAccessOptions, messages: McpAccessMessages): TokenManagerGrant {
|
|
58
|
+
const lastUse = grant.lastUsedAt === null ? messages.list.neverUsed : formatMessage(messages.list.lastUsed, { date: formatDateTime(config, grant.lastUsedAt) });
|
|
59
|
+
return {
|
|
60
|
+
id: grant.id,
|
|
61
|
+
clientName: grant.clientName,
|
|
62
|
+
scopeText: grant.canWrite && options.allowWrites ? messages.list.readWrite : messages.list.readOnly,
|
|
63
|
+
details: [formatMessage(messages.grants.connectedOn, { date: formatDate(config, grant.createdAt) }), lastUse],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toTool(tool: McpAccessOptions["tools"][number], config: SoftureConfig, options: McpAccessOptions, messages: McpAccessMessages): TokenManagerTool {
|
|
68
|
+
const accessText = tool.access === "read" ? messages.tools.read : options.allowWrites ? messages.tools.write : messages.tools.writeUnavailable;
|
|
69
|
+
return { name: tool.name, description: getLocalizedText(tool.description, config.locale), accessText };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The tools, the user's tokens and (while OAuth is on) connected apps, and the page settings, ready to
|
|
74
|
+
* spread into `TokenManager` next to the actions. Reads the database; it checks no session itself.
|
|
75
|
+
*/
|
|
76
|
+
export async function getTokenManagerProps({ userId, config = getSoftureConfig(), now }: TokenManagerPropsInput): Promise<TokenManagerData> {
|
|
77
|
+
const options = getMcpAccessOptions(config);
|
|
78
|
+
const messages = getMcpAccessMessages(config);
|
|
79
|
+
const ctx = await getMcpAccessContext(config);
|
|
80
|
+
const at = now ?? ctx.clock.now();
|
|
81
|
+
const tokens = await listAccessTokens(ctx, userId);
|
|
82
|
+
const grants = options.oauth.enabled ? await listOAuthGrants(ctx, userId) : null;
|
|
83
|
+
return {
|
|
84
|
+
tools: options.tools.map((tool) => toTool(tool, config, options, messages)),
|
|
85
|
+
tokens: tokens.map((token) => toRow(token, at, config, options, messages)),
|
|
86
|
+
...(grants === null ? {} : { grants: grants.map((grant) => toGrant(grant, config, options, messages)) }),
|
|
87
|
+
allowWrites: options.allowWrites,
|
|
88
|
+
maxTokens: options.maxTokensPerUser,
|
|
89
|
+
messages,
|
|
90
|
+
locale: config.locale,
|
|
91
|
+
};
|
|
92
|
+
}
|
package/src/ui/token-manager.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { formatMessage, type Locale } from "@softure-ai/core";
|
|
4
4
|
import { Button, ButtonLink, Checkbox, type ClassNames, createSlotClassGetter, FormError, TextField } from "@softure-ai/ui";
|
|
5
|
-
import { useActionState, useState } from "react";
|
|
5
|
+
import { useActionState, useEffect, useRef, useState } from "react";
|
|
6
6
|
import type { IssuedToken, IssueTokenFormState, RevokeGrantFormState, RevokeTokenFormState } from "../contract.js";
|
|
7
7
|
import { getTokenErrorMessage, type McpAccessMessages } from "../messages/index.js";
|
|
8
8
|
import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
|
|
@@ -11,8 +11,9 @@ import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
|
|
|
11
11
|
// setup snippets, the owner's tokens with a revoke button each, and, with OAuth on, the apps
|
|
12
12
|
// connected through OAuth with a disconnect button each. Both forms submit to their
|
|
13
13
|
// server actions through `useActionState`. The plaintext lives only in the issue form's state:
|
|
14
|
-
// "Done" or leaving the page drops it.
|
|
15
|
-
//
|
|
14
|
+
// "Done" or leaving the page drops it. Focus follows the one-time token: its heading takes focus
|
|
15
|
+
// when it appears, and "Done" hands focus back to the name field. Copy comes from the module's
|
|
16
|
+
// messages; styling only from @softure-ai/ui classes.
|
|
16
17
|
|
|
17
18
|
export type IssueTokenAction = (previous: IssueTokenFormState, formData: FormData) => Promise<IssueTokenFormState>;
|
|
18
19
|
export type RevokeTokenAction = (previous: RevokeTokenFormState, formData: FormData) => Promise<RevokeTokenFormState>;
|
|
@@ -124,9 +125,14 @@ function IssuedTokenPanel({
|
|
|
124
125
|
unstyled: boolean | undefined;
|
|
125
126
|
}) {
|
|
126
127
|
const snippet = (title: string, value: string) => <Snippet title={title} value={value} messages={messages} slot={slot} unstyled={unstyled} />;
|
|
128
|
+
const headingRef = useRef<HTMLHeadingElement>(null);
|
|
129
|
+
// Once per issued token, so a screen reader announces it; not on re-renders while the owner copies.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
headingRef.current?.focus();
|
|
132
|
+
}, [issued.id]);
|
|
127
133
|
return (
|
|
128
134
|
<section className={slot("section")}>
|
|
129
|
-
<h3 className={slot("heading")}>{formatMessage(messages.issued.title, { name: issued.name })}</h3>
|
|
135
|
+
<h3 ref={headingRef} tabIndex={-1} className={slot("heading")}>{formatMessage(messages.issued.title, { name: issued.name })}</h3>
|
|
130
136
|
<p className={slot("text")}>{messages.issued.once}</p>
|
|
131
137
|
<p className={slot("text")}>
|
|
132
138
|
{issued.expiresText} {issued.canWrite ? messages.issued.readWrite : messages.issued.readOnly}
|
|
@@ -168,11 +174,20 @@ function IssueForm({
|
|
|
168
174
|
// The id of the issued token the owner dismissed; a new one shows again.
|
|
169
175
|
const [dismissedId, setDismissedId] = useState<string | null>(null);
|
|
170
176
|
const issued = state.status === "ok" && state.issued.id !== dismissedId ? state.issued : null;
|
|
177
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
178
|
+
function dismiss(id: string) {
|
|
179
|
+
setDismissedId(id);
|
|
180
|
+
// The focused "Done" button unmounts with the panel; once it is gone, the name field is the next step.
|
|
181
|
+
requestAnimationFrame(() => {
|
|
182
|
+
const field = formRef.current?.elements.namedItem("name");
|
|
183
|
+
if (field instanceof HTMLInputElement) field.focus();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
171
186
|
return (
|
|
172
187
|
<>
|
|
173
188
|
<section className={slot("section")}>
|
|
174
189
|
<h2 className={slot("heading")}>{messages.issue.title}</h2>
|
|
175
|
-
<form action={formAction} className={slot("list")}>
|
|
190
|
+
<form ref={formRef} action={formAction} className={slot("list")}>
|
|
176
191
|
<TextField
|
|
177
192
|
name="name"
|
|
178
193
|
label={messages.issue.name}
|
|
@@ -191,7 +206,7 @@ function IssueForm({
|
|
|
191
206
|
</Button>
|
|
192
207
|
</form>
|
|
193
208
|
</section>
|
|
194
|
-
{issued === null ? null : <IssuedTokenPanel issued={issued} onDone={() =>
|
|
209
|
+
{issued === null ? null : <IssuedTokenPanel issued={issued} onDone={() => dismiss(issued.id)} messages={messages} slot={slot} unstyled={unstyled} />}
|
|
195
210
|
</>
|
|
196
211
|
);
|
|
197
212
|
}
|