@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/auth/post-confirmation.js +2 -0
- package/dist/auth/user-admin.js +2 -0
- package/dist/chunk-BYXBJQAS.js +0 -0
- package/dist/events/dispatcher.js +4 -5
- package/dist/events/processor-trusted.d.ts +5 -5
- package/dist/events/processor-trusted.js +17 -22
- package/dist/events/processor-untrusted.js +4 -4
- package/dist/functions/api-key-renewer.js +2 -0
- package/dist/functions/mcp-handler.d.ts +41 -0
- package/dist/functions/mcp-handler.js +1192 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.js +79 -33
- package/package.json +12 -2
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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(["
|
|
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(["
|
|
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(["
|
|
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:
|
|
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(["
|
|
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
|
|
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({
|
|
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.
|
|
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
|
-
"
|
|
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",
|