@ampless/backend 0.2.0-alpha.0
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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/auth/post-confirmation.d.ts +14 -0
- package/dist/auth/post-confirmation.js +30 -0
- package/dist/events/dispatcher.d.ts +15 -0
- package/dist/events/dispatcher.js +86 -0
- package/dist/events/processor-trusted.d.ts +36 -0
- package/dist/events/processor-trusted.js +159 -0
- package/dist/events/processor-untrusted.d.ts +28 -0
- package/dist/events/processor-untrusted.js +44 -0
- package/dist/functions/api-key-renewer.d.ts +8 -0
- package/dist/functions/api-key-renewer.js +40 -0
- package/dist/index.d.ts +158 -0
- package/dist/index.js +393 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ampless contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @ampless/backend
|
|
2
|
+
|
|
3
|
+
Amplify Gen 2 backend factories for [ampless](https://github.com/heavymoons/ampless). Bundles the IAM / SQS / DynamoDB-stream wiring, the auth / data / storage definitions, and every event-processing Lambda behind one `defineAmplessBackend(...)` factory.
|
|
4
|
+
|
|
5
|
+
> **Pre-release / alpha.** Breaking changes possible in any minor version until v1.0.
|
|
6
|
+
|
|
7
|
+
Splitting this out of the template lets you `npm update @ampless/backend` without re-running the scaffolder. Backend bug fixes and infrastructure improvements arrive through the package; the user-side `amplify/` tree becomes a handful of 1–5 line shells that compose the factories.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @ampless/backend@alpha ampless@alpha
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer dependencies: `@aws-amplify/backend` (^1), `aws-cdk-lib` (^2). The CLI scaffolder pins compatible versions in the template's `package.json`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### `amplify/backend.ts`
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineAmplessBackend } from '@ampless/backend'
|
|
23
|
+
import { auth } from './auth/resource'
|
|
24
|
+
import { data } from './data/resource'
|
|
25
|
+
import { storage } from './storage/resource'
|
|
26
|
+
import { postConfirmation } from './auth/post-confirmation/resource'
|
|
27
|
+
import { eventDispatcher } from './events/dispatcher/resource'
|
|
28
|
+
import { processorTrusted } from './events/processor-trusted/resource'
|
|
29
|
+
import { processorUntrusted } from './events/processor-untrusted/resource'
|
|
30
|
+
import { apiKeyRenewer } from './functions/api-key-renewer/resource'
|
|
31
|
+
|
|
32
|
+
export default defineAmplessBackend({
|
|
33
|
+
auth, data, storage, postConfirmation,
|
|
34
|
+
eventDispatcher, processorTrusted, processorUntrusted, apiKeyRenewer,
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `amplify/auth/resource.ts`
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { defineAmplessAuth } from '@ampless/backend'
|
|
42
|
+
import { postConfirmation } from './post-confirmation/resource'
|
|
43
|
+
|
|
44
|
+
export const auth = defineAmplessAuth({ postConfirmation })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `amplify/data/resource.ts`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
|
|
51
|
+
import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
|
|
52
|
+
|
|
53
|
+
const schema = a.schema({
|
|
54
|
+
...amplessSchemaModels(a),
|
|
55
|
+
// Add custom models here — they live alongside the built-ins:
|
|
56
|
+
// MyCustomModel: a.model({ ... }).authorization((allow) => [...]),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export type Schema = ClientSchema<typeof schema>
|
|
60
|
+
export const data = defineData({ schema, authorizationModes: defaultAuthorizationModes })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The three AppSync JS resolver files (`list-published-posts.js`, `get-published-post.js`, `list-posts-by-tag.js`) stay in the template — AppSync resolves resolver `entry` paths at CDK synth time relative to the file that calls `defineData`, and pnpm-symlinked `node_modules` paths don't survive that resolution. If you move them out of `amplify/data/`, pass new paths through `amplessSchemaModels(a, { resolverPaths })`.
|
|
64
|
+
|
|
65
|
+
### `amplify/storage/resource.ts`
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { defineAmplessStorage } from '@ampless/backend'
|
|
69
|
+
export const storage = defineAmplessStorage()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Lambda thin shells
|
|
73
|
+
|
|
74
|
+
Every handler file in `amplify/auth/`, `amplify/events/`, and `amplify/functions/` becomes a 1–3 line re-export. Amplify's esbuild follows the import into this package and bundles the real handler code into the Lambda artifact.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// amplify/auth/post-confirmation/handler.ts
|
|
78
|
+
export { handler } from '@ampless/backend/auth/post-confirmation'
|
|
79
|
+
|
|
80
|
+
// amplify/events/dispatcher/handler.ts
|
|
81
|
+
export { handler } from '@ampless/backend/events/dispatcher'
|
|
82
|
+
|
|
83
|
+
// amplify/events/processor-trusted/handler.ts
|
|
84
|
+
import config from '../../../cms.config'
|
|
85
|
+
import { createProcessorTrustedHandler } from '@ampless/backend/events/processor-trusted'
|
|
86
|
+
export const handler = createProcessorTrustedHandler({
|
|
87
|
+
plugins: config.plugins,
|
|
88
|
+
site: config.site,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// amplify/events/processor-untrusted/handler.ts
|
|
92
|
+
import config from '../../../cms.config'
|
|
93
|
+
import { createProcessorUntrustedHandler } from '@ampless/backend/events/processor-untrusted'
|
|
94
|
+
export const handler = createProcessorUntrustedHandler({
|
|
95
|
+
plugins: config.plugins,
|
|
96
|
+
site: config.site,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// amplify/functions/api-key-renewer/handler.ts
|
|
100
|
+
export { handler } from '@ampless/backend/functions/api-key-renewer'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Sub-paths
|
|
104
|
+
|
|
105
|
+
- `@ampless/backend` — `defineAmplessBackend`, `defineAmplessAuth`, `defineAmplessStorage`, `amplessSchemaModels`, `extendAmplessSchema`, `defaultAuthorizationModes`
|
|
106
|
+
- `@ampless/backend/auth/post-confirmation` — Lambda handler
|
|
107
|
+
- `@ampless/backend/events/dispatcher` — Lambda handler
|
|
108
|
+
- `@ampless/backend/events/processor-trusted` — `createProcessorTrustedHandler({ plugins, site })`
|
|
109
|
+
- `@ampless/backend/events/processor-untrusted` — `createProcessorUntrustedHandler({ plugins, site })`
|
|
110
|
+
- `@ampless/backend/functions/api-key-renewer` — Lambda handler
|
|
111
|
+
|
|
112
|
+
## What's still in the template
|
|
113
|
+
|
|
114
|
+
- `amplify/data/*.js` — AppSync JS resolvers (file-path constraint).
|
|
115
|
+
- Every `resource.ts` and `handler.ts` — they're thin shells, but they hold the `entry: './handler.ts'` paths that Amplify's CDK synth resolves on the user side.
|
|
116
|
+
- `cms.config.ts` and `themes-registry.ts` — user-owned customisation surface.
|
|
117
|
+
- Theme components under `templates/<theme>/` — user-owned.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PostConfirmationTriggerHandler } from 'aws-lambda';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-confirmation Cognito trigger: promote the first confirmed user
|
|
5
|
+
* to `ampless-admin`. Subsequent signups stay in the default group
|
|
6
|
+
* (i.e. no group) and the admin must promote them manually.
|
|
7
|
+
*
|
|
8
|
+
* Re-exported from the template's thin shell
|
|
9
|
+
* `amplify/auth/post-confirmation/handler.ts`; Amplify's esbuild
|
|
10
|
+
* bundles this module into the Lambda artifact.
|
|
11
|
+
*/
|
|
12
|
+
declare const handler: PostConfirmationTriggerHandler;
|
|
13
|
+
|
|
14
|
+
export { handler };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/auth/post-confirmation.ts
|
|
2
|
+
import {
|
|
3
|
+
CognitoIdentityProviderClient,
|
|
4
|
+
AdminAddUserToGroupCommand,
|
|
5
|
+
ListUsersInGroupCommand
|
|
6
|
+
} from "@aws-sdk/client-cognito-identity-provider";
|
|
7
|
+
var cognito = new CognitoIdentityProviderClient({});
|
|
8
|
+
var handler = async (event) => {
|
|
9
|
+
const { userPoolId, userName } = event;
|
|
10
|
+
const existing = await cognito.send(
|
|
11
|
+
new ListUsersInGroupCommand({
|
|
12
|
+
UserPoolId: userPoolId,
|
|
13
|
+
GroupName: "ampless-admin",
|
|
14
|
+
Limit: 1
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
if (!existing.Users || existing.Users.length === 0) {
|
|
18
|
+
await cognito.send(
|
|
19
|
+
new AdminAddUserToGroupCommand({
|
|
20
|
+
UserPoolId: userPoolId,
|
|
21
|
+
Username: userName,
|
|
22
|
+
GroupName: "ampless-admin"
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return event;
|
|
27
|
+
};
|
|
28
|
+
export {
|
|
29
|
+
handler
|
|
30
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DynamoDBStreamHandler } from 'aws-lambda';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DynamoDB-stream → SQS-fanout dispatcher. Wired to both the Post and
|
|
5
|
+
* KvStore tables; routes content events to the trusted+untrusted
|
|
6
|
+
* queues and site-settings updates likewise. trust_level isolation
|
|
7
|
+
* is enforced by the downstream Lambdas' IAM roles, not by message
|
|
8
|
+
* routing here.
|
|
9
|
+
*
|
|
10
|
+
* Re-exported by the template's thin shell
|
|
11
|
+
* `amplify/events/dispatcher/handler.ts`.
|
|
12
|
+
*/
|
|
13
|
+
declare const handler: DynamoDBStreamHandler;
|
|
14
|
+
|
|
15
|
+
export { handler };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/events/dispatcher.ts
|
|
2
|
+
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
3
|
+
import {
|
|
4
|
+
SQSClient,
|
|
5
|
+
SendMessageBatchCommand
|
|
6
|
+
} from "@aws-sdk/client-sqs";
|
|
7
|
+
import { detectContentEvents } from "ampless";
|
|
8
|
+
function requireEnv(name) {
|
|
9
|
+
const v = process.env[name];
|
|
10
|
+
if (!v) throw new Error(`event-dispatcher: missing required env var ${name}`);
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
var sqs = new SQSClient({});
|
|
14
|
+
var TRUSTED_QUEUE_URL = requireEnv("TRUSTED_QUEUE_URL");
|
|
15
|
+
var UNTRUSTED_QUEUE_URL = requireEnv("UNTRUSTED_QUEUE_URL");
|
|
16
|
+
function tableNameFromArn(arn) {
|
|
17
|
+
if (!arn) return null;
|
|
18
|
+
const match = arn.match(/:table\/([^/]+)/);
|
|
19
|
+
return match ? match[1] : null;
|
|
20
|
+
}
|
|
21
|
+
function emitContentEvents(record, timestamp) {
|
|
22
|
+
const oldItem = record.dynamodb?.OldImage ? unmarshall(record.dynamodb.OldImage) : null;
|
|
23
|
+
const newItem = record.dynamodb?.NewImage ? unmarshall(record.dynamodb.NewImage) : null;
|
|
24
|
+
const types = detectContentEvents({
|
|
25
|
+
eventName: record.eventName,
|
|
26
|
+
oldStatus: oldItem?.status,
|
|
27
|
+
newStatus: newItem?.status
|
|
28
|
+
});
|
|
29
|
+
if (types.length === 0) return [];
|
|
30
|
+
const item = newItem ?? oldItem ?? {};
|
|
31
|
+
const payload = {
|
|
32
|
+
siteId: item.siteId,
|
|
33
|
+
postId: item.postId,
|
|
34
|
+
slug: item.slug,
|
|
35
|
+
title: item.title,
|
|
36
|
+
status: item.status,
|
|
37
|
+
publishedAt: item.publishedAt,
|
|
38
|
+
tags: item.tags
|
|
39
|
+
};
|
|
40
|
+
return types.map((type) => ({ type, payload, timestamp }));
|
|
41
|
+
}
|
|
42
|
+
function emitKvEvents(record, timestamp) {
|
|
43
|
+
const item = record.dynamodb?.NewImage ?? record.dynamodb?.OldImage ? unmarshall(record.dynamodb.NewImage ?? record.dynamodb.OldImage) : {};
|
|
44
|
+
const pk = item.pk;
|
|
45
|
+
if (!pk) return [];
|
|
46
|
+
if (!pk.startsWith("siteconfig:")) return [];
|
|
47
|
+
const siteId = pk.slice("siteconfig:".length);
|
|
48
|
+
if (!siteId) return [];
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
type: "site.settings.updated",
|
|
52
|
+
payload: { siteId },
|
|
53
|
+
timestamp
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
async function sendBatch(queueUrl, entries) {
|
|
58
|
+
for (let i = 0; i < entries.length; i += 10) {
|
|
59
|
+
const chunk = entries.slice(i, i + 10);
|
|
60
|
+
await sqs.send(new SendMessageBatchCommand({ QueueUrl: queueUrl, Entries: chunk }));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var handler = async (event) => {
|
|
64
|
+
const messages = [];
|
|
65
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
66
|
+
for (const record of event.Records) {
|
|
67
|
+
const tableName = tableNameFromArn(record.eventSourceARN);
|
|
68
|
+
if (tableName && tableName.startsWith("Post-")) {
|
|
69
|
+
messages.push(...emitContentEvents(record, timestamp));
|
|
70
|
+
} else if (tableName && tableName.startsWith("KvStore-")) {
|
|
71
|
+
messages.push(...emitKvEvents(record, timestamp));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (messages.length === 0) return;
|
|
75
|
+
const entries = messages.map((m, i) => ({
|
|
76
|
+
Id: `msg-${i}`,
|
|
77
|
+
MessageBody: JSON.stringify(m)
|
|
78
|
+
}));
|
|
79
|
+
await Promise.all([
|
|
80
|
+
sendBatch(TRUSTED_QUEUE_URL, entries),
|
|
81
|
+
sendBatch(UNTRUSTED_QUEUE_URL, entries)
|
|
82
|
+
]);
|
|
83
|
+
};
|
|
84
|
+
export {
|
|
85
|
+
handler
|
|
86
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SQSHandler } from 'aws-lambda';
|
|
2
|
+
import { AmplessPlugin, Config } from 'ampless';
|
|
3
|
+
|
|
4
|
+
interface CreateProcessorTrustedHandlerOpts {
|
|
5
|
+
/**
|
|
6
|
+
* The full `cms.config.plugins` array. The handler filters down to
|
|
7
|
+
* trusted plugins itself so callers don't need to remember the
|
|
8
|
+
* filter, and so adding `privileged` later only touches the handler
|
|
9
|
+
* code in this package.
|
|
10
|
+
*/
|
|
11
|
+
plugins?: AmplessPlugin[];
|
|
12
|
+
/**
|
|
13
|
+
* The `cms.config.site` block, surfaced to plugin hooks via
|
|
14
|
+
* `ctx.site`. Pass through from the thin shell — handlers must
|
|
15
|
+
* not import `cms.config` directly because the package can't know
|
|
16
|
+
* the user's project layout.
|
|
17
|
+
*/
|
|
18
|
+
site: Config['site'];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SQS-driven trusted plugin executor. Trusted plugins get a runtime
|
|
22
|
+
* context with `listPublishedPosts` (one Query per site partition)
|
|
23
|
+
* and `writePublicAsset` (S3 PutObject under
|
|
24
|
+
* `public/plugins/{name}/{siteId}/{key}`).
|
|
25
|
+
*
|
|
26
|
+
* Built-in: rebuilds the site-settings JSON cache at
|
|
27
|
+
* `public/site-settings/{siteId}.json` whenever a
|
|
28
|
+
* `site.settings.updated` event arrives.
|
|
29
|
+
*
|
|
30
|
+
* Re-exported by the template's thin shell
|
|
31
|
+
* `amplify/events/processor-trusted/handler.ts` which supplies the
|
|
32
|
+
* site-wide plugin list and site config from `cms.config`.
|
|
33
|
+
*/
|
|
34
|
+
declare function createProcessorTrustedHandler(opts: CreateProcessorTrustedHandlerOpts): SQSHandler;
|
|
35
|
+
|
|
36
|
+
export { type CreateProcessorTrustedHandlerOpts, createProcessorTrustedHandler };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/events/processor-trusted.ts
|
|
2
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
3
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
4
|
+
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
5
|
+
import {
|
|
6
|
+
formatPublicAssetUrl
|
|
7
|
+
} from "ampless";
|
|
8
|
+
function requireEnv(name) {
|
|
9
|
+
const v = process.env[name];
|
|
10
|
+
if (!v) throw new Error(`processor-trusted: missing required env var ${name}`);
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
var POST_BY_SITE_STATUS_INDEX = "bySiteIdStatus";
|
|
14
|
+
function safeParse(s) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(s);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.warn("[trusted-processor] post.body is not valid JSON; passing through as string", err);
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function createProcessorTrustedHandler(opts) {
|
|
23
|
+
const trustedPlugins = (opts.plugins ?? []).filter(
|
|
24
|
+
(p) => typeof p === "object" && p.trust_level === "trusted"
|
|
25
|
+
);
|
|
26
|
+
const s3 = new S3Client({});
|
|
27
|
+
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
28
|
+
const BUCKET = requireEnv("AMPLESS_BUCKET_NAME");
|
|
29
|
+
const POST_TABLE = requireEnv("AMPLESS_POST_TABLE");
|
|
30
|
+
const KV_TABLE = requireEnv("AMPLESS_KV_TABLE");
|
|
31
|
+
const REGION = requireEnv("AWS_REGION");
|
|
32
|
+
async function listPublished(siteId) {
|
|
33
|
+
const items = [];
|
|
34
|
+
let exclusiveStartKey;
|
|
35
|
+
const partition = `${siteId}#published`;
|
|
36
|
+
do {
|
|
37
|
+
const res = await ddb.send(
|
|
38
|
+
new QueryCommand({
|
|
39
|
+
TableName: POST_TABLE,
|
|
40
|
+
IndexName: POST_BY_SITE_STATUS_INDEX,
|
|
41
|
+
KeyConditionExpression: "#siteIdStatus = :siteIdStatus",
|
|
42
|
+
ExpressionAttributeNames: { "#siteIdStatus": "siteIdStatus" },
|
|
43
|
+
ExpressionAttributeValues: { ":siteIdStatus": partition },
|
|
44
|
+
ScanIndexForward: false,
|
|
45
|
+
ExclusiveStartKey: exclusiveStartKey
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
for (const row of res.Items ?? []) {
|
|
49
|
+
items.push({
|
|
50
|
+
postId: row.postId,
|
|
51
|
+
siteId: row.siteId,
|
|
52
|
+
slug: row.slug,
|
|
53
|
+
title: row.title,
|
|
54
|
+
excerpt: row.excerpt ?? void 0,
|
|
55
|
+
format: row.format ?? "markdown",
|
|
56
|
+
body: typeof row.body === "string" ? safeParse(row.body) : row.body,
|
|
57
|
+
status: row.status ?? "published",
|
|
58
|
+
publishedAt: row.publishedAt ?? void 0,
|
|
59
|
+
tags: Array.isArray(row.tags) ? row.tags : []
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
exclusiveStartKey = res.LastEvaluatedKey;
|
|
63
|
+
} while (exclusiveStartKey);
|
|
64
|
+
return items;
|
|
65
|
+
}
|
|
66
|
+
function makeContext(plugin, siteId) {
|
|
67
|
+
return {
|
|
68
|
+
siteId,
|
|
69
|
+
site: opts.site,
|
|
70
|
+
listPublishedPosts: () => listPublished(siteId),
|
|
71
|
+
async writePublicAsset(key, body, contentType) {
|
|
72
|
+
const objectKey = `public/plugins/${plugin.name}/${siteId}/${key}`;
|
|
73
|
+
await s3.send(
|
|
74
|
+
new PutObjectCommand({
|
|
75
|
+
Bucket: BUCKET,
|
|
76
|
+
Key: objectKey,
|
|
77
|
+
Body: body,
|
|
78
|
+
ContentType: contentType,
|
|
79
|
+
CacheControl: "public, max-age=300"
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
return formatPublicAssetUrl(BUCKET, REGION, objectKey);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function rebuildSiteSettingsCache(siteId) {
|
|
87
|
+
const settings = {};
|
|
88
|
+
let exclusiveStartKey;
|
|
89
|
+
do {
|
|
90
|
+
const res = await ddb.send(
|
|
91
|
+
new QueryCommand({
|
|
92
|
+
TableName: KV_TABLE,
|
|
93
|
+
KeyConditionExpression: "#pk = :pk",
|
|
94
|
+
ExpressionAttributeNames: { "#pk": "pk" },
|
|
95
|
+
ExpressionAttributeValues: { ":pk": `siteconfig:${siteId}` },
|
|
96
|
+
ExclusiveStartKey: exclusiveStartKey
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
for (const row of res.Items ?? []) {
|
|
100
|
+
const sk = row.sk;
|
|
101
|
+
if (!sk) continue;
|
|
102
|
+
const raw = row.value;
|
|
103
|
+
settings[sk] = typeof raw === "string" ? safeParse(raw) : raw;
|
|
104
|
+
}
|
|
105
|
+
exclusiveStartKey = res.LastEvaluatedKey;
|
|
106
|
+
} while (exclusiveStartKey);
|
|
107
|
+
const objectKey = `public/site-settings/${siteId}.json`;
|
|
108
|
+
await s3.send(
|
|
109
|
+
new PutObjectCommand({
|
|
110
|
+
Bucket: BUCKET,
|
|
111
|
+
Key: objectKey,
|
|
112
|
+
Body: JSON.stringify(settings),
|
|
113
|
+
ContentType: "application/json; charset=utf-8",
|
|
114
|
+
// Public site fetches with Next.js fetch cache (60s) — keep this
|
|
115
|
+
// short so admin edits propagate quickly without thrashing.
|
|
116
|
+
CacheControl: "public, max-age=60"
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
console.log(
|
|
120
|
+
`[site-settings-cache] wrote ${objectKey} (${Object.keys(settings).length} keys)`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return async (event) => {
|
|
124
|
+
for (const record of event.Records) {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(record.body);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error("[trusted-processor] bad message", record.body, err);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const siteId = parsed.payload.siteId ?? "default";
|
|
133
|
+
if (parsed.type === "site.settings.updated") {
|
|
134
|
+
try {
|
|
135
|
+
await rebuildSiteSettingsCache(siteId);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(
|
|
138
|
+
`[trusted-processor] site-settings-cache rebuild failed for ${siteId}`,
|
|
139
|
+
err
|
|
140
|
+
);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const plugin of trustedPlugins) {
|
|
145
|
+
const hook = plugin.hooks?.[parsed.type];
|
|
146
|
+
if (!hook) continue;
|
|
147
|
+
try {
|
|
148
|
+
await hook(parsed, makeContext(plugin, siteId));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error(`[trusted-processor] ${plugin.name}.${parsed.type} failed`, err);
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
createProcessorTrustedHandler
|
|
159
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SQSHandler } from 'aws-lambda';
|
|
2
|
+
import { AmplessPlugin, Config } from 'ampless';
|
|
3
|
+
|
|
4
|
+
interface CreateProcessorUntrustedHandlerOpts {
|
|
5
|
+
/**
|
|
6
|
+
* The full `cms.config.plugins` array. The handler filters down to
|
|
7
|
+
* untrusted plugins itself.
|
|
8
|
+
*/
|
|
9
|
+
plugins?: AmplessPlugin[];
|
|
10
|
+
/**
|
|
11
|
+
* The `cms.config.site` block, surfaced to plugin hooks via
|
|
12
|
+
* `ctx.site` (read-only — untrusted plugins have no AWS-touching
|
|
13
|
+
* capabilities).
|
|
14
|
+
*/
|
|
15
|
+
site: Config['site'];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* SQS-driven untrusted plugin executor. Untrusted plugins get a runtime
|
|
19
|
+
* context with NO AWS-touching capabilities — they can only read the
|
|
20
|
+
* event payload, run pure JS, and return. Any attempt to call
|
|
21
|
+
* `listPublishedPosts` or `writePublicAsset` throws.
|
|
22
|
+
*
|
|
23
|
+
* Re-exported by the template's thin shell
|
|
24
|
+
* `amplify/events/processor-untrusted/handler.ts`.
|
|
25
|
+
*/
|
|
26
|
+
declare function createProcessorUntrustedHandler(opts: CreateProcessorUntrustedHandlerOpts): SQSHandler;
|
|
27
|
+
|
|
28
|
+
export { type CreateProcessorUntrustedHandlerOpts, createProcessorUntrustedHandler };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/events/processor-untrusted.ts
|
|
2
|
+
function createProcessorUntrustedHandler(opts) {
|
|
3
|
+
const untrustedPlugins = (opts.plugins ?? []).filter(
|
|
4
|
+
(p) => typeof p === "object" && p.trust_level === "untrusted"
|
|
5
|
+
);
|
|
6
|
+
function makeContext(siteId) {
|
|
7
|
+
return {
|
|
8
|
+
siteId,
|
|
9
|
+
site: opts.site,
|
|
10
|
+
async listPublishedPosts() {
|
|
11
|
+
throw new Error("untrusted plugins cannot list posts");
|
|
12
|
+
},
|
|
13
|
+
async writePublicAsset() {
|
|
14
|
+
throw new Error("untrusted plugins cannot write assets");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return async (event) => {
|
|
19
|
+
if (untrustedPlugins.length === 0) return;
|
|
20
|
+
for (const record of event.Records) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(record.body);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error("[untrusted-processor] bad message", record.body, err);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const siteId = parsed.payload.siteId ?? "default";
|
|
29
|
+
for (const plugin of untrustedPlugins) {
|
|
30
|
+
const hook = plugin.hooks?.[parsed.type];
|
|
31
|
+
if (!hook) continue;
|
|
32
|
+
try {
|
|
33
|
+
await hook(parsed, makeContext(siteId));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`[untrusted-processor] ${plugin.name}.${parsed.type} failed`, err);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
createProcessorUntrustedHandler
|
|
44
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBridge-triggered Lambda that rolls every AppSync API key on
|
|
3
|
+
* this API forward to `now + 364 days`. Re-exported by the template's
|
|
4
|
+
* thin shell `amplify/functions/api-key-renewer/handler.ts`.
|
|
5
|
+
*/
|
|
6
|
+
declare const handler: () => Promise<void>;
|
|
7
|
+
|
|
8
|
+
export { handler };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/functions/api-key-renewer.ts
|
|
2
|
+
import {
|
|
3
|
+
AppSyncClient,
|
|
4
|
+
ListApiKeysCommand,
|
|
5
|
+
UpdateApiKeyCommand
|
|
6
|
+
} from "@aws-sdk/client-appsync";
|
|
7
|
+
var client = new AppSyncClient({});
|
|
8
|
+
function requireEnv(name) {
|
|
9
|
+
const v = process.env[name];
|
|
10
|
+
if (!v) throw new Error(`api-key-renewer: missing required env var ${name}`);
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
var APPSYNC_API_ID = requireEnv("APPSYNC_API_ID");
|
|
14
|
+
var TTL_DAYS = 364;
|
|
15
|
+
var handler = async () => {
|
|
16
|
+
const { apiKeys } = await client.send(
|
|
17
|
+
new ListApiKeysCommand({ apiId: APPSYNC_API_ID })
|
|
18
|
+
);
|
|
19
|
+
if (!apiKeys || apiKeys.length === 0) {
|
|
20
|
+
console.warn("[api-key-renewer] no api keys to renew");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const expires = Math.floor(Date.now() / 1e3) + TTL_DAYS * 86400;
|
|
24
|
+
const targetIso = new Date(expires * 1e3).toISOString();
|
|
25
|
+
for (const k of apiKeys) {
|
|
26
|
+
if (!k.id) continue;
|
|
27
|
+
const beforeIso = k.expires ? new Date(k.expires * 1e3).toISOString() : "(none)";
|
|
28
|
+
await client.send(
|
|
29
|
+
new UpdateApiKeyCommand({
|
|
30
|
+
apiId: APPSYNC_API_ID,
|
|
31
|
+
id: k.id,
|
|
32
|
+
expires
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
console.log(`[api-key-renewer] ${k.id}: ${beforeIso} -> ${targetIso}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export {
|
|
39
|
+
handler
|
|
40
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { defineData } from '@aws-amplify/backend';
|
|
2
|
+
|
|
3
|
+
type AuthResource = any;
|
|
4
|
+
type DataResource = any;
|
|
5
|
+
type StorageResource = any;
|
|
6
|
+
type FunctionResource = any;
|
|
7
|
+
interface DefineAmplessBackendOpts {
|
|
8
|
+
auth: AuthResource;
|
|
9
|
+
data: DataResource;
|
|
10
|
+
storage: StorageResource;
|
|
11
|
+
postConfirmation: FunctionResource;
|
|
12
|
+
eventDispatcher: FunctionResource;
|
|
13
|
+
processorTrusted: FunctionResource;
|
|
14
|
+
processorUntrusted: FunctionResource;
|
|
15
|
+
apiKeyRenewer: FunctionResource;
|
|
16
|
+
}
|
|
17
|
+
type AmplessBackend = any;
|
|
18
|
+
/**
|
|
19
|
+
* The end-to-end ampless backend wiring: identical to the 251-line
|
|
20
|
+
* `backend.ts` that used to live in `templates/_shared/amplify/`,
|
|
21
|
+
* but parameterised on the resource objects so users only have to
|
|
22
|
+
* compose the imports.
|
|
23
|
+
*
|
|
24
|
+
* What it does, in order:
|
|
25
|
+
* 1. Calls `defineBackend` with every Ampless resource.
|
|
26
|
+
* 2. Opens up the S3 bucket to anonymous reads under `public/*`
|
|
27
|
+
* plus a permissive CORS rule for cross-origin asset fetches.
|
|
28
|
+
* 3. Relaxes the Cognito password policy to length-only.
|
|
29
|
+
* 4. Grants the post-confirmation Lambda permission to add
|
|
30
|
+
* newly-confirmed users to Cognito groups.
|
|
31
|
+
* 5. Enables DynamoDB Streams on Post + KvStore, provisions
|
|
32
|
+
* Trusted/Untrusted SQS queues with a shared DLQ, wires the
|
|
33
|
+
* dispatcher Lambda to both streams and both queues, and
|
|
34
|
+
* grants the trusted processor scoped data / S3 access.
|
|
35
|
+
* 6. Schedules the AppSync API key renewer to run monthly.
|
|
36
|
+
*
|
|
37
|
+
* The user-side `amplify/backend.ts` becomes:
|
|
38
|
+
*
|
|
39
|
+
* import { defineAmplessBackend } from '@ampless/backend'
|
|
40
|
+
* import { auth } from './auth/resource'
|
|
41
|
+
* import { data } from './data/resource'
|
|
42
|
+
* // ...
|
|
43
|
+
* export default defineAmplessBackend({ auth, data, ... })
|
|
44
|
+
*/
|
|
45
|
+
declare function defineAmplessBackend(opts: DefineAmplessBackendOpts): AmplessBackend;
|
|
46
|
+
|
|
47
|
+
interface DefineAmplessAuthOpts {
|
|
48
|
+
/**
|
|
49
|
+
* The `post-confirmation` Cognito Lambda trigger that promotes the
|
|
50
|
+
* first signup to `ampless-admin`. Wire your own `defineFunction`
|
|
51
|
+
* pointing at the thin-shell handler that re-exports from
|
|
52
|
+
* `@ampless/backend/auth/post-confirmation`.
|
|
53
|
+
*
|
|
54
|
+
* Typed as `unknown` because Amplify's `defineFunction` return type
|
|
55
|
+
* carries internal pnpm paths that don't survive declaration emit —
|
|
56
|
+
* caller-side `defineFunction({ entry: './handler.ts' })` flows
|
|
57
|
+
* through without losing functionality.
|
|
58
|
+
*/
|
|
59
|
+
postConfirmation?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Provision the ampless Cognito User Pool / Identity Pool with the
|
|
63
|
+
* three role groups and the optional post-confirmation trigger.
|
|
64
|
+
*
|
|
65
|
+
* Public reads use AppSync's API key (not the guest Identity Pool
|
|
66
|
+
* role) because `a.handler.custom` doesn't accept `allow.guest()`
|
|
67
|
+
* in Amplify Gen 2 — see `data/resource.ts` and the RUNBOOK.
|
|
68
|
+
*/
|
|
69
|
+
declare function defineAmplessAuth(opts?: DefineAmplessAuthOpts): any;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Provision the ampless S3 bucket with the standard access map:
|
|
73
|
+
* - `public/media/*` — guest read, admin/editor full
|
|
74
|
+
* - `public/plugins/*` — guest read, admin full
|
|
75
|
+
*
|
|
76
|
+
* Bucket-level overrides (PublicAccessBlock, CORS, IAM policies for
|
|
77
|
+
* `public/site-settings/*`) are applied by `defineAmplessBackend`
|
|
78
|
+
* after `defineBackend` runs.
|
|
79
|
+
*
|
|
80
|
+
* Return type is `unknown` because Amplify's storage construct type
|
|
81
|
+
* carries internal pnpm paths that don't survive declaration emit —
|
|
82
|
+
* the resulting `storage` resource flows into `defineAmplessBackend`
|
|
83
|
+
* unchanged.
|
|
84
|
+
*/
|
|
85
|
+
declare function defineAmplessStorage(): any;
|
|
86
|
+
|
|
87
|
+
interface AmplessResolverPaths {
|
|
88
|
+
listPublishedPosts: string;
|
|
89
|
+
getPublishedPost: string;
|
|
90
|
+
listPostsByTag: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Default AppSync JS resolver paths. AppSync resolves these strings at
|
|
94
|
+
* CDK synth time relative to the file that calls `defineData` — i.e.
|
|
95
|
+
* the user's `amplify/data/resource.ts`. Templates ship the three
|
|
96
|
+
* resolver files at exactly these paths.
|
|
97
|
+
*
|
|
98
|
+
* Users with a non-default layout (e.g. moving the resolvers under a
|
|
99
|
+
* `resolvers/` subdir) pass overrides through `amplessSchemaModels(a, {
|
|
100
|
+
* resolverPaths: { ... } })`.
|
|
101
|
+
*/
|
|
102
|
+
declare const DEFAULT_RESOLVER_PATHS: AmplessResolverPaths;
|
|
103
|
+
interface AmplessSchemaModelsOpts {
|
|
104
|
+
/**
|
|
105
|
+
* Override the relative paths used for AppSync JS resolver entries.
|
|
106
|
+
* Paths are interpreted by Amplify at synth time relative to the
|
|
107
|
+
* file that calls `defineData`, NOT relative to this package — so
|
|
108
|
+
* they must point to actual `.js` files inside the user's project.
|
|
109
|
+
*/
|
|
110
|
+
resolverPaths?: Partial<AmplessResolverPaths>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The Ampless model + custom query definitions, returned as a plain
|
|
114
|
+
* object suitable for spreading into `a.schema({ ... })`.
|
|
115
|
+
*
|
|
116
|
+
* const schema = a.schema({
|
|
117
|
+
* ...amplessSchemaModels(a),
|
|
118
|
+
* MyCustomModel: a.model({ ... }).authorization(...),
|
|
119
|
+
* })
|
|
120
|
+
*
|
|
121
|
+
* Returning a builder-shape rather than a fully-instantiated `schema`
|
|
122
|
+
* lets the user weave custom models alongside ampless's defaults
|
|
123
|
+
* inside one `a.schema(...)` call, which is the only way Amplify Gen
|
|
124
|
+
* 2's `ClientSchema<typeof schema>` inference picks up the user's
|
|
125
|
+
* extra models.
|
|
126
|
+
*/
|
|
127
|
+
declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
|
|
128
|
+
Post: any;
|
|
129
|
+
Page: any;
|
|
130
|
+
Media: any;
|
|
131
|
+
Taxonomy: any;
|
|
132
|
+
PostTag: any;
|
|
133
|
+
KvStore: any;
|
|
134
|
+
PublicPost: any;
|
|
135
|
+
PublicPostConnection: any;
|
|
136
|
+
listPublishedPosts: any;
|
|
137
|
+
getPublishedPost: any;
|
|
138
|
+
listPostsByTag: any;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Convenience helper: build a fully-instantiated `a.schema(...)`
|
|
142
|
+
* containing ampless's models plus any caller-supplied custom models.
|
|
143
|
+
* Pick whichever pattern fits — direct spread is more transparent
|
|
144
|
+
* when reading `amplify/data/resource.ts`, this helper is shorter
|
|
145
|
+
* when there are no custom models.
|
|
146
|
+
*/
|
|
147
|
+
declare function extendAmplessSchema(a: any, custom?: Record<string, any>, opts?: AmplessSchemaModelsOpts): any;
|
|
148
|
+
/**
|
|
149
|
+
* Standard authorization modes for ampless. `userPool` is the default
|
|
150
|
+
* (admin/editor access); the API key serves the public read endpoints
|
|
151
|
+
* because `a.handler.custom` doesn't support `allow.guest()` in
|
|
152
|
+
* Amplify Gen 2 (verified 2026-04). The key has a 365-day TTL; the
|
|
153
|
+
* `@ampless/backend/functions/api-key-renewer` Lambda rotates it
|
|
154
|
+
* monthly so the public site never silently 401s.
|
|
155
|
+
*/
|
|
156
|
+
declare const defaultAuthorizationModes: Parameters<typeof defineData>[0]['authorizationModes'];
|
|
157
|
+
|
|
158
|
+
export { type AmplessBackend, type AmplessResolverPaths, type AmplessSchemaModelsOpts, DEFAULT_RESOLVER_PATHS, type DefineAmplessAuthOpts, type DefineAmplessBackendOpts, amplessSchemaModels, defaultAuthorizationModes, defineAmplessAuth, defineAmplessBackend, defineAmplessStorage, extendAmplessSchema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// src/backend.ts
|
|
2
|
+
import { defineBackend } from "@aws-amplify/backend";
|
|
3
|
+
import { Effect, PolicyStatement, AnyPrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
import { Duration, Stack } from "aws-cdk-lib";
|
|
5
|
+
import { Queue } from "aws-cdk-lib/aws-sqs";
|
|
6
|
+
import { StartingPosition } from "aws-cdk-lib/aws-lambda";
|
|
7
|
+
import { DynamoEventSource, SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
|
|
8
|
+
import { Rule, Schedule } from "aws-cdk-lib/aws-events";
|
|
9
|
+
import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
|
|
10
|
+
function defineAmplessBackend(opts) {
|
|
11
|
+
const backend = defineBackend({
|
|
12
|
+
auth: opts.auth,
|
|
13
|
+
data: opts.data,
|
|
14
|
+
storage: opts.storage,
|
|
15
|
+
postConfirmation: opts.postConfirmation,
|
|
16
|
+
eventDispatcher: opts.eventDispatcher,
|
|
17
|
+
processorTrusted: opts.processorTrusted,
|
|
18
|
+
processorUntrusted: opts.processorUntrusted,
|
|
19
|
+
apiKeyRenewer: opts.apiKeyRenewer
|
|
20
|
+
});
|
|
21
|
+
const cfnBucket = backend.storage.resources.cfnResources.cfnBucket;
|
|
22
|
+
cfnBucket.publicAccessBlockConfiguration = {
|
|
23
|
+
blockPublicAcls: true,
|
|
24
|
+
blockPublicPolicy: false,
|
|
25
|
+
ignorePublicAcls: true,
|
|
26
|
+
restrictPublicBuckets: false
|
|
27
|
+
};
|
|
28
|
+
cfnBucket.corsConfiguration = {
|
|
29
|
+
corsRules: [
|
|
30
|
+
{
|
|
31
|
+
allowedMethods: ["GET", "HEAD"],
|
|
32
|
+
allowedOrigins: ["*"],
|
|
33
|
+
allowedHeaders: ["*"],
|
|
34
|
+
maxAge: 3e3
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
backend.storage.resources.bucket.addToResourcePolicy(
|
|
39
|
+
new PolicyStatement({
|
|
40
|
+
effect: Effect.ALLOW,
|
|
41
|
+
principals: [new AnyPrincipal()],
|
|
42
|
+
actions: ["s3:GetObject"],
|
|
43
|
+
resources: [`${backend.storage.resources.bucket.bucketArn}/public/*`]
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
const cfnUserPool = backend.auth.resources.cfnResources.cfnUserPool;
|
|
47
|
+
cfnUserPool.policies = {
|
|
48
|
+
passwordPolicy: {
|
|
49
|
+
minimumLength: 8,
|
|
50
|
+
requireLowercase: false,
|
|
51
|
+
requireUppercase: false,
|
|
52
|
+
requireNumbers: false,
|
|
53
|
+
requireSymbols: false,
|
|
54
|
+
temporaryPasswordValidityDays: 7
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
backend.postConfirmation.resources.lambda.addToRolePolicy(
|
|
58
|
+
new PolicyStatement({
|
|
59
|
+
effect: Effect.ALLOW,
|
|
60
|
+
actions: ["cognito-idp:AdminAddUserToGroup", "cognito-idp:ListUsersInGroup"],
|
|
61
|
+
resources: ["arn:aws:cognito-idp:*:*:userpool/*"]
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
const postTable = backend.data.resources.tables["Post"];
|
|
65
|
+
const cfnPostTable = backend.data.resources.cfnResources.amplifyDynamoDbTables["Post"];
|
|
66
|
+
cfnPostTable.streamSpecification = { streamViewType: "NEW_AND_OLD_IMAGES" };
|
|
67
|
+
const kvTable = backend.data.resources.tables["KvStore"];
|
|
68
|
+
const cfnKvTable = backend.data.resources.cfnResources.amplifyDynamoDbTables["KvStore"];
|
|
69
|
+
cfnKvTable.streamSpecification = { streamViewType: "NEW_AND_OLD_IMAGES" };
|
|
70
|
+
cfnKvTable.timeToLiveSpecification = { attributeName: "ttl", enabled: true };
|
|
71
|
+
const eventsStack = backend.createStack("amplessEvents");
|
|
72
|
+
const eventsDlq = new Queue(eventsStack, "EventsDlq", {
|
|
73
|
+
retentionPeriod: Duration.days(14)
|
|
74
|
+
});
|
|
75
|
+
const trustedQueue = new Queue(eventsStack, "TrustedEventsQueue", {
|
|
76
|
+
visibilityTimeout: Duration.seconds(120),
|
|
77
|
+
deadLetterQueue: { maxReceiveCount: 3, queue: eventsDlq }
|
|
78
|
+
});
|
|
79
|
+
const untrustedQueue = new Queue(eventsStack, "UntrustedEventsQueue", {
|
|
80
|
+
visibilityTimeout: Duration.seconds(60),
|
|
81
|
+
deadLetterQueue: { maxReceiveCount: 3, queue: eventsDlq }
|
|
82
|
+
});
|
|
83
|
+
const dispatcherFn = backend.eventDispatcher.resources.lambda;
|
|
84
|
+
dispatcherFn.addEventSource(
|
|
85
|
+
new DynamoEventSource(postTable, {
|
|
86
|
+
startingPosition: StartingPosition.LATEST,
|
|
87
|
+
batchSize: 10,
|
|
88
|
+
retryAttempts: 3,
|
|
89
|
+
bisectBatchOnError: true
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
dispatcherFn.addEventSource(
|
|
93
|
+
new DynamoEventSource(kvTable, {
|
|
94
|
+
startingPosition: StartingPosition.LATEST,
|
|
95
|
+
batchSize: 10,
|
|
96
|
+
retryAttempts: 3,
|
|
97
|
+
bisectBatchOnError: true
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
trustedQueue.grantSendMessages(dispatcherFn);
|
|
101
|
+
untrustedQueue.grantSendMessages(dispatcherFn);
|
|
102
|
+
dispatcherFn.addEnvironment("TRUSTED_QUEUE_URL", trustedQueue.queueUrl);
|
|
103
|
+
dispatcherFn.addEnvironment("UNTRUSTED_QUEUE_URL", untrustedQueue.queueUrl);
|
|
104
|
+
const trustedFn = backend.processorTrusted.resources.lambda;
|
|
105
|
+
trustedFn.addEventSource(new SqsEventSource(trustedQueue, { batchSize: 5 }));
|
|
106
|
+
postTable.grantReadData(trustedFn);
|
|
107
|
+
trustedFn.addToRolePolicy(
|
|
108
|
+
new PolicyStatement({
|
|
109
|
+
effect: Effect.ALLOW,
|
|
110
|
+
actions: ["dynamodb:Query", "dynamodb:Scan"],
|
|
111
|
+
resources: [`${postTable.tableArn}/index/*`]
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
kvTable.grantReadData(trustedFn);
|
|
115
|
+
trustedFn.addToRolePolicy(
|
|
116
|
+
new PolicyStatement({
|
|
117
|
+
effect: Effect.ALLOW,
|
|
118
|
+
actions: ["s3:PutObject", "s3:DeleteObject"],
|
|
119
|
+
resources: [
|
|
120
|
+
`${backend.storage.resources.bucket.bucketArn}/public/plugins/*`,
|
|
121
|
+
// Built-in cache: rebuildSiteSettingsCache writes the per-site
|
|
122
|
+
// JSON the public site reads. Without this entry the
|
|
123
|
+
// `site.settings.updated` handler S3 PutObject call fails
|
|
124
|
+
// silently (CloudWatch shows AccessDenied; the public site never
|
|
125
|
+
// sees theme.active overrides because the cache file never
|
|
126
|
+
// appears).
|
|
127
|
+
`${backend.storage.resources.bucket.bucketArn}/public/site-settings/*`
|
|
128
|
+
]
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
trustedFn.addEnvironment("AMPLESS_BUCKET_NAME", backend.storage.resources.bucket.bucketName);
|
|
132
|
+
trustedFn.addEnvironment("AMPLESS_POST_TABLE", postTable.tableName);
|
|
133
|
+
trustedFn.addEnvironment("AMPLESS_KV_TABLE", kvTable.tableName);
|
|
134
|
+
const untrustedFn = backend.processorUntrusted.resources.lambda;
|
|
135
|
+
untrustedFn.addEventSource(new SqsEventSource(untrustedQueue, { batchSize: 5 }));
|
|
136
|
+
const graphqlApi = backend.data.resources.cfnResources.cfnGraphqlApi;
|
|
137
|
+
const apiKeyRenewerFn = backend.apiKeyRenewer.resources.lambda;
|
|
138
|
+
apiKeyRenewerFn.addEnvironment("APPSYNC_API_ID", graphqlApi.attrApiId);
|
|
139
|
+
apiKeyRenewerFn.addToRolePolicy(
|
|
140
|
+
new PolicyStatement({
|
|
141
|
+
effect: Effect.ALLOW,
|
|
142
|
+
actions: ["appsync:ListApiKeys", "appsync:UpdateApiKey"],
|
|
143
|
+
// attrArn is `arn:aws:appsync:{region}:{account}:apis/{apiId}`; the API
|
|
144
|
+
// key resources live underneath, so a wildcard suffix covers them.
|
|
145
|
+
resources: [graphqlApi.attrArn, `${graphqlApi.attrArn}/*`]
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
new Rule(Stack.of(apiKeyRenewerFn), "ApiKeyRenewerSchedule", {
|
|
149
|
+
schedule: Schedule.cron({ minute: "0", hour: "3", day: "1", month: "*", year: "*" }),
|
|
150
|
+
targets: [new LambdaFunction(apiKeyRenewerFn)]
|
|
151
|
+
});
|
|
152
|
+
return backend;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/auth/index.ts
|
|
156
|
+
import { defineAuth } from "@aws-amplify/backend";
|
|
157
|
+
function defineAmplessAuth(opts = {}) {
|
|
158
|
+
return defineAuth({
|
|
159
|
+
loginWith: {
|
|
160
|
+
email: true
|
|
161
|
+
},
|
|
162
|
+
groups: ["ampless-admin", "ampless-editor", "ampless-reader"],
|
|
163
|
+
triggers: opts.postConfirmation ? (
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
{ postConfirmation: opts.postConfirmation }
|
|
166
|
+
) : void 0
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/storage/index.ts
|
|
171
|
+
import { defineStorage } from "@aws-amplify/backend";
|
|
172
|
+
function defineAmplessStorage() {
|
|
173
|
+
return defineStorage({
|
|
174
|
+
name: "amplessMedia",
|
|
175
|
+
access: (allow) => ({
|
|
176
|
+
"public/media/*": [
|
|
177
|
+
allow.guest.to(["read"]),
|
|
178
|
+
allow.groups(["ampless-admin", "ampless-editor"]).to(["read", "write", "delete"])
|
|
179
|
+
],
|
|
180
|
+
"public/plugins/*": [
|
|
181
|
+
allow.guest.to(["read"]),
|
|
182
|
+
allow.groups(["ampless-admin"]).to(["read", "write", "delete"])
|
|
183
|
+
]
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/data/index.ts
|
|
189
|
+
var DEFAULT_RESOLVER_PATHS = {
|
|
190
|
+
listPublishedPosts: "./list-published-posts.js",
|
|
191
|
+
getPublishedPost: "./get-published-post.js",
|
|
192
|
+
listPostsByTag: "./list-posts-by-tag.js"
|
|
193
|
+
};
|
|
194
|
+
function amplessSchemaModels(a, opts = {}) {
|
|
195
|
+
const resolverPaths = {
|
|
196
|
+
...DEFAULT_RESOLVER_PATHS,
|
|
197
|
+
...opts.resolverPaths
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
Post: a.model({
|
|
201
|
+
siteId: a.string().required(),
|
|
202
|
+
postId: a.id().required(),
|
|
203
|
+
slug: a.string().required(),
|
|
204
|
+
title: a.string().required(),
|
|
205
|
+
excerpt: a.string(),
|
|
206
|
+
format: a.enum(["tiptap", "markdown", "html"]),
|
|
207
|
+
body: a.json(),
|
|
208
|
+
status: a.enum(["draft", "published"]),
|
|
209
|
+
publishedAt: a.datetime(),
|
|
210
|
+
tags: a.string().array(),
|
|
211
|
+
// Denormalized GSI keys — set by every write path (admin client,
|
|
212
|
+
// MCP tools). Same pattern as siteIdStatus: composing the
|
|
213
|
+
// partition key as a single string lets each public-read query
|
|
214
|
+
// hit DynamoDB with a pure PK Query, no filter pass.
|
|
215
|
+
// siteIdStatus = `${siteId}#${status}`
|
|
216
|
+
// → bySiteIdStatus partitions per site×status, sorted by publishedAt
|
|
217
|
+
// siteIdSlug = `${siteId}#${slug}`
|
|
218
|
+
// → bySiteIdSlug locates a single post by slug in O(1)
|
|
219
|
+
siteIdStatus: a.string(),
|
|
220
|
+
siteIdSlug: a.string()
|
|
221
|
+
}).identifier(["siteId", "postId"]).secondaryIndexes((index) => [
|
|
222
|
+
index("siteIdStatus").sortKeys(["publishedAt"]).name("bySiteIdStatus"),
|
|
223
|
+
index("siteIdSlug").name("bySiteIdSlug")
|
|
224
|
+
]).authorization((allow) => [
|
|
225
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
226
|
+
]),
|
|
227
|
+
Page: a.model({
|
|
228
|
+
siteId: a.string().required(),
|
|
229
|
+
pageId: a.id().required(),
|
|
230
|
+
slug: a.string().required(),
|
|
231
|
+
title: a.string().required(),
|
|
232
|
+
format: a.enum(["tiptap", "markdown", "html"]),
|
|
233
|
+
body: a.json(),
|
|
234
|
+
status: a.enum(["draft", "published"]),
|
|
235
|
+
publishedAt: a.datetime()
|
|
236
|
+
}).identifier(["siteId", "pageId"]).authorization((allow) => [
|
|
237
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
238
|
+
]),
|
|
239
|
+
Media: a.model({
|
|
240
|
+
siteId: a.string().required(),
|
|
241
|
+
mediaId: a.id().required(),
|
|
242
|
+
src: a.string().required(),
|
|
243
|
+
mimeType: a.string().required(),
|
|
244
|
+
size: a.integer(),
|
|
245
|
+
delivery: a.string()
|
|
246
|
+
}).identifier(["siteId", "mediaId"]).authorization((allow) => [
|
|
247
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
248
|
+
]),
|
|
249
|
+
Taxonomy: a.model({
|
|
250
|
+
siteId: a.string().required(),
|
|
251
|
+
termId: a.id().required(),
|
|
252
|
+
type: a.enum(["category", "tag"]),
|
|
253
|
+
name: a.string().required(),
|
|
254
|
+
slug: a.string().required()
|
|
255
|
+
}).identifier(["siteId", "termId"]).authorization((allow) => [
|
|
256
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
257
|
+
]),
|
|
258
|
+
// Denormalized index for "posts by tag" queries. Each Post tag becomes
|
|
259
|
+
// one PostTag row, so listing/filtering by tag is a single Query —
|
|
260
|
+
// including date-range archive views, since publishedAt is part of the
|
|
261
|
+
// sort key. Maintained by the admin client whenever a post is created,
|
|
262
|
+
// updated, or deleted; never edited by end users.
|
|
263
|
+
//
|
|
264
|
+
// PK: siteIdTag e.g. "default#tech"
|
|
265
|
+
// SK: publishedAtPostId e.g. "2026-04-27T13:57:05.679Z#post-001"
|
|
266
|
+
PostTag: a.model({
|
|
267
|
+
siteIdTag: a.string().required(),
|
|
268
|
+
publishedAtPostId: a.string().required(),
|
|
269
|
+
siteId: a.string().required(),
|
|
270
|
+
tag: a.string().required(),
|
|
271
|
+
postId: a.id().required(),
|
|
272
|
+
publishedAt: a.datetime().required(),
|
|
273
|
+
slug: a.string().required(),
|
|
274
|
+
title: a.string().required(),
|
|
275
|
+
excerpt: a.string(),
|
|
276
|
+
// Full tag list of the post (for chip rendering on tag pages).
|
|
277
|
+
tags: a.string().array()
|
|
278
|
+
}).identifier(["siteIdTag", "publishedAtPostId"]).authorization((allow) => [
|
|
279
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
280
|
+
]),
|
|
281
|
+
// Generic key/value store. Two roles in one table:
|
|
282
|
+
// - Site settings: PK = `siteconfig:{siteId}`, SK = dotted key
|
|
283
|
+
// (`site.name`, `media.imageDisplay`, ...). No TTL → persistent.
|
|
284
|
+
// - Caches / plugin state: PK = whatever namespace the caller picks
|
|
285
|
+
// (`cache:{ns}`, `pluginstate:{name}:...`). TTL set → DynamoDB
|
|
286
|
+
// auto-deletes within ~48h of expiry.
|
|
287
|
+
//
|
|
288
|
+
// Auth is admin/editor write only. Guests never read this table
|
|
289
|
+
// directly — site settings are mirrored to S3 by the site-settings-
|
|
290
|
+
// cache built-in plugin and the public site fetches from there.
|
|
291
|
+
KvStore: a.model({
|
|
292
|
+
pk: a.string().required(),
|
|
293
|
+
sk: a.string().required(),
|
|
294
|
+
value: a.json(),
|
|
295
|
+
// Unix epoch seconds. When set, DynamoDB removes the row
|
|
296
|
+
// automatically (TimeToLive enabled in backend.ts).
|
|
297
|
+
ttl: a.integer()
|
|
298
|
+
}).identifier(["pk", "sk"]).authorization((allow) => [
|
|
299
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
300
|
+
]),
|
|
301
|
+
// Custom return type for public post reads. Decoupling from `Post` lets
|
|
302
|
+
// AppSync skip the model-level (admin-only) auth check on fields.
|
|
303
|
+
PublicPost: a.customType({
|
|
304
|
+
siteId: a.string().required(),
|
|
305
|
+
postId: a.id().required(),
|
|
306
|
+
slug: a.string().required(),
|
|
307
|
+
title: a.string().required(),
|
|
308
|
+
excerpt: a.string(),
|
|
309
|
+
format: a.string(),
|
|
310
|
+
body: a.json(),
|
|
311
|
+
status: a.string(),
|
|
312
|
+
publishedAt: a.datetime(),
|
|
313
|
+
tags: a.string().array()
|
|
314
|
+
}),
|
|
315
|
+
// Paginated wrapper for list responses.
|
|
316
|
+
PublicPostConnection: a.customType({
|
|
317
|
+
items: a.ref("PublicPost").array(),
|
|
318
|
+
nextToken: a.string()
|
|
319
|
+
}),
|
|
320
|
+
// Public read endpoints — guard against draft leakage via custom resolvers.
|
|
321
|
+
// Custom handlers (`a.handler.custom`) only support apiKey / userPool /
|
|
322
|
+
// lambda / group / owner auth. `allow.guest()` (Cognito Identity Pool
|
|
323
|
+
// unauthenticated role) is NOT supported with custom handlers as of
|
|
324
|
+
// Amplify Gen 2 (2026-04). So anonymous visitors get a public API key,
|
|
325
|
+
// which has a 365-day TTL — see RUNBOOK.md for the rotation procedure.
|
|
326
|
+
// The resolvers themselves strip drafts at the data-source level, so the
|
|
327
|
+
// only thing guests can see is `status === 'published'` rows projected
|
|
328
|
+
// onto `PublicPost`.
|
|
329
|
+
listPublishedPosts: a.query().arguments({
|
|
330
|
+
siteId: a.string(),
|
|
331
|
+
// Both ISO 8601 strings; query SK condition is pushed into DynamoDB
|
|
332
|
+
// so only the matching publishedAt range is read.
|
|
333
|
+
from: a.datetime(),
|
|
334
|
+
to: a.datetime(),
|
|
335
|
+
limit: a.integer(),
|
|
336
|
+
// Opaque DynamoDB pagination cursor. Pass back the previous response's
|
|
337
|
+
// `nextToken` to fetch the next page.
|
|
338
|
+
nextToken: a.string()
|
|
339
|
+
}).returns(a.ref("PublicPostConnection")).handler(
|
|
340
|
+
a.handler.custom({
|
|
341
|
+
dataSource: a.ref("Post"),
|
|
342
|
+
entry: resolverPaths.listPublishedPosts
|
|
343
|
+
})
|
|
344
|
+
).authorization((allow) => [
|
|
345
|
+
allow.publicApiKey(),
|
|
346
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
347
|
+
]),
|
|
348
|
+
getPublishedPost: a.query().arguments({ siteId: a.string(), slug: a.string().required() }).returns(a.ref("PublicPost")).handler(
|
|
349
|
+
a.handler.custom({
|
|
350
|
+
dataSource: a.ref("Post"),
|
|
351
|
+
entry: resolverPaths.getPublishedPost
|
|
352
|
+
})
|
|
353
|
+
).authorization((allow) => [
|
|
354
|
+
allow.publicApiKey(),
|
|
355
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
356
|
+
]),
|
|
357
|
+
// Tag pages: newest-first only. Archive (date range) within a tag is not
|
|
358
|
+
// a common UX pattern; if it's ever needed, add `from`/`to` args here.
|
|
359
|
+
listPostsByTag: a.query().arguments({
|
|
360
|
+
siteId: a.string(),
|
|
361
|
+
tag: a.string().required(),
|
|
362
|
+
limit: a.integer(),
|
|
363
|
+
nextToken: a.string()
|
|
364
|
+
}).returns(a.ref("PublicPostConnection")).handler(
|
|
365
|
+
a.handler.custom({
|
|
366
|
+
dataSource: a.ref("PostTag"),
|
|
367
|
+
entry: resolverPaths.listPostsByTag
|
|
368
|
+
})
|
|
369
|
+
).authorization((allow) => [
|
|
370
|
+
allow.publicApiKey(),
|
|
371
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
372
|
+
])
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function extendAmplessSchema(a, custom, opts) {
|
|
376
|
+
return a.schema({
|
|
377
|
+
...amplessSchemaModels(a, opts),
|
|
378
|
+
...custom ?? {}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
var defaultAuthorizationModes = {
|
|
382
|
+
defaultAuthorizationMode: "userPool",
|
|
383
|
+
apiKeyAuthorizationMode: { expiresInDays: 365 }
|
|
384
|
+
};
|
|
385
|
+
export {
|
|
386
|
+
DEFAULT_RESOLVER_PATHS,
|
|
387
|
+
amplessSchemaModels,
|
|
388
|
+
defaultAuthorizationModes,
|
|
389
|
+
defineAmplessAuth,
|
|
390
|
+
defineAmplessBackend,
|
|
391
|
+
defineAmplessStorage,
|
|
392
|
+
extendAmplessSchema
|
|
393
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ampless/backend",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./auth/post-confirmation": {
|
|
13
|
+
"import": "./dist/auth/post-confirmation.js",
|
|
14
|
+
"types": "./dist/auth/post-confirmation.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./events/dispatcher": {
|
|
17
|
+
"import": "./dist/events/dispatcher.js",
|
|
18
|
+
"types": "./dist/events/dispatcher.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./events/processor-trusted": {
|
|
21
|
+
"import": "./dist/events/processor-trusted.js",
|
|
22
|
+
"types": "./dist/events/processor-trusted.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./events/processor-untrusted": {
|
|
25
|
+
"import": "./dist/events/processor-untrusted.js",
|
|
26
|
+
"types": "./dist/events/processor-untrusted.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./functions/api-key-renewer": {
|
|
29
|
+
"import": "./dist/functions/api-key-renewer.js",
|
|
30
|
+
"types": "./dist/functions/api-key-renewer.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/heavymoons/ampless.git",
|
|
43
|
+
"directory": "packages/backend"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/heavymoons/ampless/tree/main/packages/backend#readme",
|
|
46
|
+
"bugs": "https://github.com/heavymoons/ampless/issues",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@aws-sdk/client-appsync": "^3.717.0",
|
|
49
|
+
"@aws-sdk/client-cognito-identity-provider": "^3.717.0",
|
|
50
|
+
"@aws-sdk/client-dynamodb": "^3.717.0",
|
|
51
|
+
"@aws-sdk/client-s3": "^3.717.0",
|
|
52
|
+
"@aws-sdk/client-sqs": "^3.717.0",
|
|
53
|
+
"@aws-sdk/lib-dynamodb": "^3.717.0",
|
|
54
|
+
"@aws-sdk/util-dynamodb": "^3.717.0",
|
|
55
|
+
"ampless": "0.2.0-alpha.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@aws-amplify/backend": "^1",
|
|
59
|
+
"aws-cdk-lib": "^2"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@aws-amplify/backend": "^1.13.0",
|
|
63
|
+
"@types/aws-lambda": "^8.10.147",
|
|
64
|
+
"aws-cdk-lib": "^2.174.0"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"ampless",
|
|
68
|
+
"backend",
|
|
69
|
+
"amplify",
|
|
70
|
+
"cms"
|
|
71
|
+
],
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsup",
|
|
74
|
+
"dev": "tsup --watch",
|
|
75
|
+
"lint": "tsc --noEmit",
|
|
76
|
+
"test": "vitest run --passWithNoTests"
|
|
77
|
+
}
|
|
78
|
+
}
|