@hasna/connectors 0.3.1 → 0.3.2
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/bin/index.js +229 -59
- package/bin/mcp.js +214 -71
- package/bin/serve.js +0 -16
- package/connectors/connect-aws/package.json +4 -1
- package/connectors/connect-aws/src/api/awsses.ts +311 -0
- package/connectors/connect-aws/src/api/client.ts +1 -0
- package/connectors/connect-aws/src/api/index.ts +5 -1
- package/connectors/connect-aws/src/index.ts +1 -1
- package/connectors/connect-aws/src/types/index.ts +91 -0
- package/dashboard/dist/assets/index-CSlS3oNV.css +1 -0
- package/dashboard/dist/assets/index-sSIkMXYs.js +284 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +121 -0
- package/dist/lib/runner.d.ts +44 -0
- package/dist/lib/runner.test.d.ts +1 -0
- package/package.json +3 -1
- package/dashboard/dist/assets/index-DmR_QNtT.css +0 -1
- package/dashboard/dist/assets/index-Dp-apHbC.js +0 -284
package/bin/mcp.js
CHANGED
|
@@ -4569,7 +4569,6 @@ var require_limitLength = __commonJS((exports) => {
|
|
|
4569
4569
|
var require_pattern = __commonJS((exports) => {
|
|
4570
4570
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4571
4571
|
var code_1 = require_code2();
|
|
4572
|
-
var util_1 = require_util();
|
|
4573
4572
|
var codegen_1 = require_codegen();
|
|
4574
4573
|
var error2 = {
|
|
4575
4574
|
message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
|
|
@@ -4582,18 +4581,10 @@ var require_pattern = __commonJS((exports) => {
|
|
|
4582
4581
|
$data: true,
|
|
4583
4582
|
error: error2,
|
|
4584
4583
|
code(cxt) {
|
|
4585
|
-
const {
|
|
4584
|
+
const { data, $data, schema, schemaCode, it } = cxt;
|
|
4586
4585
|
const u = it.opts.unicodeRegExp ? "u" : "";
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
|
|
4590
|
-
const valid = gen.let("valid");
|
|
4591
|
-
gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
|
|
4592
|
-
cxt.fail$data((0, codegen_1._)`!${valid}`);
|
|
4593
|
-
} else {
|
|
4594
|
-
const regExp = (0, code_1.usePattern)(cxt, schema);
|
|
4595
|
-
cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
|
|
4596
|
-
}
|
|
4586
|
+
const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema);
|
|
4587
|
+
cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
|
|
4597
4588
|
}
|
|
4598
4589
|
};
|
|
4599
4590
|
exports.default = def;
|
|
@@ -6286,7 +6277,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
6286
6277
|
}
|
|
6287
6278
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6288
6279
|
function getTime(strictTimeZone) {
|
|
6289
|
-
return function
|
|
6280
|
+
return function time3(str) {
|
|
6290
6281
|
const matches = TIME.exec(str);
|
|
6291
6282
|
if (!matches)
|
|
6292
6283
|
return false;
|
|
@@ -18120,62 +18111,6 @@ class ExperimentalServerTasks {
|
|
|
18120
18111
|
requestStream(request, resultSchema, options) {
|
|
18121
18112
|
return this._server.requestStream(request, resultSchema, options);
|
|
18122
18113
|
}
|
|
18123
|
-
createMessageStream(params, options) {
|
|
18124
|
-
const clientCapabilities = this._server.getClientCapabilities();
|
|
18125
|
-
if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
|
|
18126
|
-
throw new Error("Client does not support sampling tools capability.");
|
|
18127
|
-
}
|
|
18128
|
-
if (params.messages.length > 0) {
|
|
18129
|
-
const lastMessage = params.messages[params.messages.length - 1];
|
|
18130
|
-
const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
|
|
18131
|
-
const hasToolResults = lastContent.some((c) => c.type === "tool_result");
|
|
18132
|
-
const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
|
|
18133
|
-
const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
|
|
18134
|
-
const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
|
|
18135
|
-
if (hasToolResults) {
|
|
18136
|
-
if (lastContent.some((c) => c.type !== "tool_result")) {
|
|
18137
|
-
throw new Error("The last message must contain only tool_result content if any is present");
|
|
18138
|
-
}
|
|
18139
|
-
if (!hasPreviousToolUse) {
|
|
18140
|
-
throw new Error("tool_result blocks are not matching any tool_use from the previous message");
|
|
18141
|
-
}
|
|
18142
|
-
}
|
|
18143
|
-
if (hasPreviousToolUse) {
|
|
18144
|
-
const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
|
|
18145
|
-
const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
|
|
18146
|
-
if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
|
|
18147
|
-
throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
|
|
18148
|
-
}
|
|
18149
|
-
}
|
|
18150
|
-
}
|
|
18151
|
-
return this.requestStream({
|
|
18152
|
-
method: "sampling/createMessage",
|
|
18153
|
-
params
|
|
18154
|
-
}, CreateMessageResultSchema, options);
|
|
18155
|
-
}
|
|
18156
|
-
elicitInputStream(params, options) {
|
|
18157
|
-
const clientCapabilities = this._server.getClientCapabilities();
|
|
18158
|
-
const mode = params.mode ?? "form";
|
|
18159
|
-
switch (mode) {
|
|
18160
|
-
case "url": {
|
|
18161
|
-
if (!clientCapabilities?.elicitation?.url) {
|
|
18162
|
-
throw new Error("Client does not support url elicitation.");
|
|
18163
|
-
}
|
|
18164
|
-
break;
|
|
18165
|
-
}
|
|
18166
|
-
case "form": {
|
|
18167
|
-
if (!clientCapabilities?.elicitation?.form) {
|
|
18168
|
-
throw new Error("Client does not support form elicitation.");
|
|
18169
|
-
}
|
|
18170
|
-
break;
|
|
18171
|
-
}
|
|
18172
|
-
}
|
|
18173
|
-
const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
|
|
18174
|
-
return this.requestStream({
|
|
18175
|
-
method: "elicitation/create",
|
|
18176
|
-
params: normalizedParams
|
|
18177
|
-
}, ElicitResultSchema, options);
|
|
18178
|
-
}
|
|
18179
18114
|
async getTask(taskId, options) {
|
|
18180
18115
|
return this._server.getTask({ taskId }, options);
|
|
18181
18116
|
}
|
|
@@ -20271,11 +20206,110 @@ function guessKeyField(name) {
|
|
|
20271
20206
|
return "apiKey";
|
|
20272
20207
|
}
|
|
20273
20208
|
|
|
20209
|
+
// src/lib/runner.ts
|
|
20210
|
+
import { existsSync as existsSync4 } from "fs";
|
|
20211
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
20212
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
20213
|
+
import { spawn } from "child_process";
|
|
20214
|
+
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
20215
|
+
function resolveConnectorsDir2() {
|
|
20216
|
+
const fromBin = join4(__dirname3, "..", "connectors");
|
|
20217
|
+
if (existsSync4(fromBin))
|
|
20218
|
+
return fromBin;
|
|
20219
|
+
const fromSrc = join4(__dirname3, "..", "..", "connectors");
|
|
20220
|
+
if (existsSync4(fromSrc))
|
|
20221
|
+
return fromSrc;
|
|
20222
|
+
return fromBin;
|
|
20223
|
+
}
|
|
20224
|
+
var CONNECTORS_DIR2 = resolveConnectorsDir2();
|
|
20225
|
+
function getConnectorCliPath(name) {
|
|
20226
|
+
const safeName = name.replace(/[^a-z0-9-]/g, "");
|
|
20227
|
+
const connectorDir = join4(CONNECTORS_DIR2, `connect-${safeName}`);
|
|
20228
|
+
const cliPath = join4(connectorDir, "src", "cli", "index.ts");
|
|
20229
|
+
if (existsSync4(cliPath))
|
|
20230
|
+
return cliPath;
|
|
20231
|
+
return null;
|
|
20232
|
+
}
|
|
20233
|
+
function runConnectorCommand(name, args, timeoutMs = 30000) {
|
|
20234
|
+
const cliPath = getConnectorCliPath(name);
|
|
20235
|
+
if (!cliPath) {
|
|
20236
|
+
return Promise.resolve({
|
|
20237
|
+
stdout: "",
|
|
20238
|
+
stderr: `Connector '${name}' not found or has no CLI.`,
|
|
20239
|
+
exitCode: 1,
|
|
20240
|
+
success: false
|
|
20241
|
+
});
|
|
20242
|
+
}
|
|
20243
|
+
return new Promise((resolve) => {
|
|
20244
|
+
const proc = spawn("bun", ["run", cliPath, ...args], {
|
|
20245
|
+
timeout: timeoutMs,
|
|
20246
|
+
env: { ...process.env },
|
|
20247
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
20248
|
+
});
|
|
20249
|
+
let stdout = "";
|
|
20250
|
+
let stderr = "";
|
|
20251
|
+
proc.stdout.on("data", (data) => {
|
|
20252
|
+
stdout += data.toString();
|
|
20253
|
+
});
|
|
20254
|
+
proc.stderr.on("data", (data) => {
|
|
20255
|
+
stderr += data.toString();
|
|
20256
|
+
});
|
|
20257
|
+
proc.on("close", (code) => {
|
|
20258
|
+
resolve({
|
|
20259
|
+
stdout: stdout.trim(),
|
|
20260
|
+
stderr: stderr.trim(),
|
|
20261
|
+
exitCode: code ?? 1,
|
|
20262
|
+
success: code === 0
|
|
20263
|
+
});
|
|
20264
|
+
});
|
|
20265
|
+
proc.on("error", (err) => {
|
|
20266
|
+
resolve({
|
|
20267
|
+
stdout: "",
|
|
20268
|
+
stderr: err.message,
|
|
20269
|
+
exitCode: 1,
|
|
20270
|
+
success: false
|
|
20271
|
+
});
|
|
20272
|
+
});
|
|
20273
|
+
});
|
|
20274
|
+
}
|
|
20275
|
+
async function getConnectorOperations(name) {
|
|
20276
|
+
const cliPath = getConnectorCliPath(name);
|
|
20277
|
+
if (!cliPath) {
|
|
20278
|
+
return { commands: [], helpText: "", hasCli: false };
|
|
20279
|
+
}
|
|
20280
|
+
const result = await runConnectorCommand(name, ["--help"]);
|
|
20281
|
+
const helpText = result.stdout || result.stderr;
|
|
20282
|
+
const commands = [];
|
|
20283
|
+
const lines = helpText.split(`
|
|
20284
|
+
`);
|
|
20285
|
+
let inCommands = false;
|
|
20286
|
+
for (const line of lines) {
|
|
20287
|
+
if (line.trim().startsWith("Commands:")) {
|
|
20288
|
+
inCommands = true;
|
|
20289
|
+
continue;
|
|
20290
|
+
}
|
|
20291
|
+
if (inCommands) {
|
|
20292
|
+
const match = line.match(/^\s{2,}(\S+)/);
|
|
20293
|
+
if (match && match[1] !== "help") {
|
|
20294
|
+
commands.push(match[1]);
|
|
20295
|
+
}
|
|
20296
|
+
if (line.trim() === "" && commands.length > 0) {
|
|
20297
|
+
inCommands = false;
|
|
20298
|
+
}
|
|
20299
|
+
}
|
|
20300
|
+
}
|
|
20301
|
+
return { commands, helpText, hasCli: true };
|
|
20302
|
+
}
|
|
20303
|
+
async function getConnectorCommandHelp(name, command) {
|
|
20304
|
+
const result = await runConnectorCommand(name, [command, "--help"]);
|
|
20305
|
+
return result.stdout || result.stderr;
|
|
20306
|
+
}
|
|
20307
|
+
|
|
20274
20308
|
// src/mcp/index.ts
|
|
20275
20309
|
loadConnectorVersions();
|
|
20276
20310
|
var server = new McpServer({
|
|
20277
20311
|
name: "connectors",
|
|
20278
|
-
version: "0.3.
|
|
20312
|
+
version: "0.3.2"
|
|
20279
20313
|
});
|
|
20280
20314
|
server.registerTool("search_connectors", {
|
|
20281
20315
|
title: "Search Connectors",
|
|
@@ -20523,6 +20557,111 @@ server.registerTool("list_categories", {
|
|
|
20523
20557
|
]
|
|
20524
20558
|
};
|
|
20525
20559
|
});
|
|
20560
|
+
server.registerTool("list_connector_operations", {
|
|
20561
|
+
title: "List Connector Operations",
|
|
20562
|
+
description: "Discover available API operations for a connector. Returns CLI commands the connector supports (e.g. messages, products, customers). Use this before run_connector_operation to know what's available.",
|
|
20563
|
+
inputSchema: {
|
|
20564
|
+
name: exports_external.string().describe("Connector name (e.g. stripe, gmail, anthropic)"),
|
|
20565
|
+
command: exports_external.string().optional().describe("Get detailed help for a specific subcommand (e.g. products, messages)")
|
|
20566
|
+
}
|
|
20567
|
+
}, async ({ name, command }) => {
|
|
20568
|
+
const meta = getConnector(name);
|
|
20569
|
+
if (!meta) {
|
|
20570
|
+
return {
|
|
20571
|
+
content: [{ type: "text", text: `Connector '${name}' not found.` }],
|
|
20572
|
+
isError: true
|
|
20573
|
+
};
|
|
20574
|
+
}
|
|
20575
|
+
if (!getConnectorCliPath(name)) {
|
|
20576
|
+
return {
|
|
20577
|
+
content: [
|
|
20578
|
+
{
|
|
20579
|
+
type: "text",
|
|
20580
|
+
text: `Connector '${name}' does not have a CLI. It may be API-only.`
|
|
20581
|
+
}
|
|
20582
|
+
],
|
|
20583
|
+
isError: true
|
|
20584
|
+
};
|
|
20585
|
+
}
|
|
20586
|
+
if (command) {
|
|
20587
|
+
const help = await getConnectorCommandHelp(name, command);
|
|
20588
|
+
return {
|
|
20589
|
+
content: [
|
|
20590
|
+
{
|
|
20591
|
+
type: "text",
|
|
20592
|
+
text: JSON.stringify({ connector: name, command, help }, null, 2)
|
|
20593
|
+
}
|
|
20594
|
+
]
|
|
20595
|
+
};
|
|
20596
|
+
}
|
|
20597
|
+
const ops = await getConnectorOperations(name);
|
|
20598
|
+
return {
|
|
20599
|
+
content: [
|
|
20600
|
+
{
|
|
20601
|
+
type: "text",
|
|
20602
|
+
text: JSON.stringify({
|
|
20603
|
+
connector: name,
|
|
20604
|
+
displayName: meta.displayName,
|
|
20605
|
+
commands: ops.commands,
|
|
20606
|
+
helpText: ops.helpText
|
|
20607
|
+
}, null, 2)
|
|
20608
|
+
}
|
|
20609
|
+
]
|
|
20610
|
+
};
|
|
20611
|
+
});
|
|
20612
|
+
server.registerTool("run_connector_operation", {
|
|
20613
|
+
title: "Run Connector Operation",
|
|
20614
|
+
description: "Execute an API operation on a connector. Pass the connector name and CLI arguments. Use list_connector_operations first to discover available commands. Example: name='stripe', args=['products', 'list', '--limit', '5']",
|
|
20615
|
+
inputSchema: {
|
|
20616
|
+
name: exports_external.string().describe("Connector name (e.g. stripe, gmail, anthropic)"),
|
|
20617
|
+
args: exports_external.array(exports_external.string()).describe("CLI arguments for the connector command (e.g. ['products', 'list', '--limit', '5'])"),
|
|
20618
|
+
format: exports_external.enum(["json", "pretty"]).optional().describe("Output format (default: json for structured parsing)"),
|
|
20619
|
+
timeout: exports_external.number().optional().describe("Timeout in milliseconds (default: 30000)")
|
|
20620
|
+
}
|
|
20621
|
+
}, async ({ name, args, format, timeout }) => {
|
|
20622
|
+
const meta = getConnector(name);
|
|
20623
|
+
if (!meta) {
|
|
20624
|
+
return {
|
|
20625
|
+
content: [{ type: "text", text: `Connector '${name}' not found.` }],
|
|
20626
|
+
isError: true
|
|
20627
|
+
};
|
|
20628
|
+
}
|
|
20629
|
+
const finalArgs = [...args];
|
|
20630
|
+
if (format) {
|
|
20631
|
+
finalArgs.push("--format", format);
|
|
20632
|
+
} else if (!args.includes("--format") && !args.includes("-f")) {
|
|
20633
|
+
finalArgs.push("--format", "json");
|
|
20634
|
+
}
|
|
20635
|
+
const result = await runConnectorCommand(name, finalArgs, timeout ?? 30000);
|
|
20636
|
+
if (!result.success) {
|
|
20637
|
+
return {
|
|
20638
|
+
content: [
|
|
20639
|
+
{
|
|
20640
|
+
type: "text",
|
|
20641
|
+
text: JSON.stringify({
|
|
20642
|
+
connector: name,
|
|
20643
|
+
success: false,
|
|
20644
|
+
error: result.stderr || result.stdout,
|
|
20645
|
+
exitCode: result.exitCode
|
|
20646
|
+
}, null, 2)
|
|
20647
|
+
}
|
|
20648
|
+
],
|
|
20649
|
+
isError: true
|
|
20650
|
+
};
|
|
20651
|
+
}
|
|
20652
|
+
return {
|
|
20653
|
+
content: [
|
|
20654
|
+
{
|
|
20655
|
+
type: "text",
|
|
20656
|
+
text: JSON.stringify({
|
|
20657
|
+
connector: name,
|
|
20658
|
+
success: true,
|
|
20659
|
+
output: result.stdout
|
|
20660
|
+
}, null, 2)
|
|
20661
|
+
}
|
|
20662
|
+
]
|
|
20663
|
+
};
|
|
20664
|
+
});
|
|
20526
20665
|
server.registerTool("search_tools", {
|
|
20527
20666
|
title: "Search Tools",
|
|
20528
20667
|
description: "List tool names, optionally filtered by keyword.",
|
|
@@ -20539,6 +20678,8 @@ server.registerTool("search_tools", {
|
|
|
20539
20678
|
"connector_auth_status",
|
|
20540
20679
|
"configure_auth",
|
|
20541
20680
|
"list_categories",
|
|
20681
|
+
"list_connector_operations",
|
|
20682
|
+
"run_connector_operation",
|
|
20542
20683
|
"search_tools",
|
|
20543
20684
|
"describe_tools"
|
|
20544
20685
|
];
|
|
@@ -20560,7 +20701,9 @@ server.registerTool("describe_tools", {
|
|
|
20560
20701
|
connector_info: "Get metadata and install status. Params: name",
|
|
20561
20702
|
connector_auth_status: "Check auth status and env vars. Params: name",
|
|
20562
20703
|
configure_auth: "Save API key or token. Params: name, key, field?",
|
|
20563
|
-
list_categories: "List connector categories with counts."
|
|
20704
|
+
list_categories: "List connector categories with counts.",
|
|
20705
|
+
list_connector_operations: "Discover available API operations for a connector. Params: name, command?",
|
|
20706
|
+
run_connector_operation: "Execute an API operation on a connector. Params: name, args[], format?, timeout?"
|
|
20564
20707
|
};
|
|
20565
20708
|
const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
|
|
20566
20709
|
`);
|
package/bin/serve.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
-
for (let key of __getOwnPropNames(mod))
|
|
12
|
-
if (!__hasOwnProp.call(to, key))
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
3
|
var __require = import.meta.require;
|
|
20
4
|
|
|
21
5
|
// src/server/serve.ts
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"s3",
|
|
26
26
|
"lambda",
|
|
27
27
|
"dynamodb",
|
|
28
|
+
"ses",
|
|
29
|
+
"email",
|
|
28
30
|
"connector",
|
|
29
31
|
"cli",
|
|
30
32
|
"typescript",
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
43
|
"commander": "^12.1.0",
|
|
42
|
-
"chalk": "^5.3.0"
|
|
44
|
+
"chalk": "^5.3.0",
|
|
45
|
+
"@aws-sdk/client-sesv2": "^3.0.0"
|
|
43
46
|
},
|
|
44
47
|
"engines": {
|
|
45
48
|
"bun": ">=1.0.0"
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type { AWSClient } from './client';
|
|
2
|
+
import type {
|
|
3
|
+
SesEmailIdentity,
|
|
4
|
+
SesSendEmailRequest,
|
|
5
|
+
SesSendEmailResponse,
|
|
6
|
+
SesListIdentitiesResponse,
|
|
7
|
+
SesCreateIdentityResponse,
|
|
8
|
+
SesDkimAttributes,
|
|
9
|
+
SesSendStatistics,
|
|
10
|
+
SesSuppressedDestination,
|
|
11
|
+
SesSuppressedDestinationsResponse,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SES v2 API client
|
|
16
|
+
*/
|
|
17
|
+
export class AwsSesApi {
|
|
18
|
+
private readonly client: AWSClient;
|
|
19
|
+
|
|
20
|
+
constructor(client: AWSClient) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send a transactional email
|
|
26
|
+
*/
|
|
27
|
+
async sendEmail(opts: SesSendEmailRequest): Promise<SesSendEmailResponse> {
|
|
28
|
+
const body: Record<string, unknown> = {
|
|
29
|
+
FromEmailAddress: opts.from,
|
|
30
|
+
Destination: {
|
|
31
|
+
ToAddresses: Array.isArray(opts.to) ? opts.to : [opts.to],
|
|
32
|
+
...(opts.cc && { CcAddresses: Array.isArray(opts.cc) ? opts.cc : [opts.cc] }),
|
|
33
|
+
...(opts.bcc && { BccAddresses: Array.isArray(opts.bcc) ? opts.bcc : [opts.bcc] }),
|
|
34
|
+
},
|
|
35
|
+
Content: {
|
|
36
|
+
Simple: {
|
|
37
|
+
Subject: {
|
|
38
|
+
Data: opts.subject,
|
|
39
|
+
Charset: 'UTF-8',
|
|
40
|
+
},
|
|
41
|
+
Body: {
|
|
42
|
+
...(opts.html && {
|
|
43
|
+
Html: {
|
|
44
|
+
Data: opts.html,
|
|
45
|
+
Charset: 'UTF-8',
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
...(opts.text && {
|
|
49
|
+
Text: {
|
|
50
|
+
Data: opts.text,
|
|
51
|
+
Charset: 'UTF-8',
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
...(opts.attachments && opts.attachments.length > 0 && {
|
|
56
|
+
Attachments: opts.attachments.map(att => ({
|
|
57
|
+
FileName: att.filename,
|
|
58
|
+
Data: att.data,
|
|
59
|
+
ContentType: att.contentType,
|
|
60
|
+
})),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
...(opts.replyTo && { ReplyToAddresses: Array.isArray(opts.replyTo) ? opts.replyTo : [opts.replyTo] }),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const response = await this.client.request<{ MessageId: string }>('/v2/email/outbound-emails', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
service: 'sesv2',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
messageId: response.MessageId,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* List verified email/domain identities
|
|
83
|
+
*/
|
|
84
|
+
async listIdentities(type?: 'EMAIL_ADDRESS' | 'DOMAIN'): Promise<SesListIdentitiesResponse> {
|
|
85
|
+
const params: Record<string, string | number | boolean | undefined> = {
|
|
86
|
+
...(type && { IdentityType: type }),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const response = await this.client.request<{
|
|
90
|
+
EmailIdentities?: Array<{
|
|
91
|
+
IdentityName?: string;
|
|
92
|
+
IdentityType?: string;
|
|
93
|
+
SendingEnabled?: boolean;
|
|
94
|
+
VerificationStatus?: string;
|
|
95
|
+
}>;
|
|
96
|
+
NextToken?: string;
|
|
97
|
+
}>('/v2/email/identities', {
|
|
98
|
+
method: 'GET',
|
|
99
|
+
service: 'sesv2',
|
|
100
|
+
params,
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
identities: (response.EmailIdentities || []).map(identity => ({
|
|
108
|
+
identityName: identity.IdentityName || '',
|
|
109
|
+
identityType: (identity.IdentityType as 'EMAIL_ADDRESS' | 'DOMAIN') || 'EMAIL_ADDRESS',
|
|
110
|
+
sendingEnabled: identity.SendingEnabled ?? false,
|
|
111
|
+
verificationStatus: identity.VerificationStatus || 'NOT_STARTED',
|
|
112
|
+
})),
|
|
113
|
+
nextToken: response.NextToken,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create (verify) an email address or domain identity
|
|
119
|
+
*/
|
|
120
|
+
async createIdentity(identity: string): Promise<SesCreateIdentityResponse> {
|
|
121
|
+
const response = await this.client.request<{
|
|
122
|
+
IdentityType?: string;
|
|
123
|
+
VerifiedForSendingStatus?: boolean;
|
|
124
|
+
DkimAttributes?: {
|
|
125
|
+
SigningEnabled?: boolean;
|
|
126
|
+
Status?: string;
|
|
127
|
+
Tokens?: string[];
|
|
128
|
+
SigningAttributesOrigin?: string;
|
|
129
|
+
NextSigningKeyLength?: string;
|
|
130
|
+
CurrentSigningKeyLength?: string;
|
|
131
|
+
LastKeyGenerationTimestamp?: string;
|
|
132
|
+
};
|
|
133
|
+
}>('/v2/email/identities', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
service: 'sesv2',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({ EmailIdentity: identity }),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
identityType: (response.IdentityType as 'EMAIL_ADDRESS' | 'DOMAIN') || 'EMAIL_ADDRESS',
|
|
144
|
+
verifiedForSendingStatus: response.VerifiedForSendingStatus ?? false,
|
|
145
|
+
dkimAttributes: response.DkimAttributes
|
|
146
|
+
? {
|
|
147
|
+
signingEnabled: response.DkimAttributes.SigningEnabled ?? false,
|
|
148
|
+
status: response.DkimAttributes.Status || 'NOT_STARTED',
|
|
149
|
+
tokens: response.DkimAttributes.Tokens || [],
|
|
150
|
+
}
|
|
151
|
+
: undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Delete an email address or domain identity
|
|
157
|
+
*/
|
|
158
|
+
async deleteIdentity(identity: string): Promise<void> {
|
|
159
|
+
await this.client.request<Record<string, never>>(
|
|
160
|
+
`/v2/email/identities/${encodeURIComponent(identity)}`,
|
|
161
|
+
{
|
|
162
|
+
method: 'DELETE',
|
|
163
|
+
service: 'sesv2',
|
|
164
|
+
headers: {
|
|
165
|
+
'Content-Type': 'application/json',
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get DKIM signing attributes and DNS records for a domain identity
|
|
173
|
+
*/
|
|
174
|
+
async getDkimAttributes(identity: string): Promise<SesDkimAttributes> {
|
|
175
|
+
const response = await this.client.request<{
|
|
176
|
+
DkimAttributes?: {
|
|
177
|
+
SigningEnabled?: boolean;
|
|
178
|
+
Status?: string;
|
|
179
|
+
Tokens?: string[];
|
|
180
|
+
SigningAttributesOrigin?: string;
|
|
181
|
+
NextSigningKeyLength?: string;
|
|
182
|
+
CurrentSigningKeyLength?: string;
|
|
183
|
+
LastKeyGenerationTimestamp?: string;
|
|
184
|
+
};
|
|
185
|
+
IdentityType?: string;
|
|
186
|
+
FeedbackForwardingStatus?: boolean;
|
|
187
|
+
VerifiedForSendingStatus?: boolean;
|
|
188
|
+
}>(`/v2/email/identities/${encodeURIComponent(identity)}`, {
|
|
189
|
+
method: 'GET',
|
|
190
|
+
service: 'sesv2',
|
|
191
|
+
headers: {
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const dkim = response.DkimAttributes || {};
|
|
197
|
+
const region = this.client.getRegion();
|
|
198
|
+
const tokens = dkim.Tokens || [];
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
signingEnabled: dkim.SigningEnabled ?? false,
|
|
202
|
+
status: dkim.Status || 'NOT_STARTED',
|
|
203
|
+
tokens,
|
|
204
|
+
signingAttributesOrigin: dkim.SigningAttributesOrigin || 'AWS_SES',
|
|
205
|
+
dnsRecords: tokens.map(token => ({
|
|
206
|
+
name: `${token}._domainkey.${identity}`,
|
|
207
|
+
type: 'CNAME',
|
|
208
|
+
value: `${token}.dkim.amazonses.com`,
|
|
209
|
+
region,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get send statistics for a given period
|
|
216
|
+
*/
|
|
217
|
+
async getStatistics(period?: string): Promise<SesSendStatistics> {
|
|
218
|
+
const response = await this.client.request<{
|
|
219
|
+
SendDataPoints?: Array<{
|
|
220
|
+
Timestamp?: string;
|
|
221
|
+
DeliveryAttempts?: number;
|
|
222
|
+
Bounces?: number;
|
|
223
|
+
Complaints?: number;
|
|
224
|
+
Rejects?: number;
|
|
225
|
+
}>;
|
|
226
|
+
}>('/v2/email/account/sending-statistics', {
|
|
227
|
+
method: 'GET',
|
|
228
|
+
service: 'sesv2',
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const dataPoints = response.SendDataPoints || [];
|
|
235
|
+
|
|
236
|
+
// Filter by period if provided (period is expected as ISO duration or cutoff date string)
|
|
237
|
+
let filtered = dataPoints;
|
|
238
|
+
if (period) {
|
|
239
|
+
const cutoff = new Date(period);
|
|
240
|
+
if (!isNaN(cutoff.getTime())) {
|
|
241
|
+
filtered = dataPoints.filter(dp => dp.Timestamp && new Date(dp.Timestamp) >= cutoff);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Aggregate totals
|
|
246
|
+
const totals = filtered.reduce(
|
|
247
|
+
(acc, dp) => ({
|
|
248
|
+
deliveryAttempts: acc.deliveryAttempts + (dp.DeliveryAttempts || 0),
|
|
249
|
+
bounces: acc.bounces + (dp.Bounces || 0),
|
|
250
|
+
complaints: acc.complaints + (dp.Complaints || 0),
|
|
251
|
+
rejects: acc.rejects + (dp.Rejects || 0),
|
|
252
|
+
}),
|
|
253
|
+
{ deliveryAttempts: 0, bounces: 0, complaints: 0, rejects: 0 }
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
...totals,
|
|
258
|
+
dataPoints: filtered.map(dp => ({
|
|
259
|
+
timestamp: dp.Timestamp || '',
|
|
260
|
+
deliveryAttempts: dp.DeliveryAttempts || 0,
|
|
261
|
+
bounces: dp.Bounces || 0,
|
|
262
|
+
complaints: dp.Complaints || 0,
|
|
263
|
+
rejects: dp.Rejects || 0,
|
|
264
|
+
})),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* List suppression list entries
|
|
270
|
+
*/
|
|
271
|
+
async getSuppressedDestinations(options?: {
|
|
272
|
+
reasons?: Array<'BOUNCE' | 'COMPLAINT'>;
|
|
273
|
+
startDate?: string;
|
|
274
|
+
endDate?: string;
|
|
275
|
+
nextToken?: string;
|
|
276
|
+
pageSize?: number;
|
|
277
|
+
}): Promise<SesSuppressedDestinationsResponse> {
|
|
278
|
+
const params: Record<string, string | number | boolean | undefined> = {
|
|
279
|
+
...(options?.reasons && { Reason: options.reasons.join(',') }),
|
|
280
|
+
...(options?.startDate && { StartDate: options.startDate }),
|
|
281
|
+
...(options?.endDate && { EndDate: options.endDate }),
|
|
282
|
+
...(options?.nextToken && { NextToken: options.nextToken }),
|
|
283
|
+
...(options?.pageSize && { PageSize: options.pageSize }),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const response = await this.client.request<{
|
|
287
|
+
SuppressedDestinationSummaries?: Array<{
|
|
288
|
+
EmailAddress?: string;
|
|
289
|
+
Reason?: string;
|
|
290
|
+
LastUpdateTime?: string;
|
|
291
|
+
}>;
|
|
292
|
+
NextToken?: string;
|
|
293
|
+
}>('/v2/email/suppression/addresses', {
|
|
294
|
+
method: 'GET',
|
|
295
|
+
service: 'sesv2',
|
|
296
|
+
params,
|
|
297
|
+
headers: {
|
|
298
|
+
'Content-Type': 'application/json',
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
suppressedDestinations: (response.SuppressedDestinationSummaries || []).map(entry => ({
|
|
304
|
+
emailAddress: entry.EmailAddress || '',
|
|
305
|
+
reason: (entry.Reason as 'BOUNCE' | 'COMPLAINT') || 'BOUNCE',
|
|
306
|
+
lastUpdateTime: entry.LastUpdateTime || '',
|
|
307
|
+
})),
|
|
308
|
+
nextToken: response.NextToken,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|