@ampless/backend 0.2.0-alpha.9 → 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
@@ -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
@@ -350,7 +365,6 @@ function amplessSchemaModels(a, opts = {}) {
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.
352
367
  PublicPost: a.customType({
353
- siteId: a.string().required(),
354
368
  postId: a.id().required(),
355
369
  slug: a.string().required(),
356
370
  title: a.string().required(),
@@ -377,7 +391,6 @@ function amplessSchemaModels(a, opts = {}) {
377
391
  // only thing guests can see is `status === 'published'` rows projected
378
392
  // onto `PublicPost`.
379
393
  listPublishedPosts: a.query().arguments({
380
- siteId: a.string(),
381
394
  // Both ISO 8601 strings; query SK condition is pushed into DynamoDB
382
395
  // so only the matching publishedAt range is read.
383
396
  from: a.datetime(),
@@ -395,7 +408,7 @@ function amplessSchemaModels(a, opts = {}) {
395
408
  allow.publicApiKey(),
396
409
  allow.groups(["ampless-admin", "ampless-editor"])
397
410
  ]),
398
- 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(
399
412
  a.handler.custom({
400
413
  dataSource: a.ref("Post"),
401
414
  entry: resolverPaths.getPublishedPost
@@ -407,7 +420,6 @@ function amplessSchemaModels(a, opts = {}) {
407
420
  // Tag pages: newest-first only. Archive (date range) within a tag is not
408
421
  // a common UX pattern; if it's ever needed, add `from`/`to` args here.
409
422
  listPostsByTag: a.query().arguments({
410
- siteId: a.string(),
411
423
  tag: a.string().required(),
412
424
  limit: a.integer(),
413
425
  nextToken: a.string()
@@ -448,6 +460,13 @@ function extendAmplessSchema(a, custom, opts) {
448
460
  ...custom ?? {}
449
461
  });
450
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
+ }
451
470
  var defaultAuthorizationModes = {
452
471
  defaultAuthorizationMode: "userPool",
453
472
  apiKeyAuthorizationMode: { expiresInDays: 365 }
@@ -455,6 +474,7 @@ var defaultAuthorizationModes = {
455
474
  export {
456
475
  DEFAULT_RESOLVER_PATHS,
457
476
  amplessAuthConfig,
477
+ amplessSchemaAuthorization,
458
478
  amplessSchemaModels,
459
479
  amplessStorageConfig,
460
480
  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.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",
@@ -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.9",
69
+ "@ampless/mcp-server": "1.0.0-alpha.10"
64
70
  },
65
71
  "peerDependencies": {
66
72
  "@aws-amplify/backend": "^1",