@ampless/backend 0.2.0-alpha.9 → 1.0.0-alpha.20

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
@@ -189,6 +189,52 @@ declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
189
189
  * when there are no custom models.
190
190
  */
191
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[];
192
238
  /**
193
239
  * Standard authorization modes for ampless. `userPool` is the default
194
240
  * (admin/editor access); the API key serves the public read endpoints
@@ -199,4 +245,4 @@ declare function extendAmplessSchema(a: any, custom?: Record<string, any>, opts?
199
245
  */
200
246
  declare const defaultAuthorizationModes: Parameters<typeof defineData>[0]['authorizationModes'];
201
247
 
202
- 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,3 +1,5 @@
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";
@@ -176,11 +178,40 @@ function defineAmplessBackend(opts) {
176
178
  })
177
179
  );
178
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);
179
210
  const mcpFunctionUrl = mcpHandlerFn.addFunctionUrl({
180
211
  authType: FunctionUrlAuthType.NONE,
181
212
  cors: {
182
213
  allowedOrigins: ["*"],
183
- allowedMethods: [HttpMethod.POST, HttpMethod.OPTIONS],
214
+ allowedMethods: [HttpMethod.POST],
184
215
  allowedHeaders: ["*"],
185
216
  maxAge: Duration.hours(1)
186
217
  }
@@ -239,7 +270,6 @@ function amplessSchemaModels(a, opts = {}) {
239
270
  };
240
271
  return {
241
272
  Post: a.model({
242
- siteId: a.string().required(),
243
273
  postId: a.id().required(),
244
274
  slug: a.string().required(),
245
275
  title: a.string().required(),
@@ -256,25 +286,14 @@ function amplessSchemaModels(a, opts = {}) {
256
286
  // chrome). The runtime's post dispatcher redirects to the
257
287
  // raw route handler when this is true.
258
288
  // Other keys are passed through unchanged for plugin / app use.
259
- metadata: a.json(),
260
- // Denormalized GSI keys — set by every write path (admin client,
261
- // MCP tools). Same pattern as siteIdStatus: composing the
262
- // partition key as a single string lets each public-read query
263
- // hit DynamoDB with a pure PK Query, no filter pass.
264
- // siteIdStatus = `${siteId}#${status}`
265
- // → bySiteIdStatus partitions per site×status, sorted by publishedAt
266
- // siteIdSlug = `${siteId}#${slug}`
267
- // → bySiteIdSlug locates a single post by slug in O(1)
268
- siteIdStatus: a.string(),
269
- siteIdSlug: a.string()
270
- }).identifier(["siteId", "postId"]).secondaryIndexes((index) => [
271
- index("siteIdStatus").sortKeys(["publishedAt"]).name("bySiteIdStatus"),
272
- 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")
273
293
  ]).authorization((allow) => [
274
294
  allow.groups(["ampless-admin", "ampless-editor"])
275
295
  ]),
276
296
  Page: a.model({
277
- siteId: a.string().required(),
278
297
  pageId: a.id().required(),
279
298
  slug: a.string().required(),
280
299
  title: a.string().required(),
@@ -282,26 +301,24 @@ function amplessSchemaModels(a, opts = {}) {
282
301
  body: a.json(),
283
302
  status: a.enum(["draft", "published"]),
284
303
  publishedAt: a.datetime()
285
- }).identifier(["siteId", "pageId"]).authorization((allow) => [
304
+ }).identifier(["pageId"]).authorization((allow) => [
286
305
  allow.groups(["ampless-admin", "ampless-editor"])
287
306
  ]),
288
307
  Media: a.model({
289
- siteId: a.string().required(),
290
308
  mediaId: a.id().required(),
291
309
  src: a.string().required(),
292
310
  mimeType: a.string().required(),
293
311
  size: a.integer(),
294
312
  delivery: a.string()
295
- }).identifier(["siteId", "mediaId"]).authorization((allow) => [
313
+ }).identifier(["mediaId"]).authorization((allow) => [
296
314
  allow.groups(["ampless-admin", "ampless-editor"])
297
315
  ]),
298
316
  Taxonomy: a.model({
299
- siteId: a.string().required(),
300
317
  termId: a.id().required(),
301
318
  type: a.enum(["category", "tag"]),
302
319
  name: a.string().required(),
303
320
  slug: a.string().required()
304
- }).identifier(["siteId", "termId"]).authorization((allow) => [
321
+ }).identifier(["termId"]).authorization((allow) => [
305
322
  allow.groups(["ampless-admin", "ampless-editor"])
306
323
  ]),
307
324
  // Denormalized index for "posts by tag" queries. Each Post tag becomes
@@ -310,13 +327,11 @@ function amplessSchemaModels(a, opts = {}) {
310
327
  // sort key. Maintained by the admin client whenever a post is created,
311
328
  // updated, or deleted; never edited by end users.
312
329
  //
313
- // PK: siteIdTag e.g. "default#tech"
330
+ // PK: tag e.g. "tech"
314
331
  // SK: publishedAtPostId e.g. "2026-04-27T13:57:05.679Z#post-001"
315
332
  PostTag: a.model({
316
- siteIdTag: a.string().required(),
317
- publishedAtPostId: a.string().required(),
318
- siteId: a.string().required(),
319
333
  tag: a.string().required(),
334
+ publishedAtPostId: a.string().required(),
320
335
  postId: a.id().required(),
321
336
  publishedAt: a.datetime().required(),
322
337
  slug: a.string().required(),
@@ -324,11 +339,11 @@ function amplessSchemaModels(a, opts = {}) {
324
339
  excerpt: a.string(),
325
340
  // Full tag list of the post (for chip rendering on tag pages).
326
341
  tags: a.string().array()
327
- }).identifier(["siteIdTag", "publishedAtPostId"]).authorization((allow) => [
342
+ }).identifier(["tag", "publishedAtPostId"]).authorization((allow) => [
328
343
  allow.groups(["ampless-admin", "ampless-editor"])
329
344
  ]),
330
345
  // Generic key/value store. Two roles in one table:
331
- // - Site settings: PK = `siteconfig:{siteId}`, SK = dotted key
346
+ // - Site settings: PK = `siteconfig`, SK = dotted key
332
347
  // (`site.name`, `media.imageDisplay`, ...). No TTL → persistent.
333
348
  // - Caches / plugin state: PK = whatever namespace the caller picks
334
349
  // (`cache:{ns}`, `pluginstate:{name}:...`). TTL set → DynamoDB
@@ -349,8 +364,13 @@ function amplessSchemaModels(a, opts = {}) {
349
364
  ]),
350
365
  // Custom return type for public post reads. Decoupling from `Post` lets
351
366
  // AppSync skip the model-level (admin-only) auth check on fields.
367
+ //
368
+ // `updatedAt` is projected through so middleware can compute the
369
+ // `metadata.cache='auto'` cooldown without re-fetching the model
370
+ // row. It's an Amplify-managed DynamoDB attribute (set on every
371
+ // write); the JS resolvers pass items through verbatim so the
372
+ // value naturally appears here once the schema declares it.
352
373
  PublicPost: a.customType({
353
- siteId: a.string().required(),
354
374
  postId: a.id().required(),
355
375
  slug: a.string().required(),
356
376
  title: a.string().required(),
@@ -360,7 +380,8 @@ function amplessSchemaModels(a, opts = {}) {
360
380
  status: a.string(),
361
381
  publishedAt: a.datetime(),
362
382
  tags: a.string().array(),
363
- metadata: a.json()
383
+ metadata: a.json(),
384
+ updatedAt: a.datetime()
364
385
  }),
365
386
  // Paginated wrapper for list responses.
366
387
  PublicPostConnection: a.customType({
@@ -377,7 +398,6 @@ function amplessSchemaModels(a, opts = {}) {
377
398
  // only thing guests can see is `status === 'published'` rows projected
378
399
  // onto `PublicPost`.
379
400
  listPublishedPosts: a.query().arguments({
380
- siteId: a.string(),
381
401
  // Both ISO 8601 strings; query SK condition is pushed into DynamoDB
382
402
  // so only the matching publishedAt range is read.
383
403
  from: a.datetime(),
@@ -395,7 +415,7 @@ function amplessSchemaModels(a, opts = {}) {
395
415
  allow.publicApiKey(),
396
416
  allow.groups(["ampless-admin", "ampless-editor"])
397
417
  ]),
398
- getPublishedPost: a.query().arguments({ siteId: a.string(), slug: a.string().required() }).returns(a.ref("PublicPost")).handler(
418
+ getPublishedPost: a.query().arguments({ slug: a.string().required() }).returns(a.ref("PublicPost")).handler(
399
419
  a.handler.custom({
400
420
  dataSource: a.ref("Post"),
401
421
  entry: resolverPaths.getPublishedPost
@@ -407,7 +427,6 @@ function amplessSchemaModels(a, opts = {}) {
407
427
  // Tag pages: newest-first only. Archive (date range) within a tag is not
408
428
  // a common UX pattern; if it's ever needed, add `from`/`to` args here.
409
429
  listPostsByTag: a.query().arguments({
410
- siteId: a.string(),
411
430
  tag: a.string().required(),
412
431
  limit: a.integer(),
413
432
  nextToken: a.string()
@@ -448,6 +467,13 @@ function extendAmplessSchema(a, custom, opts) {
448
467
  ...custom ?? {}
449
468
  });
450
469
  }
470
+ function amplessSchemaAuthorization(allow, opts = {}) {
471
+ const rules = [];
472
+ if (opts.mcpHandlerFunction) {
473
+ rules.push(allow.resource(opts.mcpHandlerFunction).to(["query", "mutate"]));
474
+ }
475
+ return rules;
476
+ }
451
477
  var defaultAuthorizationModes = {
452
478
  defaultAuthorizationMode: "userPool",
453
479
  apiKeyAuthorizationMode: { expiresInDays: 365 }
@@ -455,6 +481,7 @@ var defaultAuthorizationModes = {
455
481
  export {
456
482
  DEFAULT_RESOLVER_PATHS,
457
483
  amplessAuthConfig,
484
+ amplessSchemaAuthorization,
458
485
  amplessSchemaModels,
459
486
  amplessStorageConfig,
460
487
  defaultAuthorizationModes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/backend",
3
- "version": "0.2.0-alpha.9",
3
+ "version": "1.0.0-alpha.20",
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",
@@ -53,14 +53,20 @@
53
53
  "homepage": "https://github.com/heavymoons/ampless/tree/main/packages/backend#readme",
54
54
  "bugs": "https://github.com/heavymoons/ampless/issues",
55
55
  "dependencies": {
56
+ "@aws-crypto/sha256-js": "^5.2.0",
56
57
  "@aws-sdk/client-appsync": "^3.717.0",
57
58
  "@aws-sdk/client-cognito-identity-provider": "^3.717.0",
58
59
  "@aws-sdk/client-dynamodb": "^3.717.0",
59
60
  "@aws-sdk/client-s3": "^3.1048.0",
60
61
  "@aws-sdk/client-sqs": "^3.717.0",
62
+ "@aws-sdk/credential-provider-node": "^3.717.0",
61
63
  "@aws-sdk/lib-dynamodb": "^3.717.0",
62
64
  "@aws-sdk/util-dynamodb": "^3.717.0",
63
- "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.10",
69
+ "@ampless/mcp-server": "1.0.0-alpha.11"
64
70
  },
65
71
  "peerDependencies": {
66
72
  "@aws-amplify/backend": "^1",