@softure-ai/mcp-access 0.0.0-stage → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +177 -2
  4. package/dist/client-setup.d.ts +32 -0
  5. package/dist/client-setup.d.ts.map +1 -0
  6. package/dist/client-setup.js +57 -0
  7. package/dist/client-setup.js.map +1 -0
  8. package/dist/contract.d.ts +80 -0
  9. package/dist/contract.d.ts.map +1 -0
  10. package/dist/contract.js +2 -0
  11. package/dist/contract.js.map +1 -0
  12. package/dist/index.d.ts +119 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +50 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/messages/en.d.ts +77 -0
  17. package/dist/messages/en.d.ts.map +1 -0
  18. package/dist/messages/en.js +77 -0
  19. package/dist/messages/en.js.map +1 -0
  20. package/dist/messages/index.d.ts +161 -0
  21. package/dist/messages/index.d.ts.map +1 -0
  22. package/dist/messages/index.js +14 -0
  23. package/dist/messages/index.js.map +1 -0
  24. package/dist/messages/pl.d.ts +3 -0
  25. package/dist/messages/pl.d.ts.map +1 -0
  26. package/dist/messages/pl.js +77 -0
  27. package/dist/messages/pl.js.map +1 -0
  28. package/dist/next/actions.d.ts +6 -0
  29. package/dist/next/actions.d.ts.map +1 -0
  30. package/dist/next/actions.js +88 -0
  31. package/dist/next/actions.js.map +1 -0
  32. package/dist/next/context.d.ts +4 -0
  33. package/dist/next/context.d.ts.map +1 -0
  34. package/dist/next/context.js +13 -0
  35. package/dist/next/context.js.map +1 -0
  36. package/dist/next/format.d.ts +4 -0
  37. package/dist/next/format.d.ts.map +1 -0
  38. package/dist/next/format.js +7 -0
  39. package/dist/next/format.js.map +1 -0
  40. package/dist/next/index.d.ts +4 -0
  41. package/dist/next/index.d.ts.map +1 -0
  42. package/dist/next/index.js +6 -0
  43. package/dist/next/index.js.map +1 -0
  44. package/dist/next/pages.d.ts +2 -0
  45. package/dist/next/pages.d.ts.map +1 -0
  46. package/dist/next/pages.js +61 -0
  47. package/dist/next/pages.js.map +1 -0
  48. package/dist/next/route.d.ts +3 -0
  49. package/dist/next/route.d.ts.map +1 -0
  50. package/dist/next/route.js +24 -0
  51. package/dist/next/route.js.map +1 -0
  52. package/dist/options.d.ts +43 -0
  53. package/dist/options.d.ts.map +1 -0
  54. package/dist/options.js +47 -0
  55. package/dist/options.js.map +1 -0
  56. package/dist/schema.d.ts +145 -0
  57. package/dist/schema.d.ts.map +1 -0
  58. package/dist/schema.js +18 -0
  59. package/dist/schema.js.map +1 -0
  60. package/dist/server/endpoint.d.ts +26 -0
  61. package/dist/server/endpoint.d.ts.map +1 -0
  62. package/dist/server/endpoint.js +107 -0
  63. package/dist/server/endpoint.js.map +1 -0
  64. package/dist/server/health.d.ts +3 -0
  65. package/dist/server/health.d.ts.map +1 -0
  66. package/dist/server/health.js +11 -0
  67. package/dist/server/health.js.map +1 -0
  68. package/dist/server/index.d.ts +6 -0
  69. package/dist/server/index.d.ts.map +1 -0
  70. package/dist/server/index.js +8 -0
  71. package/dist/server/index.js.map +1 -0
  72. package/dist/server/options.d.ts +20 -0
  73. package/dist/server/options.d.ts.map +1 -0
  74. package/dist/server/options.js +40 -0
  75. package/dist/server/options.js.map +1 -0
  76. package/dist/server/privacy.d.ts +15 -0
  77. package/dist/server/privacy.d.ts.map +1 -0
  78. package/dist/server/privacy.js +36 -0
  79. package/dist/server/privacy.js.map +1 -0
  80. package/dist/server/tokens.d.ts +66 -0
  81. package/dist/server/tokens.d.ts.map +1 -0
  82. package/dist/server/tokens.js +138 -0
  83. package/dist/server/tokens.js.map +1 -0
  84. package/dist/token-status.d.ts +9 -0
  85. package/dist/token-status.d.ts.map +1 -0
  86. package/dist/token-status.js +14 -0
  87. package/dist/token-status.js.map +1 -0
  88. package/dist/ui/index.d.ts +2 -0
  89. package/dist/ui/index.d.ts.map +1 -0
  90. package/dist/ui/index.js +4 -0
  91. package/dist/ui/index.js.map +1 -0
  92. package/dist/ui/token-manager.d.ts +43 -0
  93. package/dist/ui/token-manager.d.ts.map +1 -0
  94. package/dist/ui/token-manager.js +50 -0
  95. package/dist/ui/token-manager.js.map +1 -0
  96. package/migrations/0001_create_access_tokens.sql +24 -0
  97. package/module.json +15 -0
  98. package/package.json +69 -4
  99. package/src/client-setup.ts +71 -0
  100. package/src/contract.ts +80 -0
  101. package/src/index.ts +81 -0
  102. package/src/messages/en.ts +76 -0
  103. package/src/messages/index.ts +18 -0
  104. package/src/messages/pl.ts +78 -0
  105. package/src/next/actions.ts +91 -0
  106. package/src/next/context.ts +14 -0
  107. package/src/next/format.ts +10 -0
  108. package/src/next/index.ts +5 -0
  109. package/src/next/next-modules.d.ts +16 -0
  110. package/src/next/pages.tsx +80 -0
  111. package/src/next/route.ts +24 -0
  112. package/src/options.ts +58 -0
  113. package/src/schema.ts +19 -0
  114. package/src/server/endpoint.ts +141 -0
  115. package/src/server/health.ts +12 -0
  116. package/src/server/index.ts +36 -0
  117. package/src/server/options.ts +54 -0
  118. package/src/server/privacy.ts +49 -0
  119. package/src/server/tokens.ts +188 -0
  120. package/src/token-status.ts +27 -0
  121. package/src/ui/index.ts +11 -0
  122. package/src/ui/token-manager.tsx +254 -0
