diff --git a/README.md b/README.md index 25f3702..7f28d77 100644 --- a/README.md +++ b/README.md @@ -916,6 +916,7 @@ Create a new API token. The response includes the secret `token` value — this **Parameters:** - `name` (required): Display name for the token +- `expires_at` (optional): Token expiration as an ISO 8601 date-time. Omit for the server default (a 1-year default is being rolled out); pass an explicit `null` for a token that never expires. Past values or values more than 5 years ahead are rejected - `resources` (optional): Array of resource permissions to scope the token to. Each entry has: - `resource_type` (required): One of `account`, `project`, `inbox`, `domain`, `billing` - `resource_id` (required): ID of the resource @@ -936,6 +937,7 @@ Reset (rotate) an API token by ID. The response includes the **new** secret `tok **Parameters:** - `api_token_id` (required): ID of the API token to reset +- `expires_at` (optional): Expiration for the new token as an ISO 8601 date-time. Omit for the server default (a 1-year default is being rolled out); pass an explicit `null` for a token that never expires. Past values or values more than 5 years ahead are rejected ### delete-api-token diff --git a/src/server.ts b/src/server.ts index e35df74..7cf0051 100644 --- a/src/server.ts +++ b/src/server.ts @@ -192,6 +192,7 @@ import { createApiTokenSchema, apiTokenSchema, getApiToken, + resetApiTokenSchema, resetApiToken, deleteApiToken, } from "./tools/apiTokens"; @@ -1032,7 +1033,7 @@ const tools = [ name: "reset-api-token", description: "Reset (rotate) an API token by ID. The response includes the **new** secret `token` value — returned only on this call, so store it immediately. The previous token is invalidated.", - inputSchema: apiTokenSchema, + inputSchema: resetApiTokenSchema, handler: resetApiToken, annotations: { destructiveHint: true, diff --git a/src/tools/apiTokens/__tests__/createApiToken.test.ts b/src/tools/apiTokens/__tests__/createApiToken.test.ts index 9b2ea7c..0ffbd1f 100644 --- a/src/tools/apiTokens/__tests__/createApiToken.test.ts +++ b/src/tools/apiTokens/__tests__/createApiToken.test.ts @@ -64,6 +64,40 @@ describe("createApiToken", () => { }); }); + it("forwards expires_at when provided", async () => { + mockClient.general.apiTokens.create.mockResolvedValue({ + id: 9, + name: "Expiring", + expires_at: "2027-06-01T00:00:00Z", + token: "mt-token-expiring", + }); + + await createApiToken({ + name: "Expiring", + expires_at: "2027-06-01T00:00:00Z", + }); + + expect(mockClient.general.apiTokens.create).toHaveBeenCalledWith({ + name: "Expiring", + expires_at: "2027-06-01T00:00:00Z", + }); + }); + + it("forwards an explicit null expires_at for a token that never expires", async () => { + mockClient.general.apiTokens.create.mockResolvedValue({ + id: 10, + name: "Forever", + expires_at: null, + token: "mt-token-forever", + }); + + await createApiToken({ name: "Forever", expires_at: null }); + + const callArg = mockClient.general.apiTokens.create.mock.calls[0][0]; + expect("expires_at" in callArg).toBe(true); + expect(callArg.expires_at).toBeNull(); + }); + it("surfaces API errors", async () => { mockClient.general.apiTokens.create.mockRejectedValue( new Error("name taken") diff --git a/src/tools/apiTokens/__tests__/resetApiToken.test.ts b/src/tools/apiTokens/__tests__/resetApiToken.test.ts index 93fe852..eb1af71 100644 --- a/src/tools/apiTokens/__tests__/resetApiToken.test.ts +++ b/src/tools/apiTokens/__tests__/resetApiToken.test.ts @@ -34,6 +34,55 @@ describe("resetApiToken", () => { expect(result.isError).toBeUndefined(); }); + it("forwards expires_at for the new token when provided", async () => { + mockClient.general.apiTokens.reset.mockResolvedValue({ + id: 7, + name: "CI", + expires_at: "2027-06-01T00:00:00Z", + token: "mt-token-new", + }); + + await resetApiToken({ + api_token_id: 7, + expires_at: "2027-06-01T00:00:00Z", + }); + + expect(mockClient.general.apiTokens.reset).toHaveBeenCalledWith(7, { + expires_at: "2027-06-01T00:00:00Z", + }); + }); + + it("forwards an explicit null expires_at for a token that never expires", async () => { + mockClient.general.apiTokens.reset.mockResolvedValue({ + id: 7, + name: "CI", + expires_at: null, + token: "mt-token-new", + }); + + await resetApiToken({ api_token_id: 7, expires_at: null }); + + expect(mockClient.general.apiTokens.reset).toHaveBeenCalledWith(7, { + expires_at: null, + }); + }); + + it("surfaces server-side expiration validation errors", async () => { + mockClient.general.apiTokens.reset.mockRejectedValue( + new Error("expires_at: must not be in the past") + ); + + const result = await resetApiToken({ + api_token_id: 7, + expires_at: "2020-01-01T00:00:00Z", + }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toBe( + "Failed to reset API token: expires_at: must not be in the past" + ); + }); + it("surfaces API errors", async () => { mockClient.general.apiTokens.reset.mockRejectedValue( new Error("not found") diff --git a/src/tools/apiTokens/index.ts b/src/tools/apiTokens/index.ts index 405f539..076a240 100644 --- a/src/tools/apiTokens/index.ts +++ b/src/tools/apiTokens/index.ts @@ -4,6 +4,7 @@ import createApiTokenSchema from "./schemas/createApiToken"; import createApiToken from "./createApiToken"; import apiTokenSchema from "./schemas/apiToken"; import getApiToken from "./getApiToken"; +import resetApiTokenSchema from "./schemas/resetApiToken"; import resetApiToken from "./resetApiToken"; import deleteApiToken from "./deleteApiToken"; @@ -14,6 +15,7 @@ export { createApiToken, apiTokenSchema, getApiToken, + resetApiTokenSchema, resetApiToken, deleteApiToken, }; diff --git a/src/tools/apiTokens/resetApiToken.ts b/src/tools/apiTokens/resetApiToken.ts index 71be665..a50a207 100644 --- a/src/tools/apiTokens/resetApiToken.ts +++ b/src/tools/apiTokens/resetApiToken.ts @@ -1,18 +1,33 @@ import { requireClient } from "../../client"; -import { ApiTokenRequest } from "../../types/mailtrap"; +import { ResetApiTokenRequest } from "../../types/mailtrap"; import { buildErrorResponse, buildSuccessResponse, ToolResponse, } from "../utils/responses"; -async function resetApiToken({ - api_token_id, -}: ApiTokenRequest): Promise { +async function resetApiToken( + params: ResetApiTokenRequest +): Promise { try { const mailtrap = requireClient("API tokens"); - const response = await mailtrap.general.apiTokens.reset(api_token_id); + // mailtrap@4.8 typings don't know the optional reset body yet – widen the + // signature locally and drop this cast once the dependency is bumped to + // the release that ships MT-23076. + const apiTokens = mailtrap.general.apiTokens as unknown as { + reset: ( + id: number, + resetParams?: { expires_at?: string | null } + ) => Promise; + }; + + const response = + "expires_at" in params + ? await apiTokens.reset(params.api_token_id, { + expires_at: params.expires_at, + }) + : await apiTokens.reset(params.api_token_id); return buildSuccessResponse(JSON.stringify(response, null, 2)); } catch (error) { diff --git a/src/tools/apiTokens/schemas/createApiToken.ts b/src/tools/apiTokens/schemas/createApiToken.ts index 402b445..54f1506 100644 --- a/src/tools/apiTokens/schemas/createApiToken.ts +++ b/src/tools/apiTokens/schemas/createApiToken.ts @@ -5,6 +5,11 @@ const createApiTokenSchema = { type: "string", description: "Display name for the API token.", }, + expires_at: { + type: ["string", "null"], + description: + "Optional token expiration as an ISO 8601 date-time (e.g. 2027-06-01T00:00:00Z). Omit for the server default (a 1-year default is being rolled out). Pass an explicit null for a token that never expires. Past values or values more than 5 years ahead are rejected with a 422 error.", + }, resources: { type: "array", description: diff --git a/src/tools/apiTokens/schemas/resetApiToken.ts b/src/tools/apiTokens/schemas/resetApiToken.ts new file mode 100644 index 0000000..1e6b06a --- /dev/null +++ b/src/tools/apiTokens/schemas/resetApiToken.ts @@ -0,0 +1,23 @@ +/** + * Input schema for reset-api-token: `api_token_id` plus an optional + * `expires_at` for the replacement token. Kept separate from the shared + * `apiTokenSchema` (get, delete), which allows no extra properties. + */ +const resetApiTokenSchema = { + type: "object", + properties: { + api_token_id: { + type: "number", + description: "ID of the API token.", + }, + expires_at: { + type: ["string", "null"], + description: + "Optional expiration for the new token as an ISO 8601 date-time (e.g. 2027-06-01T00:00:00Z). Omit for the server default (a 1-year default is being rolled out). Pass an explicit null for a token that never expires. Past values or values more than 5 years ahead are rejected with a 422 error.", + }, + }, + required: ["api_token_id"], + additionalProperties: false, +}; + +export default resetApiTokenSchema; diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 7950544..6619610 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -667,6 +667,7 @@ export interface ApiTokenResourcePermission { export interface CreateApiTokenRequest { name: string; + expires_at?: string | null; resources?: ApiTokenResourcePermission[]; } @@ -674,6 +675,11 @@ export interface ApiTokenRequest { api_token_id: number; } +export interface ResetApiTokenRequest { + api_token_id: number; + expires_at?: string | null; +} + // --- Organization / sub-account types --- export interface SubAccount {