@ampless/backend 1.0.0-alpha.51 → 1.0.0-alpha.52

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 McpToken table only — no
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: ["arn:aws:cognito-idp:*:*:userpool/*"]
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: ["arn:aws:cognito-idp:*:*:userpool/*"]
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.51",
3
+ "version": "1.0.0-alpha.52",
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.36",
73
- "ampless": "1.0.0-alpha.30"
72
+ "@ampless/mcp-server": "1.0.0-alpha.37",
73
+ "ampless": "1.0.0-alpha.31"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@aws-amplify/backend": "^1",