@@ -0,0 +1,40 @@
1
+ // The mcp-access options of the running app, read from the configuration in the module context.
2
+ import { getModule } from "@softure-ai/core";
3
+ const MODULE_ID = "mcp-access";
4
+ /** The enabled module. Throws when the app did not enable it: calling its functions then is a bug. */
5
+ export function getMcpAccessModule(config) {
6
+ const module = getModule(config, MODULE_ID);
7
+ if (module === undefined) {
8
+ throw new Error("@softure-ai/mcp-access: the module is not enabled; add mcpAccess({ ... }) to modules in softure.config.ts");
9
+ }
10
+ return module;
11
+ }
12
+ export function getMcpAccessOptions(config) {
13
+ // The module factory parsed these options with mcpAccessOptionsSchema.
14
+ return getMcpAccessModule(config).options;
15
+ }
16
+ export function getMcpAccessRoutes(config) {
17
+ const routes = getMcpAccessModule(config).routes;
18
+ const read = (name) => {
19
+ const path = routes[name];
20
+ // The manifest declares every route, so a missing one means a broken module definition.
21
+ if (path === undefined)
22
+ throw new Error(`@softure-ai/mcp-access: route "${name}" is missing from the module manifest`);
23
+ return path;
24
+ };
25
+ return { page: read("page"), endpoint: read("endpoint") };
26
+ }
27
+ /** The absolute URL MCP clients call: the app's origin and the endpoint route. */
28
+ export function getMcpEndpointUrl(config) {
29
+ return new URL(getMcpAccessRoutes(config).endpoint, config.appOrigin).toString();
30
+ }
31
+ /** The module's copy in the app's locale, with the app's overrides applied. */
32
+ export function getMcpAccessMessages(config) {
33
+ // The module factory merged the dictionaries; their shape is the module's own.
34
+ return getMcpAccessModule(config).messages[config.locale];
35
+ }
36
+ /** The text in `locale`, else in `en`. */
37
+ export function getLocalizedText(text, locale) {
38
+ return text[locale] ?? text.en ?? "";
39
+ }
40
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,OAAO,EAAE,SAAS,EAAmC,MAAM,kBAAkB,CAAC;AAI9E,MAAM,SAAS,GAAG,YAAY,CAAC;AAE/B,sGAAsG;AACtG,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,2GAA2G,CAAC,CAAC;IAC/H,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAqB;IACvD,uEAAuE;IACvE,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC,OAA2B,CAAC;AAChE,CAAC;AASD,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IACjD,MAAM,IAAI,GAAG,CAAC,IAA2B,EAAU,EAAE;QACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,wFAAwF;QACxF,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,uCAAuC,CAAC,CAAC;QACvH,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,MAAqB;IACrD,OAAO,IAAI,GAAG,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC;AACnF,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,oBAAoB,CAAC,MAAqB;IACxD,+EAA+E;IAC/E,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAsB,CAAC;AACjF,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,gBAAgB,CAAC,IAAqC,EAAE,MAAc;IACpF,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;AACvC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { type ModuleContext, type Ok, type PrivacyContributor } from "@softure-ai/core";
2
+ /** What mcp-access holds about one user, as it appears in their export. */
3
+ export interface McpAccessUserData {
4
+ readonly accessTokens: readonly {
5
+ readonly name: string;
6
+ readonly canWrite: boolean;
7
+ readonly createdAt: Date;
8
+ readonly expiresAt: Date;
9
+ readonly lastUsedAt: Date | null;
10
+ }[];
11
+ }
12
+ export declare function exportMcpAccessUserData(context: ModuleContext, userId: string): Promise<Ok<McpAccessUserData>>;
13
+ export declare function deleteMcpAccessUserData(context: ModuleContext, userId: string): Promise<Ok<undefined>>;
14
+ export declare const mcpAccessPrivacyContributor: PrivacyContributor;
15
+ //# sourceMappingURL=privacy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privacy.d.ts","sourceRoot":"","sources":["../../src/server/privacy.ts"],"names":[],"mappings":"AAGA,OAAO,EAAM,KAAK,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAK5F,2EAA2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,YAAY,EAAE,SAAS;QAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;QACzB,QAAQ,CAAC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;KAClC,EAAE,CAAC;CACL;AAID,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAgBpH;AAED,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAI5G;AAED,eAAO,MAAM,2BAA2B,EAAE,kBAGzC,CAAC"}
@@ -0,0 +1,36 @@
1
+ // The mcp-access part of a GDPR export and deletion (`@softure-ai/privacy`): the user's access
2
+ // tokens, by name and dates. Never a token hash. Deletion removes the tokens by name before auth
3
+ // deletes the account, so it does not rest on the foreign key's cascade alone.
4
+ import { ok } from "@softure-ai/core";
5
+ import { asc, eq } from "drizzle-orm";
6
+ import { accessTokens } from "../schema.js";
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
+ export async function exportMcpAccessUserData(context, userId) {
9
+ if (!UUID.test(userId))
10
+ return ok({ accessTokens: [] });
11
+ // Core types `db` as unknown; privacy passes the @softure-ai/db handle or its transaction.
12
+ const db = context.db;
13
+ const rows = await db
14
+ .select({
15
+ name: accessTokens.name,
16
+ canWrite: accessTokens.canWrite,
17
+ createdAt: accessTokens.createdAt,
18
+ expiresAt: accessTokens.expiresAt,
19
+ lastUsedAt: accessTokens.lastUsedAt,
20
+ })
21
+ .from(accessTokens)
22
+ .where(eq(accessTokens.userId, userId))
23
+ .orderBy(asc(accessTokens.createdAt));
24
+ return ok({ accessTokens: rows });
25
+ }
26
+ export async function deleteMcpAccessUserData(context, userId) {
27
+ if (!UUID.test(userId))
28
+ return ok();
29
+ await context.db.delete(accessTokens).where(eq(accessTokens.userId, userId));
30
+ return ok();
31
+ }
32
+ export const mcpAccessPrivacyContributor = {
33
+ exportUserData: exportMcpAccessUserData,
34
+ deleteUserData: deleteMcpAccessUserData,
35
+ };
36
+ //# sourceMappingURL=privacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privacy.js","sourceRoot":"","sources":["../../src/server/privacy.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,iGAAiG;AACjG,+EAA+E;AAC/E,OAAO,EAAE,EAAE,EAAwD,MAAM,kBAAkB,CAAC;AAE5F,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5C,MAAM,IAAI,GAAG,iEAAiE,CAAC;AAE/E,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,OAAsB,EAAE,MAAc;IAClF,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;IACxD,2FAA2F;IAC3F,MAAM,EAAE,GAAG,OAAO,CAAC,EAAe,CAAC;IACnC,MAAM,IAAI,GAAG,MAAM,EAAE;SAClB,MAAM,CAAC;QACN,IAAI,EAAE,YAAY,CAAC,IAAI;QACvB,QAAQ,EAAE,YAAY,CAAC,QAAQ;QAC/B,SAAS,EAAE,YAAY,CAAC,SAAS;QACjC,SAAS,EAAE,YAAY,CAAC,SAAS;QACjC,UAAU,EAAE,YAAY,CAAC,UAAU;KACpC,CAAC;SACD,IAAI,CAAC,YAAY,CAAC;SAClB,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;SACtC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;IACxC,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,OAAsB,EAAE,MAAc;IAClF,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,EAAE,CAAC;IACpC,MAAO,OAAO,CAAC,EAAgB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5F,OAAO,EAAE,EAAE,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAuB;IAC7D,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,uBAAuB;CACxC,CAAC"}
@@ -0,0 +1,66 @@
1
+ import { type Err, type ModuleContext, type Ok } from "@softure-ai/core";
2
+ import type { Queryable } from "@softure-ai/db";
3
+ import type { AccessTokenView } from "../contract.js";
4
+ export type McpAccessContext = ModuleContext<Queryable>;
5
+ /** Marks a token as this module's, for people and for secret scanners. */
6
+ export declare const ACCESS_TOKEN_PREFIX = "sftmcp_";
7
+ export declare function hashAccessToken(token: string): string;
8
+ /** Whether a presented value could be a token at all; anything else is not looked up. */
9
+ export declare function isAccessTokenShape(token: string): boolean;
10
+ export interface IssueAccessTokenInput {
11
+ /** The auth user id of the owner, from the session. */
12
+ readonly userId: string;
13
+ readonly name: string;
14
+ /** Asked for write access; granted only while the app allows writes. */
15
+ readonly canWrite: boolean;
16
+ }
17
+ export interface IssuedAccessToken {
18
+ readonly id: string;
19
+ /** The plaintext: shown to the owner once, stored nowhere. */
20
+ readonly token: string;
21
+ readonly name: string;
22
+ readonly canWrite: boolean;
23
+ readonly expiresAt: Date;
24
+ }
25
+ export type IssueAccessTokenResult = Ok<IssuedAccessToken> | Err<"mcp-access.name_required" | "mcp-access.name_too_long" | "mcp-access.token_limit_reached">;
26
+ /**
27
+ * Issues a token for its owner. The per-account limit counts unexpired tokens, in one transaction
28
+ * that holds a per-user advisory lock, so two parallel requests cannot both take the last place.
29
+ * The owner's expired tokens are deleted first: they would otherwise count against the limit.
30
+ * Database failures propagate.
31
+ */
32
+ export declare function issueAccessToken(ctx: McpAccessContext, input: IssueAccessTokenInput): Promise<IssueAccessTokenResult>;
33
+ /** The owner's tokens, newest first, expired ones included (the list marks them). */
34
+ export declare function listAccessTokens(ctx: McpAccessContext, userId: string): Promise<AccessTokenView[]>;
35
+ export interface RevokeAccessTokenInput {
36
+ /** The owner, from the session. */
37
+ readonly userId: string;
38
+ /** From the form: any value, including another account's token id. */
39
+ readonly tokenId: string;
40
+ }
41
+ /**
42
+ * Deletes a token at once; the next request with it gets 401. The owner is part of the `WHERE`,
43
+ * so another account's id matches no row instead of relying on a comparison here.
44
+ */
45
+ export declare function revokeAccessToken(ctx: McpAccessContext, input: RevokeAccessTokenInput): Promise<Ok<undefined> | Err<"mcp-access.token_not_found">>;
46
+ /** The identity behind a valid token. */
47
+ export interface VerifiedAccessToken {
48
+ readonly tokenId: string;
49
+ readonly userId: string;
50
+ /** Effective write access: the token was issued for writes and the app allows them now. */
51
+ readonly canWrite: boolean;
52
+ readonly expiresAt: Date;
53
+ }
54
+ /**
55
+ * The identity behind a token, or null for anything unknown, revoked or expired: the caller cannot
56
+ * tell which, so a guess learns nothing. The expiry sits in the query, so no code path can forget
57
+ * it. Records the use (at most once a minute per token); a failed record is logged and does not
58
+ * fail the request. Database failures of the lookup propagate.
59
+ */
60
+ export declare function verifyAccessToken(ctx: McpAccessContext, token: string): Promise<VerifiedAccessToken | null>;
61
+ /**
62
+ * Deletes every expired token, of every account. Issuing already removes the issuer's own; an app
63
+ * may call this from a scheduled job so abandoned accounts do not keep dead rows.
64
+ */
65
+ export declare function pruneAccessTokens(ctx: McpAccessContext): Promise<number>;
66
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/server/tokens.ts"],"names":[],"mappings":"AAIA,OAAO,EAA0B,KAAK,GAAG,EAAE,KAAK,aAAa,EAAE,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAExD,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAU7C,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,yFAAyF;AACzF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAOD,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,sBAAsB,GAC9B,EAAE,CAAC,iBAAiB,CAAC,GACrB,GAAG,CAAC,0BAA0B,GAAG,0BAA0B,GAAG,gCAAgC,CAAC,CAAC;AAEpG;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAwB3H;AAED,qFAAqF;AACrF,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAaxG;AAED,MAAM,WAAW,sBAAsB;IACrC,mCAAmC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,4BAA4B,CAAC,CAAC,CAO5D;AAED,yCAAyC;AACzC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAgBjH;AAeD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E"}
@@ -0,0 +1,138 @@
1
+ // Access tokens (FIRE_TRACKER `src/db/access-tokens.ts`, generalised). A token is `sftmcp_` and 32
2
+ // random bytes; the database holds only its sha256, so a leaked row cannot be replayed. The
3
+ // plaintext leaves this file once, in the result of `issueAccessToken`.
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { err, errorLogLabel, ok } from "@softure-ai/core";
6
+ import { and, count, desc, eq, gt, isNull, lt, lte, or, sql } from "drizzle-orm";
7
+ import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
8
+ import { accessTokens } from "../schema.js";
9
+ import { getMcpAccessOptions } from "./options.js";
10
+ /** Marks a token as this module's, for people and for secret scanners. */
11
+ export const ACCESS_TOKEN_PREFIX = "sftmcp_";
12
+ const TOKEN_BYTES = 32;
13
+ /** The prefix and base64url of 32 bytes. */
14
+ const TOKEN_SHAPE = /^sftmcp_[A-Za-z0-9_-]{43}$/;
15
+ const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+ const DAY_MS = 24 * 60 * 60 * 1000;
17
+ /** `last_used_at` answers "is this token still in use?"; a minute is precise enough for that. */
18
+ const LAST_USED_RESOLUTION_MS = 60_000;
19
+ export function hashAccessToken(token) {
20
+ return createHash("sha256").update(token, "utf8").digest("hex");
21
+ }
22
+ /** Whether a presented value could be a token at all; anything else is not looked up. */
23
+ export function isAccessTokenShape(token) {
24
+ return TOKEN_SHAPE.test(token);
25
+ }
26
+ function createAccessTokenSecret() {
27
+ const token = `${ACCESS_TOKEN_PREFIX}${randomBytes(TOKEN_BYTES).toString("base64url")}`;
28
+ return { token, tokenHash: hashAccessToken(token) };
29
+ }
30
+ /**
31
+ * Issues a token for its owner. The per-account limit counts unexpired tokens, in one transaction
32
+ * that holds a per-user advisory lock, so two parallel requests cannot both take the last place.
33
+ * The owner's expired tokens are deleted first: they would otherwise count against the limit.
34
+ * Database failures propagate.
35
+ */
36
+ export async function issueAccessToken(ctx, input) {
37
+ const name = input.name.trim();
38
+ if (name === "")
39
+ return err("mcp-access.name_required");
40
+ if (name.length > MAX_TOKEN_NAME_LENGTH)
41
+ return err("mcp-access.name_too_long");
42
+ const options = getMcpAccessOptions(ctx.config);
43
+ const canWrite = input.canWrite && options.allowWrites;
44
+ const now = ctx.clock.now();
45
+ const expiresAt = new Date(now.getTime() + options.tokenLifetimeDays * DAY_MS);
46
+ return ctx.db.transaction(async (tx) => {
47
+ await tx.execute(sql `select pg_advisory_xact_lock(hashtextextended(${`mcp-access:${input.userId}`}, 0))`);
48
+ await tx.delete(accessTokens).where(and(eq(accessTokens.userId, input.userId), lte(accessTokens.expiresAt, now)));
49
+ const [held] = await tx.select({ total: count() }).from(accessTokens).where(eq(accessTokens.userId, input.userId));
50
+ if ((held?.total ?? 0) >= options.maxTokensPerUser)
51
+ return err("mcp-access.token_limit_reached");
52
+ const { token, tokenHash } = createAccessTokenSecret();
53
+ const [row] = await tx
54
+ .insert(accessTokens)
55
+ .values({ userId: input.userId, name, tokenHash, canWrite, createdAt: now, expiresAt })
56
+ .returning();
57
+ if (row === undefined)
58
+ throw new Error("@softure-ai/mcp-access: inserting a token returned no row");
59
+ return ok({ id: row.id, token, name, canWrite, expiresAt });
60
+ });
61
+ }
62
+ /** The owner's tokens, newest first, expired ones included (the list marks them). */
63
+ export async function listAccessTokens(ctx, userId) {
64
+ return ctx.db
65
+ .select({
66
+ id: accessTokens.id,
67
+ name: accessTokens.name,
68
+ canWrite: accessTokens.canWrite,
69
+ createdAt: accessTokens.createdAt,
70
+ expiresAt: accessTokens.expiresAt,
71
+ lastUsedAt: accessTokens.lastUsedAt,
72
+ })
73
+ .from(accessTokens)
74
+ .where(eq(accessTokens.userId, userId))
75
+ .orderBy(desc(accessTokens.createdAt), desc(accessTokens.id));
76
+ }
77
+ /**
78
+ * Deletes a token at once; the next request with it gets 401. The owner is part of the `WHERE`,
79
+ * so another account's id matches no row instead of relying on a comparison here.
80
+ */
81
+ export async function revokeAccessToken(ctx, input) {
82
+ if (!UUID_SHAPE.test(input.tokenId))
83
+ return err("mcp-access.token_not_found");
84
+ const removed = await ctx.db
85
+ .delete(accessTokens)
86
+ .where(and(eq(accessTokens.userId, input.userId), eq(accessTokens.id, input.tokenId)))
87
+ .returning();
88
+ return removed.length > 0 ? ok() : err("mcp-access.token_not_found");
89
+ }
90
+ /**
91
+ * The identity behind a token, or null for anything unknown, revoked or expired: the caller cannot
92
+ * tell which, so a guess learns nothing. The expiry sits in the query, so no code path can forget
93
+ * it. Records the use (at most once a minute per token); a failed record is logged and does not
94
+ * fail the request. Database failures of the lookup propagate.
95
+ */
96
+ export async function verifyAccessToken(ctx, token) {
97
+ if (!isAccessTokenShape(token))
98
+ return null;
99
+ const now = ctx.clock.now();
100
+ const [row] = await ctx.db
101
+ .select({ id: accessTokens.id, userId: accessTokens.userId, canWrite: accessTokens.canWrite, expiresAt: accessTokens.expiresAt })
102
+ .from(accessTokens)
103
+ .where(and(eq(accessTokens.tokenHash, hashAccessToken(token)), gt(accessTokens.expiresAt, now)));
104
+ if (row === undefined)
105
+ return null;
106
+ await recordUse(ctx, row.id, now);
107
+ return {
108
+ tokenId: row.id,
109
+ userId: row.userId,
110
+ canWrite: row.canWrite && getMcpAccessOptions(ctx.config).allowWrites,
111
+ expiresAt: row.expiresAt,
112
+ };
113
+ }
114
+ async function recordUse(ctx, tokenId, now) {
115
+ const staleBefore = new Date(now.getTime() - LAST_USED_RESOLUTION_MS);
116
+ try {
117
+ await ctx.db
118
+ .update(accessTokens)
119
+ .set({ lastUsedAt: now })
120
+ .where(and(eq(accessTokens.id, tokenId), or(isNull(accessTokens.lastUsedAt), lt(accessTokens.lastUsedAt, staleBefore))));
121
+ }
122
+ catch (error) {
123
+ // The token id is not a secret; the token is never logged.
124
+ console.error(`@softure-ai/mcp-access: recording the use of token ${tokenId} failed: ${errorLogLabel(error)}`);
125
+ }
126
+ }
127
+ /**
128
+ * Deletes every expired token, of every account. Issuing already removes the issuer's own; an app
129
+ * may call this from a scheduled job so abandoned accounts do not keep dead rows.
130
+ */
131
+ export async function pruneAccessTokens(ctx) {
132
+ const removed = await ctx.db
133
+ .delete(accessTokens)
134
+ .where(lte(accessTokens.expiresAt, ctx.clock.now()))
135
+ .returning();
136
+ return removed.length;
137
+ }
138
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/server/tokens.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,4FAA4F;AAC5F,wEAAwE;AACxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE,EAAyC,MAAM,kBAAkB,CAAC;AAEjG,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAInD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAE7C,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,4CAA4C;AAC5C,MAAM,WAAW,GAAG,4BAA4B,CAAC;AACjD,MAAM,UAAU,GAAG,iEAAiE,CAAC;AACrF,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACnC,iGAAiG;AACjG,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAEvC,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,uBAAuB;IAC9B,MAAM,KAAK,GAAG,GAAG,mBAAmB,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;IACxF,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;AACtD,CAAC;AAuBD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAqB,EAAE,KAA4B;IACxF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,GAAG,CAAC,0BAA0B,CAAC,CAAC;IACxD,IAAI,IAAI,CAAC,MAAM,GAAG,qBAAqB;QAAE,OAAO,GAAG,CAAC,0BAA0B,CAAC,CAAC;IAEhF,MAAM,OAAO,GAAG,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC;IACvD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,iBAAiB,GAAG,MAAM,CAAC,CAAC;IAE/E,OAAO,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAmC,EAAE;QACtE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,iDAAiD,cAAc,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1G,MAAM,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAClH,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnH,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,gBAAgB;YAAE,OAAO,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAEjG,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,uBAAuB,EAAE,CAAC;QACvD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;aACnB,MAAM,CAAC,YAAY,CAAC;aACpB,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;aACtF,SAAS,EAAE,CAAC;QACf,IAAI,GAAG,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;QACpG,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAqB,EAAE,MAAc;IAC1E,OAAO,GAAG,CAAC,EAAE;SACV,MAAM,CAAC;QACN,EAAE,EAAE,YAAY,CAAC,EAAE;QACnB,IAAI,EAAE,YAAY,CAAC,IAAI;QACvB,QAAQ,EAAE,YAAY,CAAC,QAAQ;QAC/B,SAAS,EAAE,YAAY,CAAC,SAAS;QACjC,SAAS,EAAE,YAAY,CAAC,SAAS;QACjC,UAAU,EAAE,YAAY,CAAC,UAAU;KACpC,CAAC;SACD,IAAI,CAAC,YAAY,CAAC;SAClB,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;SACtC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;AAClE,CAAC;AASD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAqB,EACrB,KAA6B;IAE7B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAC9E,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,EAAE;SACzB,MAAM,CAAC,YAAY,CAAC;SACpB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;SACrF,SAAS,EAAE,CAAC;IACf,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;AACvE,CAAC;AAWD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAqB,EAAE,KAAa;IAC1E,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;IAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE;SACvB,MAAM,CAAC,EAAE,EAAE,EAAE,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAC,SAAS,EAAE,CAAC;SAChI,IAAI,CAAC,YAAY,CAAC;SAClB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACnG,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAClC,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,EAAE;QACf,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,WAAW;QACrE,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAqB,EAAE,OAAe,EAAE,GAAS;IACxE,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,uBAAuB,CAAC,CAAC;IACtE,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,EAAE;aACT,MAAM,CAAC,YAAY,CAAC;aACpB,GAAG,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;aACxB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7H,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,2DAA2D;QAC3D,OAAO,CAAC,KAAK,CAAC,sDAAsD,OAAO,YAAY,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACjH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAqB;IAC3D,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,EAAE;SACzB,MAAM,CAAC,YAAY,CAAC;SACpB,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;SACnD,SAAS,EAAE,CAAC;IACf,OAAO,OAAO,CAAC,MAAM,CAAC;AACxB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { AccessTokenStatus } from "./contract.js";
2
+ export interface AccessTokenStatusOptions {
3
+ /** The app's IANA time zone: calendar days are counted there. */
4
+ readonly timezone: string;
5
+ /** From how many days before the expiry the token counts as expiring. */
6
+ readonly warningDays: number;
7
+ }
8
+ export declare function getAccessTokenStatus(expiresAt: Date, now: Date, options: AccessTokenStatusOptions): AccessTokenStatus;
9
+ //# sourceMappingURL=token-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-status.d.ts","sourceRoot":"","sources":["../src/token-status.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIvD,MAAM,WAAW,wBAAwB;IACvC,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAIrH"}
@@ -0,0 +1,14 @@
1
+ const DAY_MS = 24 * 60 * 60 * 1000;
2
+ export function getAccessTokenStatus(expiresAt, now, options) {
3
+ if (expiresAt.getTime() <= now.getTime())
4
+ return { kind: "expired" };
5
+ const daysLeft = getDayNumber(expiresAt, options.timezone) - getDayNumber(now, options.timezone);
6
+ return daysLeft <= options.warningDays ? { kind: "expiring", daysLeft } : { kind: "active" };
7
+ }
8
+ /** The calendar day of an instant in a time zone, as a day count; two of them subtract to days. */
9
+ function getDayNumber(instant, timezone) {
10
+ const format = new Intl.DateTimeFormat("en-CA", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit" });
11
+ const parts = Object.fromEntries(format.formatToParts(instant).map((part) => [part.type, part.value]));
12
+ return Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day)) / DAY_MS;
13
+ }
14
+ //# sourceMappingURL=token-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-status.js","sourceRoot":"","sources":["../src/token-status.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AASnC,MAAM,UAAU,oBAAoB,CAAC,SAAe,EAAE,GAAS,EAAE,OAAiC;IAChG,IAAI,SAAS,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IACrE,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjG,OAAO,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC/F,CAAC;AAED,mGAAmG;AACnG,SAAS,YAAY,CAAC,OAAa,EAAE,QAAgB;IACnD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IAC3H,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACvG,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;AAC3F,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { TokenManager, type IssueTokenAction, type RevokeTokenAction, type TokenManagerProps, type TokenManagerRow, type TokenManagerSlot, type TokenManagerTool, } from "./token-manager.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,4 @@
1
+ // The token manager, to compose into the app's own page. It takes the server actions from
2
+ // `@softure-ai/mcp-access/next` as props: no `next/*` import here (docs/02-module-standard.md §5).
3
+ export { TokenManager, } from "./token-manager.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,mGAAmG;AACnG,OAAO,EACL,YAAY,GAOb,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { type Locale } from "@softure-ai/core";
2
+ import { type ClassNames } from "@softure-ai/ui";
3
+ import type { IssueTokenFormState, RevokeTokenFormState } from "../contract.js";
4
+ import { type McpAccessMessages } from "../messages/index.js";
5
+ export type IssueTokenAction = (previous: IssueTokenFormState, formData: FormData) => Promise<IssueTokenFormState>;
6
+ export type RevokeTokenAction = (previous: RevokeTokenFormState, formData: FormData) => Promise<RevokeTokenFormState>;
7
+ export type TokenManagerSlot = "root" | "section" | "heading" | "text" | "list" | "item" | "itemHeader" | "badge" | "details" | "snippet" | "code";
8
+ /** One tool of the app's catalog, prepared on the server. */
9
+ export interface TokenManagerTool {
10
+ readonly name: string;
11
+ readonly description: string;
12
+ /** "Reads", "Changes data", or that writes are off, already in the app's copy. */
13
+ readonly accessText: string;
14
+ }
15
+ /** One of the owner's tokens, prepared on the server. */
16
+ export interface TokenManagerRow {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ /** Read only, or read and change, already in the app's copy. */
20
+ readonly scopeText: string;
21
+ /** Active, expiring in n days, or expired. */
22
+ readonly statusText: string;
23
+ readonly isExpired: boolean;
24
+ /** Created, valid until and last used, already formatted. */
25
+ readonly details: readonly string[];
26
+ }
27
+ export interface TokenManagerProps {
28
+ readonly tools: readonly TokenManagerTool[];
29
+ readonly tokens: readonly TokenManagerRow[];
30
+ /** Offers write tokens; off, every token reads only. */
31
+ readonly allowWrites: boolean;
32
+ /** The per-account limit, for the form's note. */
33
+ readonly maxTokens: number;
34
+ readonly issueAction: IssueTokenAction;
35
+ readonly revokeAction: RevokeTokenAction;
36
+ readonly messages: McpAccessMessages;
37
+ /** Locale of the built-in copy of the ui primitives. */
38
+ readonly locale?: Locale;
39
+ readonly classNames?: ClassNames<TokenManagerSlot>;
40
+ readonly unstyled?: boolean;
41
+ }
42
+ export declare function TokenManager({ tools, tokens, allowWrites, maxTokens, issueAction, revokeAction, messages, locale, classNames, unstyled }: TokenManagerProps): import("react").JSX.Element;
43
+ //# sourceMappingURL=token-manager.d.ts.map
@@ -0,0 +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,MAAM,gBAAgB,CAAC;AAC7F,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AASpF,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;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,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,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;AAiKD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAqC3J"}
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { formatMessage } from "@softure-ai/core";
4
+ import { Button, ButtonLink, Checkbox, createSlotClassGetter, FormError, TextField } from "@softure-ai/ui";
5
+ import { useActionState, useState } from "react";
6
+ import { getTokenErrorMessage } from "../messages/index.js";
7
+ import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
8
+ const DEFAULT_CLASSES = {
9
+ root: "sft:flex sft:flex-col sft:gap-4 sft:font-sans",
10
+ section: "sft:flex sft:flex-col sft:gap-3 sft:border-t sft:border-border sft:pt-4 sft:first-of-type:border-t-0 sft:first-of-type:pt-0",
11
+ heading: "sft:m-0 sft:text-base sft:font-semibold sft:text-foreground",
12
+ text: "sft:m-0 sft:text-sm sft:text-muted",
13
+ list: "sft:m-0 sft:flex sft:list-none sft:flex-col sft:gap-3 sft:p-0",
14
+ item: "sft:flex sft:flex-col sft:gap-1",
15
+ itemHeader: "sft:flex sft:items-center sft:justify-between sft:gap-2",
16
+ badge: "sft:text-xs sft:font-medium sft:text-muted",
17
+ details: "sft:m-0 sft:text-xs sft:text-muted",
18
+ snippet: "sft:flex sft:flex-col sft:gap-1.5",
19
+ code: "sft:m-0 sft:block sft:overflow-x-auto sft:whitespace-normal sft:break-words sft:rounded-control sft:border sft:border-border sft:bg-surface-raised sft:p-4 sft:font-mono sft:text-xs sft:text-foreground",
20
+ };
21
+ /** A labelled snippet with a copy button. */
22
+ function Snippet({ title, value, messages, slot, unstyled }) {
23
+ const [isCopied, setIsCopied] = useState(false);
24
+ function copy() {
25
+ void navigator.clipboard.writeText(value).then(() => setIsCopied(true),
26
+ // Clipboard access can be refused; the text stays on screen to select by hand.
27
+ () => setIsCopied(false));
28
+ }
29
+ return (_jsxs("div", { className: slot("snippet"), children: [_jsxs("div", { className: slot("itemHeader"), children: [_jsx("p", { className: slot("heading"), children: title }), _jsx(Button, { variant: "secondary", size: "sm", onClick: copy, unstyled: unstyled, children: isCopied ? messages.issued.copied : messages.issued.copy })] }), _jsx("pre", { className: slot("code"), children: value })] }));
30
+ }
31
+ function IssuedTokenPanel({ issued, onDone, messages, slot, unstyled, }) {
32
+ const snippet = (title, value) => _jsx(Snippet, { title: title, value: value, messages: messages, slot: slot, unstyled: unstyled });
33
+ return (_jsxs("section", { className: slot("section"), children: [_jsx("h3", { 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
+ }
35
+ function IssueForm({ action, allowWrites, maxTokens, messages, locale, slot, unstyled, }) {
36
+ const [state, formAction, isPending] = useActionState(action, { status: "idle" });
37
+ // The id of the issued token the owner dismissed; a new one shows again.
38
+ const [dismissedId, setDismissedId] = useState(null);
39
+ const issued = state.status === "ok" && state.issued.id !== dismissedId ? state.issued : null;
40
+ return (_jsxs(_Fragment, { children: [_jsxs("section", { className: slot("section"), children: [_jsx("h2", { className: slot("heading"), children: messages.issue.title }), _jsxs("form", { 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" ? 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: () => setDismissedId(issued.id), messages: messages, slot: slot, unstyled: unstyled })] }));
41
+ }
42
+ function TokenItem({ row, action, messages, slot, unstyled, }) {
43
+ const [state, formAction, isPending] = useActionState(action, { status: "idle" });
44
+ return (_jsxs("li", { className: slot("item"), "data-token-id": row.id, children: [_jsxs("div", { className: slot("itemHeader"), children: [_jsx("p", { className: slot("heading"), children: row.name }), _jsxs("form", { action: formAction, children: [_jsx("input", { type: "hidden", name: "id", value: row.id }), _jsx(Button, { type: "submit", variant: "danger", size: "sm", pending: isPending, "aria-label": `${messages.list.revoke}: ${row.name}`, unstyled: unstyled, children: isPending ? messages.list.revoking : messages.list.revoke })] })] }), _jsxs("p", { className: row.isExpired ? slot("details") : slot("badge"), children: [row.statusText, " \u00B7 ", row.scopeText] }), _jsx("p", { className: slot("details"), children: row.details.join(" · ") }), _jsx(FormError, { message: state.status === "error" ? getTokenErrorMessage(messages, state.error) : undefined, unstyled: unstyled })] }));
45
+ }
46
+ export function TokenManager({ tools, tokens, allowWrites, maxTokens, issueAction, revokeAction, messages, locale, classNames, unstyled }) {
47
+ const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
48
+ return (_jsxs("div", { className: slot("root"), children: [_jsxs("section", { className: slot("section"), children: [_jsx("h2", { className: slot("heading"), children: messages.tools.title }), tools.length === 0 ? (_jsx("p", { className: slot("text"), children: messages.tools.empty })) : (_jsx("ul", { className: slot("list"), children: tools.map((tool) => (_jsxs("li", { className: slot("item"), children: [_jsxs("div", { className: slot("itemHeader"), children: [_jsx("code", { className: slot("heading"), children: tool.name }), _jsx("span", { className: slot("badge"), children: tool.accessText })] }), _jsx("p", { className: slot("text"), children: tool.description })] }, tool.name))) }))] }), _jsx(IssueForm, { action: issueAction, allowWrites: allowWrites, maxTokens: maxTokens, messages: messages, locale: locale, slot: slot, unstyled: unstyled }), _jsxs("section", { className: slot("section"), children: [_jsx("h2", { className: slot("heading"), children: messages.list.title }), tokens.length === 0 ? (_jsx("p", { className: slot("text"), children: messages.list.empty })) : (_jsx("ul", { className: slot("list"), children: tokens.map((row) => (_jsx(TokenItem, { row: row, action: revokeAction, messages: messages, slot: slot, unstyled: unstyled }, row.id))) }))] })] }));
49
+ }
50
+ //# sourceMappingURL=token-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/ui/token-manager.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ -- Access tokens for MCP clients. Only the sha256 of a token is stored: the plaintext leaves the
2
+ -- server once, in the answer to the issue action. A token belongs to one account and goes with it.
3
+ -- Rollback: DROP TABLE mcp.access_tokens; then
4
+ -- DELETE FROM softure.migrations WHERE module = 'mcp-access' AND version = 1;
5
+ CREATE TABLE access_tokens (
6
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ user_id uuid NOT NULL REFERENCES auth.users (id) ON DELETE CASCADE,
8
+ -- The owner's label for the client that holds the token, e.g. "Laptop".
9
+ name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 60 AND name = btrim(name)),
10
+ token_hash text NOT NULL CHECK (token_hash ~ '^[0-9a-f]{64}$'),
11
+ -- Issued with write access; writes also need the app's allowWrites option.
12
+ can_write boolean NOT NULL,
13
+ created_at timestamptz NOT NULL,
14
+ expires_at timestamptz NOT NULL,
15
+ -- Informational, written at most once a minute per token; never an access decision.
16
+ last_used_at timestamptz,
17
+ CHECK (expires_at > created_at),
18
+ CONSTRAINT access_tokens_token_hash_key UNIQUE (token_hash)
19
+ );
20
+
21
+ -- The owner's list, newest first, and the per-account limit.
22
+ CREATE INDEX access_tokens_user_id_created_at_idx ON access_tokens (user_id, created_at DESC);
23
+ -- Pruning deletes by expiry.
24
+ CREATE INDEX access_tokens_expires_at_idx ON access_tokens (expires_at);
package/module.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "mcp-access",
3
+ "version": "0.1.6",
4
+ "dependsOn": { "security": "^0.1.0", "auth": "^0.1.0" },
5
+ "dbSchema": "mcp",
6
+ "tables": ["access_tokens"],
7
+ "env": [],
8
+ "switches": [],
9
+ "routes": { "page": "/account/mcp", "endpoint": "/api/mcp" },
10
+ "mount": [
11
+ { "kind": "page", "path": "app/account/mcp/page.tsx", "export": "McpAccessPage" },
12
+ { "kind": "route-handler", "path": "app/api/mcp/route.ts", "export": "createMcpRoute" }
13
+ ],
14
+ "privacy": { "exports": true, "deletes": true }
15
+ }