@ampless/backend 0.2.0-alpha.1 → 0.2.0-alpha.11

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
@@ -13,6 +13,8 @@ interface DefineAmplessBackendOpts {
13
13
  processorTrusted: FunctionResource;
14
14
  processorUntrusted: FunctionResource;
15
15
  apiKeyRenewer: FunctionResource;
16
+ userAdmin: FunctionResource;
17
+ mcpHandler: FunctionResource;
16
18
  }
17
19
  type AmplessBackend = any;
18
20
  /**
@@ -136,6 +138,33 @@ interface AmplessSchemaModelsOpts {
136
138
  * they must point to actual `.js` files inside the user's project.
137
139
  */
138
140
  resolverPaths?: Partial<AmplessResolverPaths>;
141
+ /**
142
+ * Optional Amplify `defineFunction` ref backing the user-admin
143
+ * AppSync ops (`listAdminUsers` query, `setAdminUserRole` mutation).
144
+ * When supplied, the corresponding `AdminUser` customType + the two
145
+ * ops are added to the schema, both gated to `ampless-admin`.
146
+ *
147
+ * Typed as `unknown` because `defineFunction`'s return type carries
148
+ * internal pnpm paths that don't survive declaration emit — same
149
+ * pattern as `AmplessAuthConfigOpts.postConfirmation`.
150
+ */
151
+ userAdminFunction?: unknown;
152
+ /**
153
+ * Optional Amplify `defineFunction` ref backing the MCP HTTP Lambda.
154
+ * When supplied, the Post and PostTag models gain an `allow.resource(...)`
155
+ * authorization clause granting the Lambda `query` + `mutate` access
156
+ * via AppSync IAM auth. The existing group-based clauses stay intact —
157
+ * this just adds a second principal so the Lambda can dispatch tool
158
+ * calls under its own scoped role, with no Cognito identity or
159
+ * shared API key involved.
160
+ *
161
+ * Phase 4 covers Post + PostTag only (the read/write surface for the
162
+ * post CRUD tools). Media gets the same treatment in Phase 5 once
163
+ * the presigned-PUT upload flow lands.
164
+ *
165
+ * Typed as `unknown` for the same reason as `userAdminFunction`.
166
+ */
167
+ mcpHandlerFunction?: unknown;
139
168
  }
140
169
  /**
141
170
  * The Ampless model + custom query definitions, returned as a plain
@@ -153,6 +182,9 @@ interface AmplessSchemaModelsOpts {
153
182
  * extra models.
154
183
  */
155
184
  declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
185
+ AdminUser?: any;
186
+ listAdminUsers?: any;
187
+ setAdminUserRole?: any;
156
188
  Post: any;
157
189
  Page: any;
158
190
  Media: any;
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";
@@ -16,7 +18,9 @@ function defineAmplessBackend(opts) {
16
18
  eventDispatcher: opts.eventDispatcher,
17
19
  processorTrusted: opts.processorTrusted,
18
20
  processorUntrusted: opts.processorUntrusted,
19
- apiKeyRenewer: opts.apiKeyRenewer
21
+ apiKeyRenewer: opts.apiKeyRenewer,
22
+ userAdmin: opts.userAdmin,
23
+ mcpHandler: opts.mcpHandler
20
24
  });
21
25
  const cfnBucket = backend.storage.resources.cfnResources.cfnBucket;
22
26
  cfnBucket.publicAccessBlockConfiguration = {
@@ -61,6 +65,22 @@ function defineAmplessBackend(opts) {
61
65
  resources: ["arn:aws:cognito-idp:*:*:userpool/*"]
62
66
  })
63
67
  );
68
+ backend.userAdmin.resources.lambda.addToRolePolicy(
69
+ new PolicyStatement({
70
+ effect: Effect.ALLOW,
71
+ actions: [
72
+ "cognito-idp:ListUsers",
73
+ "cognito-idp:AdminListGroupsForUser",
74
+ "cognito-idp:AdminAddUserToGroup",
75
+ "cognito-idp:AdminRemoveUserFromGroup"
76
+ ],
77
+ resources: ["arn:aws:cognito-idp:*:*:userpool/*"]
78
+ })
79
+ );
80
+ backend.userAdmin.resources.lambda.addEnvironment(
81
+ "AMPLESS_USER_POOL_ID",
82
+ backend.auth.resources.userPool.userPoolId
83
+ );
64
84
  const postTable = backend.data.resources.tables["Post"];
