@ampless/backend 0.2.0-alpha.8 → 1.0.0-alpha.19

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/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ interface DefineAmplessBackendOpts {
14
14
  processorUntrusted: FunctionResource;
15
15
  apiKeyRenewer: FunctionResource;
16
16
  userAdmin: FunctionResource;
17
+ mcpHandler: FunctionResource;
17
18
  }
18
19
  type AmplessBackend = any;
19
20
  /**
@@ -188,6 +189,52 @@ declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
188
189
  * when there are no custom models.
189
190
  */
190
191
  declare function extendAmplessSchema(a: any, custom?: Record<string, any>, opts?: AmplessSchemaModelsOpts): any;
192
+ interface AmplessSchemaAuthorizationOpts {
193
+ /**
194
+ * Optional Amplify `defineFunction` ref for the MCP HTTP handler.
195
+ * When supplied, the schema gains `allow.resource(fn).to(['query',
196
+ * 'mutate'])` so the Lambda can sign AppSync requests with SigV4
197
+ * under its own IAM role (no Cognito identity / shared API key).
198
+ *
199
+ * Resource auth is currently only honoured at schema scope by
200
+ * `@aws-amplify/data-schema` (model-level `.authorization` callbacks
201
+ * destructure `resource` out of their `allow` parameter), so this
202
+ * grant applies broadly — every model the Lambda calls is reachable.
203
+ * That's wider than strictly necessary; the MCP tools' GraphQL
204
+ * operations narrow the effective surface to Post / PostTag in
205
+ * Phase 4 and Media in Phase 5.
206
+ *
207
+ * Typed as `unknown` for the same reason `userAdminFunction` /
208
+ * `mcpHandlerFunction` are in other helpers — `defineFunction`'s
209
+ * return type carries internal pnpm paths that don't survive
210
+ * declaration emit.
211
+ */
212
+ mcpHandlerFunction?: unknown;
213
+ }
214
+ /**
215
+ * Schema-level authorization rules for ampless. Pass the result into
216
+ * `a.schema({...}).authorization((allow) => [...])` in the user's
217
+ * `amplify/data/resource.ts`. When no Lambda function refs are
218
+ * supplied the function returns `[]`, so the schema stays unaffected.
219
+ *
220
+ * Return type is `any[]` (matching the rest of this module's
221
+ * intentional looseness around `@aws-amplify/data-schema`'s heavily
222
+ * generic builder types) so callers don't have to wrestle the
223
+ * generic `SchemaAuthorization<…>` parameters that change between
224
+ * minor versions. `amplify/data/resource.ts` strict-type-checks fine
225
+ * downstream because the schema itself still resolves through
226
+ * `ClientSchema<typeof schema>` correctly.
227
+ *
228
+ * Usage:
229
+ *
230
+ * const schema = a.schema({
231
+ * ...amplessSchemaModels(a, { resolverPaths, userAdminFunction }),
232
+ * ...customSchemaModels(a),
233
+ * }).authorization((allow) => amplessSchemaAuthorization(allow, {
234
+ * mcpHandlerFunction: mcpHandler,
235
+ * }))
236
+ */
237
+ declare function amplessSchemaAuthorization(allow: any, opts?: AmplessSchemaAuthorizationOpts): any[];
191
238
  /**
192
239
  * Standard authorization modes for ampless. `userPool` is the default
193
240
  * (admin/editor access); the API key serves the public read endpoints
@@ -198,4 +245,4 @@ declare function extendAmplessSchema(a: any, custom?: Record<string, any>, opts?
198
245
  */
199
246
  declare const defaultAuthorizationModes: Parameters<typeof defineData>[0]['authorizationModes'];
200
247
 
201
- export { type AmplessAuthConfigOpts, type AmplessBackend, type AmplessResolverPaths, type AmplessSchemaModelsOpts, DEFAULT_RESOLVER_PATHS, type DefineAmplessBackendOpts, amplessAuthConfig, amplessSchemaModels, amplessStorageConfig, defaultAuthorizationModes, defineAmplessBackend, extendAmplessSchema };
248
+ export { type AmplessAuthConfigOpts, type AmplessBackend, type AmplessResolverPaths, type AmplessSchemaAuthorizationOpts, type AmplessSchemaModelsOpts, DEFAULT_RESOLVER_PATHS, type DefineAmplessBackendOpts, amplessAuthConfig, amplessSchemaAuthorization, amplessSchemaModels, amplessStorageConfig, defaultAuthorizationModes, defineAmplessBackend, extendAmplessSchema };
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
+ import "./chunk-BYXBJQAS.js";
2
+
1
3
  // src/backend.ts
2
4
  import { defineBackend } from "@aws-amplify/backend";
3
5
  import { Effect, PolicyStatement, AnyPrincipal } from "aws-cdk-lib/aws-iam";
