@ampless/backend 1.0.0-alpha.51 → 1.0.0-alpha.53
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.
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 1. Reads the admin-only `McpToken` DynamoDB table directly
|
|
6
6
|
* (identifier = SHA-256 hex of plaintext) to validate
|
|
7
7
|
* `Authorization: Bearer amk_...`. The Lambda has a narrow IAM
|
|
8
|
-
* grant: `dynamodb:GetItem` on the
|
|
9
|
-
* AppSync round-trip for token validation.
|
|
8
|
+
* grant: `dynamodb:GetItem` and `dynamodb:UpdateItem` on the
|
|
9
|
+
* McpToken table — no AppSync round-trip for token validation.
|
|
10
10
|
* 2. Parses the incoming JSON-RPC envelope by hand (no MCP SDK
|
|
11
11
|
* stdio transport in a Lambda runtime — overkill for the three
|
|
12
12
|
* verbs we actually need).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/functions/mcp-handler.ts
|
|
2
|
-
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
|
-
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { ConditionalCheckFailedException, DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
|
+
import { DynamoDBDocumentClient, GetCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
|
|
6
6
|
// ../mcp-server/dist/index.js
|
|
@@ -572,10 +572,32 @@ var deleteMediaSchema = {
|
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
574
|
};
|
|
575
|
+
var MEDIA_PREFIX = "public/media/";
|
|
576
|
+
function assertMediaPrefix(key) {
|
|
577
|
+
if (key.includes("\\")) {
|
|
578
|
+
throw new Error(`delete_media: src must start with "${MEDIA_PREFIX}" \u2014 got: ${key}`);
|
|
579
|
+
}
|
|
580
|
+
const parts = key.split("/");
|
|
581
|
+
const resolved = [];
|
|
582
|
+
for (const part of parts) {
|
|
583
|
+
if (part === "..") {
|
|
584
|
+
resolved.pop();
|
|
585
|
+
} else {
|
|
586
|
+
resolved.push(part);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const normalised = resolved.join("/");
|
|
590
|
+
if (!normalised.startsWith(MEDIA_PREFIX)) {
|
|
591
|
+
throw new Error(`delete_media: src must start with "${MEDIA_PREFIX}" \u2014 got: ${key}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
575
594
|
async function deleteMedia(graphql, storage, args) {
|
|
576
595
|
if (!args.mediaId && !args.src) {
|
|
577
596
|
throw new Error("delete_media: provide `mediaId` or `src`");
|
|
578
597
|
}
|
|
598
|
+
if (args.src !== void 0) {
|
|
599
|
+
assertMediaPrefix(args.src);
|
|
600
|
+
}
|
|
579
601
|
const dryRun = args.dryRun === true;
|
|
580
602
|
let mediaId;
|
|
581
603
|
let src;
|
|
@@ -584,12 +606,14 @@ async function deleteMedia(graphql, storage, args) {
|
|
|
584
606
|
if (data.getMedia) {
|
|
585
607
|
mediaId = data.getMedia.mediaId;
|
|
586
608
|
src = data.getMedia.src;
|
|
609
|
+
assertMediaPrefix(src);
|
|
587
610
|
}
|
|
588
611
|
} else {
|
|
589
612
|
const data = await graphql.query(GET_BY_SRC, { src: args.src });
|
|
590
613
|
if (data.getMediaBySrc) {
|
|
591
614
|
mediaId = data.getMediaBySrc.mediaId;
|
|
592
615
|
src = data.getMediaBySrc.src;
|
|
616
|
+
assertMediaPrefix(src);
|
|
593
617
|
}
|
|
594
618
|
}
|
|
595
619
|
if (!mediaId) {
|
|
@@ -1736,6 +1760,7 @@ var JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
|
1736
1760
|
var JSON_RPC_INVALID_PARAMS = -32602;
|
|
1737
1761
|
var JSON_RPC_INTERNAL_ERROR = -32603;
|
|
1738
1762
|
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
1763
|
+
var LAST_USED_THROTTLE_MS = 6e4;
|
|
1739
1764
|
function requireEnv(name) {
|
|
1740
1765
|
const v = process.env[name];
|
|
1741
1766
|
if (!v) throw new Error(`[mcp-handler] missing required env var ${name}`);
|
|
@@ -1779,6 +1804,28 @@ async function validateBearer(plaintext) {
|
|
|
1779
1804
|
if (row.expiresAt && new Date(row.expiresAt).getTime() <= Date.now()) return null;
|
|
1780
1805
|
return row;
|
|
1781
1806
|
}
|
|
1807
|
+
async function touchLastUsedAt(hash) {
|
|
1808
|
+
const now = /* @__PURE__ */ new Date();
|
|
1809
|
+
const threshold = new Date(now.getTime() - LAST_USED_THROTTLE_MS).toISOString();
|
|
1810
|
+
try {
|
|
1811
|
+
await ddb.send(
|
|
1812
|
+
new UpdateCommand({
|
|
1813
|
+
TableName: MCP_TOKEN_TABLE,
|
|
1814
|
+
Key: { hash },
|
|
1815
|
+
UpdateExpression: "SET lastUsedAt = :now",
|
|
1816
|
+
ConditionExpression: "attribute_not_exists(lastUsedAt) OR attribute_type(lastUsedAt, :nullType) OR lastUsedAt < :threshold",
|
|
1817
|
+
ExpressionAttributeValues: {
|
|
1818
|
+
":now": now.toISOString(),
|
|
1819
|
+
":threshold": threshold,
|
|
1820
|
+
":nullType": "NULL"
|
|
1821
|
+
}
|
|
1822
|
+
})
|
|
1823
|
+
);
|
|
1824
|
+
} catch (err2) {
|
|
1825
|
+
if (err2 instanceof ConditionalCheckFailedException) return;
|
|
1826
|
+
console.error("[mcp-handler] failed to update lastUsedAt:", err2);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1782
1829
|
var cachedCtx = null;
|
|
1783
1830
|
function makeContext() {
|
|
1784
1831
|
if (cachedCtx) return cachedCtx;
|
|
@@ -1861,6 +1908,7 @@ var handler = async (event) => {
|
|
|
1861
1908
|
if (!meta) {
|
|
1862
1909
|
return jsonResponse(401, { error: "invalid_token" });
|
|
1863
1910
|
}
|
|
1911
|
+
await touchLastUsedAt(meta.hash);
|
|
1864
1912
|
let req;
|
|
1865
1913
|
try {
|
|
1866
1914
|
const body = event.isBase64Encoded ? Buffer.from(event.body ?? "", "base64").toString("utf8") : event.body ?? "";
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ function defineAmplessBackend(opts) {
|
|
|
61
61
|
new PolicyStatement({
|
|
62
62
|
effect: Effect.ALLOW,
|
|
63
63
|
actions: ["cognito-idp:AdminAddUserToGroup", "cognito-idp:ListUsersInGroup"],
|
|
64
|
-
resources: [
|
|
64
|
+
resources: [backend.auth.resources.userPool.userPoolArn]
|
|
65
65
|
})
|
|
66
66
|
);
|
|
67
67
|
backend.userAdmin.resources.lambda.addToRolePolicy(
|
|
@@ -73,7 +73,7 @@ function defineAmplessBackend(opts) {
|
|
|
73
73
|
"cognito-idp:AdminAddUserToGroup",
|
|
74
74
|
"cognito-idp:AdminRemoveUserFromGroup"
|
|
75
75
|
],
|
|
76
|
-
resources: [
|
|
76
|
+
resources: [backend.auth.resources.userPool.userPoolArn]
|
|
77
77
|
})
|
|
78
78
|
);
|
|
79
79
|
backend.userAdmin.resources.lambda.addEnvironment(
|
|
@@ -197,7 +197,7 @@ function defineAmplessBackend(opts) {
|
|
|
197
197
|
mcpHandlerFn.addToRolePolicy(
|
|
198
198
|
new PolicyStatement({
|
|
199
199
|
effect: Effect.ALLOW,
|
|
200
|
-
actions: ["dynamodb:GetItem"],
|
|
200
|
+
actions: ["dynamodb:GetItem", "dynamodb:UpdateItem"],
|
|
201
201
|
resources: [mcpTokenTable.tableArn]
|
|
202
202
|
})
|
|
203
203
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/backend",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.53",
|
|
4
4
|
"description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"@smithy/protocol-http": "^5.4.4",
|
|
70
70
|
"@smithy/signature-v4": "^5.4.4",
|
|
71
71
|
"fflate": "^0.8.3",
|
|
72
|
-
"@ampless/mcp-server": "1.0.0-alpha.
|
|
73
|
-
"ampless": "1.0.0-alpha.
|
|
72
|
+
"@ampless/mcp-server": "1.0.0-alpha.38",
|
|
73
|
+
"ampless": "1.0.0-alpha.32"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@aws-amplify/backend": "^1",
|