65
85
  const cfnPostTable = backend.data.resources.cfnResources.amplifyDynamoDbTables["Post"];
66
86
  cfnPostTable.streamSpecification = { streamViewType: "NEW_AND_OLD_IMAGES" };
@@ -149,6 +169,35 @@ function defineAmplessBackend(opts) {
149
169
  schedule: Schedule.cron({ minute: "0", hour: "3", day: "1", month: "*", year: "*" }),
150
170
  targets: [new LambdaFunction(apiKeyRenewerFn)]
151
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
+ const mcpFunctionUrl = mcpHandlerFn.addFunctionUrl({
186
+ authType: FunctionUrlAuthType.NONE,
187
+ cors: {
188
+ allowedOrigins: ["*"],
189
+ allowedMethods: [HttpMethod.POST],
190
+ allowedHeaders: ["*"],
191
+ maxAge: Duration.hours(1)
192
+ }
193
+ });
194
+ backend.addOutput({
195
+ custom: {
196
+ mcp: {
197
+ endpoint: mcpFunctionUrl.url
198
+ }
199
+ }
200
+ });
152
201
  return backend;
153
202
  }
154
203
 
@@ -201,11 +250,19 @@ function amplessSchemaModels(a, opts = {}) {
201
250
  slug: a.string().required(),
202
251
  title: a.string().required(),
203
252
  excerpt: a.string(),
204
- format: a.enum(["tiptap", "markdown", "html"]),
253
+ format: a.enum(["tiptap", "markdown", "html", "static"]),
205
254
  body: a.json(),
206
255
  status: a.enum(["draft", "published"]),
207
256
  publishedAt: a.datetime(),
208
257
  tags: a.string().array(),
258
+ // Free-form per-post metadata (JSON). Reserved well-known keys
259
+ // are documented on `PostMetadata` in `ampless/src/types.ts`.
260
+ // Currently:
261
+ // no_layout: boolean — serve the post as bare HTML (no theme
262
+ // chrome). The runtime's post dispatcher redirects to the
263
+ // raw route handler when this is true.
264
+ // Other keys are passed through unchanged for plugin / app use.
265
+ metadata: a.json(),
209
266
  // Denormalized GSI keys — set by every write path (admin client,
210
267
  // MCP tools). Same pattern as siteIdStatus: composing the
211
268
  // partition key as a single string lets each public-read query
@@ -220,14 +277,15 @@ function amplessSchemaModels(a, opts = {}) {
220
277
  index("siteIdStatus").sortKeys(["publishedAt"]).name("bySiteIdStatus"),
221
278
  index("siteIdSlug").name("bySiteIdSlug")
222
279
  ]).authorization((allow) => [
223
- allow.groups(["ampless-admin", "ampless-editor"])
280
+ allow.groups(["ampless-admin", "ampless-editor"]),
281
+ ...opts.mcpHandlerFunction ? [allow.resource(opts.mcpHandlerFunction).to(["query", "mutate"])] : []
224
282
  ]),
225
283
  Page: a.model({
226
284
  siteId: a.string().required(),
227
285
  pageId: a.id().required(),
228
286
  slug: a.string().required(),
229
287
  title: a.string().required(),
230
- format: a.enum(["tiptap", "markdown", "html"]),
288
+ format: a.enum(["tiptap", "markdown", "html", "static"]),
231
289
  body: a.json(),
232
290
  status: a.enum(["draft", "published"]),
233
291
  publishedAt: a.datetime()
@@ -274,7 +332,8 @@ function amplessSchemaModels(a, opts = {}) {
274
332
  // Full tag list of the post (for chip rendering on tag pages).
275
333
  tags: a.string().array()
276
334
  }).identifier(["siteIdTag", "publishedAtPostId"]).authorization((allow) => [
277
- allow.groups(["ampless-admin", "ampless-editor"])
335
+ allow.groups(["ampless-admin", "ampless-editor"]),
336
+ ...opts.mcpHandlerFunction ? [allow.resource(opts.mcpHandlerFunction).to(["query", "mutate"])] : []
278
337
  ]),
279
338
  // Generic key/value store. Two roles in one table:
280
339
  // - Site settings: PK = `siteconfig:{siteId}`, SK = dotted key
@@ -308,7 +367,8 @@ function amplessSchemaModels(a, opts = {}) {
308
367
  body: a.json(),
309
368
  status: a.string(),
310
369
  publishedAt: a.datetime(),
311
- tags: a.string().array()
370
+ tags: a.string().array(),
371
+ metadata: a.json()
312
372
  }),
