@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/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 +16 -8
- package/dist/functions/mcp-handler.js +1146 -11
- package/dist/index.d.ts +47 -1
- package/dist/index.js +60 -33
- package/package.json +8 -2
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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(["
|
|
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(["
|
|
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(["
|
|
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:
|
|
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(["
|
|
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
|
|
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({
|
|
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.
|
|
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
|
-
"
|
|
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",
|