4
6
  import { Duration, Stack } from "aws-cdk-lib";
5
7
  import { Queue } from "aws-cdk-lib/aws-sqs";
6
- import { StartingPosition } from "aws-cdk-lib/aws-lambda";
8
+ import { FunctionUrlAuthType, HttpMethod, StartingPosition } from "aws-cdk-lib/aws-lambda";
7
9
  import { DynamoEventSource, SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
8
10
  import { Rule, Schedule } from "aws-cdk-lib/aws-events";
9
11
  import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
@@ -17,7 +19,8 @@ function defineAmplessBackend(opts) {
17
19
  processorTrusted: opts.processorTrusted,
18
20
  processorUntrusted: opts.processorUntrusted,
19
21
  apiKeyRenewer: opts.apiKeyRenewer,
20
- userAdmin: opts.userAdmin
22
+ userAdmin: opts.userAdmin,
23
+ mcpHandler: opts.mcpHandler
21
24
  });
22
25
  const cfnBucket = backend.storage.resources.cfnResources.cfnBucket;
23
26
  cfnBucket.publicAccessBlockConfiguration = {
@@ -166,6 +169,60 @@ function defineAmplessBackend(opts) {
166
169
  schedule: Schedule.cron({ minute: "0", hour: "3", day: "1", month: "*", year: "*" }),
167
170
  targets: [new LambdaFunction(apiKeyRenewerFn)]
168
171
  });
172
+ const mcpHandlerFn = backend.mcpHandler.resources.lambda;
173
+ mcpHandlerFn.addToRolePolicy(
174
+ new PolicyStatement({
175
+ effect: Effect.ALLOW,
176
+ actions: ["dynamodb:GetItem"],
177
+ resources: [kvTable.tableArn]
178
+ })
179
+ );
180
+ mcpHandlerFn.addEnvironment("AMPLESS_KV_TABLE", kvTable.tableName);
181
+ mcpHandlerFn.addEnvironment(
182
+ "AMPLESS_APPSYNC_URL",
183
+ backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl
184
+ );
185
+ mcpHandlerFn.addToRolePolicy(
186
+ new PolicyStatement({
187
+ effect: Effect.ALLOW,
188
+ actions: ["s3:PutObject"],
189
+ resources: [`${backend.storage.resources.bucket.bucketArn}/public/media/*`]
190
+ })
191
+ );
192
+ mcpHandlerFn.addToRolePolicy(
193
+ new PolicyStatement({
194
+ effect: Effect.ALLOW,
195
+ actions: ["s3:PutObject", "s3:DeleteObject"],
196
+ resources: [`${backend.storage.resources.bucket.bucketArn}/public/static/*`]
197
+ })
198
+ );
199
+ mcpHandlerFn.addToRolePolicy(
200
+ new PolicyStatement({
201
+ effect: Effect.ALLOW,
202
+ actions: ["s3:ListBucket"],
203
+ resources: [backend.storage.resources.bucket.bucketArn],
204
+ conditions: {
205
+ StringLike: { "s3:prefix": ["public/static/*"] }
206
+ }
207
+ })
208
+ );
209
+ mcpHandlerFn.addEnvironment("AMPLESS_BUCKET_NAME", backend.storage.resources.bucket.bucketName);
210
+ const mcpFunctionUrl = mcpHandlerFn.addFunctionUrl({
211
+ authType: FunctionUrlAuthType.NONE,
212
+ cors: {
213
+ allowedOrigins: ["*"],
214
+ allowedMethods: [HttpMethod.POST],
215
+ allowedHeaders: ["*"],
216
+ maxAge: Duration.hours(1)
217
+ }
218
+ });
219
+ backend.addOutput({
220
+ custom: {
221
+ mcp: {
222
+ endpoint: mcpFunctionUrl.url
223
+ }
224
+ }
225
+ });
169
226
  return backend;
170
227
  }
171
228
 
@@ -213,7 +270,6 @@ function amplessSchemaModels(a, opts = {}) {
213
270
  };