313
373
  // Paginated wrapper for list responses.
314
374
  PublicPostConnection: a.customType({
@@ -367,7 +427,27 @@ function amplessSchemaModels(a, opts = {}) {
367
427
  ).authorization((allow) => [
368
428
  allow.publicApiKey(),
369
429
  allow.groups(["ampless-admin", "ampless-editor"])
370
- ])
430
+ ]),
431
+ // User management ops, only wired when the caller supplies a
432
+ // Lambda function ref. Conditionally spread because
433
+ // `a.handler.function(undefined)` is not a valid call — projects
434
+ // that haven't opted into the user-admin Lambda must not see
435
+ // these schema entries.
436
+ ...opts.userAdminFunction ? {
437
+ AdminUser: a.customType({
438
+ userId: a.string().required(),
439
+ email: a.string().required(),
440
+ // 'admin' | 'editor' | 'none' — stored as string because
441
+ // a.enum() in customType doesn't round-trip cleanly across
442
+ // AppSync's typegen + the admin client cast pattern.
443
+ role: a.string().required()
444
+ }),
445
+ listAdminUsers: a.query().returns(a.ref("AdminUser").array()).handler(a.handler.function(opts.userAdminFunction)).authorization((allow) => [allow.groups(["ampless-admin"])]),
446
+ setAdminUserRole: a.mutation().arguments({
447
+ userId: a.string().required(),
448
+ role: a.string().required()
449
+ }).returns(a.ref("AdminUser")).handler(a.handler.function(opts.userAdminFunction)).authorization((allow) => [allow.groups(["ampless-admin"])])
450
+ } : {}
371
451
  };
372
452
  }
373
453
  function extendAmplessSchema(a, custom, opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/backend",
3
- "version": "0.2.0-alpha.1",
3
+ "version": "0.2.0-alpha.11",
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",
@@ -13,6 +13,10 @@
13
13
  "import": "./dist/auth/post-confirmation.js",
14
14
  "types": "./dist/auth/post-confirmation.d.ts"
15
15
  },
16
+ "./auth/user-admin": {
17
+ "import": "./dist/auth/user-admin.js",
18
+ "types": "./dist/auth/user-admin.d.ts"
19
+ },
16
20
  "./events/dispatcher": {
17
21
  "import": "./dist/events/dispatcher.js",
18
22
  "types": "./dist/events/dispatcher.d.ts"
@@ -28,6 +32,10 @@
28
32
  "./functions/api-key-renewer": {
29
33
  "import": "./dist/functions/api-key-renewer.js",
30
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"
31
39
  }
32
40
  },
33
41
  "files": [
@@ -45,14 +53,19 @@
45
53
  "homepage": "https://github.com/heavymoons/ampless/tree/main/packages/backend#readme",
46
54
  "bugs": "https://github.com/heavymoons/ampless/issues",
47
55
  "dependencies": {
56
+ "@aws-crypto/sha256-js": "^5.2.0",
48
57
  "@aws-sdk/client-appsync": "^3.717.0",
49
58
  "@aws-sdk/client-cognito-identity-provider": "^3.717.0",
50
59
  "@aws-sdk/client-dynamodb": "^3.717.0",
51
60
  "@aws-sdk/client-s3": "^3.1048.0",
52
61
  "@aws-sdk/client-sqs": "^3.717.0",
62
+ "@aws-sdk/credential-provider-node": "^3.717.0",
53
63
  "@aws-sdk/lib-dynamodb": "^3.717.0",
54
64
  "@aws-sdk/util-dynamodb": "^3.717.0",
55
- "ampless": "0.2.0-alpha.1"
65
+ "@smithy/protocol-http": "^5.3.0",
66
+ "@smithy/signature-v4": "^5.4.0",
67
+ "ampless": "0.2.0-alpha.6",
68
+ "@ampless/mcp-server": "0.2.0-alpha.6"
56
69
  },
57
70
  "peerDependencies": {
58
71
  "@aws-amplify/backend": "^1",