Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ import {
createApiTokenSchema,
apiTokenSchema,
getApiToken,
resetApiTokenSchema,
resetApiToken,
deleteApiToken,
} from "./tools/apiTokens";
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions src/tools/apiTokens/__tests__/createApiToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
49 changes: 49 additions & 0 deletions src/tools/apiTokens/__tests__/resetApiToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/tools/apiTokens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -14,6 +15,7 @@ export {
createApiToken,
apiTokenSchema,
getApiToken,
resetApiTokenSchema,
resetApiToken,
deleteApiToken,
};
25 changes: 20 additions & 5 deletions src/tools/apiTokens/resetApiToken.ts
Original file line number Diff line number Diff line change
@@ -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<ToolResponse> {
async function resetApiToken(
params: ResetApiTokenRequest
): Promise<ToolResponse> {
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<unknown>;
};

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) {
Expand Down
5 changes: 5 additions & 0 deletions src/tools/apiTokens/schemas/createApiToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions src/tools/apiTokens/schemas/resetApiToken.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions src/types/mailtrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,13 +667,19 @@ export interface ApiTokenResourcePermission {

export interface CreateApiTokenRequest {
name: string;
expires_at?: string | null;
resources?: ApiTokenResourcePermission[];
}

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 {
Expand Down
Loading