214
271
  return {
215
272
  Post: a.model({
216
- siteId: a.string().required(),
217
273
  postId: a.id().required(),
218
274
  slug: a.string().required(),
219
275
  title: a.string().required(),
@@ -230,25 +286,14 @@ function amplessSchemaModels(a, opts = {}) {
230
286
  // chrome). The runtime's post dispatcher redirects to the
231
287
  // raw route handler when this is true.
232
288
  // Other keys are passed through unchanged for plugin / app use.
233
- metadata: a.json(),
234
- // Denormalized GSI keys — set by every write path (admin client,
235
- // MCP tools). Same pattern as siteIdStatus: composing the
236
- // partition key as a single string lets each public-read query
237
- // hit DynamoDB with a pure PK Query, no filter pass.
238
- // siteIdStatus = `${siteId}#${status}`
239
- // → bySiteIdStatus partitions per site×status, sorted by publishedAt
240
- // siteIdSlug = `${siteId}#${slug}`
241
- // → bySiteIdSlug locates a single post by slug in O(1)
242
- siteIdStatus: a.string(),
243
- siteIdSlug: a.string()
244
- }).identifier(["siteId", "postId"]).secondaryIndexes((index) => [
245
- index("siteIdStatus").sortKeys(["publishedAt"]).name("bySiteIdStatus"),
246
- index("siteIdSlug").name("bySiteIdSlug")
289
+ metadata: a.json()
290
+ }).identifier(["postId"]).secondaryIndexes((index) => [
291
+ index("status").sortKeys(["publishedAt"]).name("byStatus"),
292
+ index("slug").name("bySlug")
247
293
  ]).authorization((allow) => [
248
294
  allow.groups(["ampless-admin", "ampless-editor"])
249
295
  ]),
250
296
  Page: a.model({
251
- siteId: a.string().required(),
252
297
  pageId: a.id().required(),
253
298
  slug: a.string().required(),
254
299
  title: a.string().required(),
@@ -256,26 +301,24 @@ function amplessSchemaModels(a, opts = {}) {
256
301
  body: a.json(),
257
302
  status: a.enum(["draft", "published"]),
258
303
  publishedAt: a.datetime()
259
- }).identifier(["siteId", "pageId"]).authorization((allow) => [
304
+ }).identifier(["pageId"]).authorization((allow) => [
260
305
  allow.groups(["ampless-admin", "ampless-editor"])
261
306
  ]),
262
307
  Media: a.model({
263
- siteId: a.string().required(),
264
308
  mediaId: a.id().required(),
265
309
  src: a.string().required(),
266
310
  mimeType: a.string().required(),
267
311
  size: a.integer(),
268
312
  delivery: a.string()
269
- }).identifier(["siteId", "mediaId"]).authorization((allow) => [
313
+ }).identifier(["mediaId"]).authorization((allow) => [
270
314
  allow.groups(["ampless-admin", "ampless-editor"])
271
315
  ]),
272
316
  Taxonomy: a.model({
273
- siteId: a.string().required(),
274
317
  termId: a.id().required(),
275
318
  type: a.enum(["category", "tag"]),
276
319
  name: a.string().required(),
277
320
  slug: a.string().required()
278
- }).identifier(["siteId", "termId"]).authorization((allow) => [
321
+ }).identifier(["termId"]).authorization((allow) => [
279
322
  allow.groups(["ampless-admin", "ampless-editor"])
280
323
  ]),
281
324
  // Denormalized index for "posts by tag" queries. Each Post tag becomes
@@ -284,13 +327,11 @@ function amplessSchemaModels(a, opts = {}) {
284
327
  // sort key. Maintained by the admin client whenever a post is created,
285
328
  // updated, or deleted; never edited by end users.
286
329
  //
287
- // PK: siteIdTag e.g. "default#tech"
330
+ // PK: tag e.g. "tech"
288
331
  // SK: publishedAtPostId e.g. "2026-04-27T13:57:05.679Z#post-001"
289
332
  PostTag: a.model({
290
- siteIdTag: a.string().required(),
291
- publishedAtPostId: a.string().required(),
292
- siteId: a.string().required(),
293
333
  tag: a.string().required(),
334
+ publishedAtPostId: a.string().required(),
294
335
  postId: a.id().required(),
295
336
  publishedAt: a.datetime().required(),
296
337
  slug: a.string().required(),
@@ -298,11 +339,11 @@ function amplessSchemaModels(a, opts = {}) {
298
339
  excerpt: a.string(),
299
340
  // Full tag list of the post (for chip rendering on tag pages).
300
341
  tags: a.string().array()
301
- }).identifier(["siteIdTag", "publishedAtPostId"]).authorization((allow) => [
342
+ }).identifier(["tag", "publishedAtPostId"]).authorization((allow) => [
302
343
  allow.groups(["ampless-admin", "ampless-editor"])
303
344
  ]),
304
345
  // Generic key/value store. Two roles in one table:
305
- // - Site settings: PK = `siteconfig:{siteId}`, SK = dotted key
346
+ // - Site settings: PK = `siteconfig`, SK = dotted key
306
347
  // (`site.name`, `media.imageDisplay`, ...). No TTL → persistent.
307
348
  // - Caches / plugin state: PK = whatever namespace the caller picks
308
349
  // (`cache:{ns}`, `pluginstate:{name}:...`). TTL set → DynamoDB
@@ -324,7 +365,6 @@ function amplessSchemaModels(a, opts = {}) {
324
365
  // Custom return type for public post reads. Decoupling from `Post` lets
325
366
  // AppSync skip the model-level (admin-only) auth check on fields.
326
367
  PublicPost: a.customType({
327
- siteId: a.string().required(),
328
368
  postId: a.id().required(),
329
369
  slug: a.string().required(),
330
370
  title: a.string().required(),
@@ -351,7 +391,6 @@ function amplessSchemaModels(a, opts = {}) {
351
391
  // only thing guests can see is `status === 'published'` rows projected
352
392
  // onto `PublicPost`.
353
393
  listPublishedPosts: a.query().arguments({
354
- siteId: a.string(),
355
394
  // Both ISO 8601 strings; query SK condition is pushed into DynamoDB
356
395
  // so only the matching publishedAt range is read.
357
396
  from: a.datetime(),
@@ -369,7 +408,7 @@ function amplessSchemaModels(a, opts = {}) {
369
408
  allow.publicApiKey(),
370
409
  allow.groups(["ampless-admin", "ampless-editor"])
371
410
  ]),
372
- getPublishedPost: a.query().arguments({ siteId: a.string(), slug: a.string().required() }).returns(a.ref("PublicPost")).handler(
411
+ getPublishedPost: a.query().arguments({ slug: a.string().required() }).returns(a.ref("PublicPost")).handler(
373
412
  a.handler.custom({
374
413
  dataSource: a.ref("Post"),
375
414
  entry: resolverPaths.getPublishedPost
@@ -381,7 +420,6 @@ function amplessSchemaModels(a, opts = {}) {
381
420
  // Tag pages: newest-first only. Archive (date range) within a tag is not
382
421
  // a common UX pattern; if it's ever needed, add `from`/`to` args here.
383
422
  listPostsByTag: a.query().arguments({
384
- siteId: a.string(),
385
423
  tag: a.string().required(),
386
424
  limit: a.integer(),
387
425
  nextToken: a.string()
@@ -422,6 +460,13 @@ function extendAmplessSchema(a, custom, opts) {
422
460
  ...custom ?? {}
423
461
  });
424
462
  }
463
+ function amplessSchemaAuthorization(allow, opts = {}) {
464
+ const rules = [];
465
+ if (opts.mcpHandlerFunction) {
466
+ rules.push(allow.resource(opts.mcpHandlerFunction).to(["query", "mutate"]));
467
+ }
468
+ return rules;
469
+ }
425
470
  var defaultAuthorizationModes = {
426
471
  defaultAuthorizationMode: "userPool",
427
472
  apiKeyAuthorizationMode: { expiresInDays: 365 }
@@ -429,6 +474,7 @@ var defaultAuthorizationModes = {
429
474
  export {
430
475
  DEFAULT_RESOLVER_PATHS,
431
476
  amplessAuthConfig,
477
+ amplessSchemaAuthorization,
432
478
  amplessSchemaModels,
433
479
  amplessStorageConfig,
434
480
  defaultAuthorizationModes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/backend",
3
- "version": "0.2.0-alpha.8",
3
+ "version": "1.0.0-alpha.19",
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",
@@ -32,6 +32,10 @@
32
32
  "./functions/api-key-renewer": {
33
33
  "import": "./dist/functions/api-key-renewer.js",
34
34
  "types": "./dist/functions/api-key-renewer.d.ts"
35
+ },
36
+ "./functions/mcp-handler": {
37
+ "import": "./dist/functions/mcp-handler.js",
38
+ "types": "./dist/functions/mcp-handler.d.ts"
35
39
  }
36
40
  },
37
41
  "files": [
@@ -49,14 +53,20 @@
49
53
  "homepage": "https://github.com/heavymoons/ampless/tree/main/packages/backend#readme",
50
54
  "bugs": "https://github.com/heavymoons/ampless/issues",
51
55
  "dependencies": {
56
+ "@aws-crypto/sha256-js": "^5.2.0",
52
57
  "@aws-sdk/client-appsync": "^3.717.0",
53
58
  "@aws-sdk/client-cognito-identity-provider": "^3.717.0",
54
59
  "@aws-sdk/client-dynamodb": "^3.717.0",
55
60
  "@aws-sdk/client-s3": "^3.1048.0",
56
61
  "@aws-sdk/client-sqs": "^3.717.0",
62
+ "@aws-sdk/credential-provider-node": "^3.717.0",
57
63
  "@aws-sdk/lib-dynamodb": "^3.717.0",
58
64
  "@aws-sdk/util-dynamodb": "^3.717.0",
59
- "ampless": "0.2.0-alpha.6"
65
+ "@smithy/protocol-http": "^5.3.0",
66
+ "@smithy/signature-v4": "^5.4.0",
67
+ "fflate": "^0.8.2",
68
+ "ampless": "1.0.0-alpha.9",
69
+ "@ampless/mcp-server": "1.0.0-alpha.10"
60
70
  },
61
71
  "peerDependencies": {
62
72
  "@aws-amplify/backend": "^1",