@ampless/backend 1.0.0-beta.80 → 1.0.0-beta.81
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/user-admin.d.ts +6 -1
- package/dist/auth/user-admin.js +7 -0
- package/dist/events/dispatcher.js +11 -1
- package/dist/events/processor-trusted.js +6 -1
- package/dist/functions/api-key-renewer.js +24 -19
- package/dist/functions/mcp-handler.js +47 -15
- package/dist/index.js +4 -0
- package/package.json +3 -3
|
@@ -39,7 +39,12 @@ interface UserAdminEvent {
|
|
|
39
39
|
typeName: string;
|
|
40
40
|
fieldName: string;
|
|
41
41
|
arguments: Partial<SetArgs>;
|
|
42
|
-
identity?:
|
|
42
|
+
identity?: {
|
|
43
|
+
sub?: string;
|
|
44
|
+
username?: string;
|
|
45
|
+
groups?: string[];
|
|
46
|
+
[k: string]: unknown;
|
|
47
|
+
};
|
|
43
48
|
source?: unknown;
|
|
44
49
|
request?: unknown;
|
|
45
50
|
prev?: unknown;
|
package/dist/auth/user-admin.js
CHANGED
|
@@ -74,10 +74,17 @@ async function setAdminUserRole(userPoolId, userId, role) {
|
|
|
74
74
|
const finalRole = await roleOf(userPoolId, userId);
|
|
75
75
|
return { userId, email, role: finalRole };
|
|
76
76
|
}
|
|
77
|
+
function isAdmin(identity) {
|
|
78
|
+
return (identity?.groups ?? []).includes(ADMIN_GROUP);
|
|
79
|
+
}
|
|
77
80
|
var handler = async (event) => {
|
|
78
81
|
const userPoolId = requireUserPoolId();
|
|
79
82
|
const field = event.fieldName;
|
|
80
83
|
try {
|
|
84
|
+
if (!isAdmin(event.identity)) {
|
|
85
|
+
console.error("[user-admin] caller is not in the ampless-admin group");
|
|
86
|
+
throw new Error("Unauthorized: ampless-admin group required");
|
|
87
|
+
}
|
|
81
88
|
if (field === "listAdminUsers") {
|
|
82
89
|
return await listAdminUsers(userPoolId);
|
|
83
90
|
}
|
|
@@ -32,6 +32,8 @@ function projectPost(raw) {
|
|
|
32
32
|
slug: raw.slug,
|
|
33
33
|
title: raw.title,
|
|
34
34
|
status: raw.status ?? "draft",
|
|
35
|
+
format: raw.format,
|
|
36
|
+
excerpt: raw.excerpt,
|
|
35
37
|
publishedAt: raw.publishedAt,
|
|
36
38
|
tags: raw.tags
|
|
37
39
|
};
|
|
@@ -86,7 +88,15 @@ function emitKvEvents(record, timestamp) {
|
|
|
86
88
|
async function sendBatch(queueUrl, entries) {
|
|
87
89
|
for (let i = 0; i < entries.length; i += 10) {
|
|
88
90
|
const chunk = entries.slice(i, i + 10);
|
|
89
|
-
await sqs.send(
|
|
91
|
+
const res = await sqs.send(
|
|
92
|
+
new SendMessageBatchCommand({ QueueUrl: queueUrl, Entries: chunk })
|
|
93
|
+
);
|
|
94
|
+
if (res.Failed && res.Failed.length > 0) {
|
|
95
|
+
console.error("[event-dispatcher] SQS SendMessageBatch partial failure:", res.Failed);
|
|
96
|
+
throw new Error(
|
|
97
|
+
`SendMessageBatch: ${res.Failed.length}/${chunk.length} message(s) failed for ${queueUrl}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
function revisedAtForRecord(record, post) {
|
|
@@ -35,7 +35,12 @@ function postTagItemsFromPost(p) {
|
|
|
35
35
|
publishedAt: p.publishedAt,
|
|
36
36
|
slug: p.slug,
|
|
37
37
|
title: p.title,
|
|
38
|
-
tags
|
|
38
|
+
tags,
|
|
39
|
+
// Omit when absent — PostTag rows are written via a direct DynamoDB put
|
|
40
|
+
// with no `removeUndefinedValues`, so an `undefined` attribute would
|
|
41
|
+
// throw "Unsupported type passed: undefined".
|
|
42
|
+
...p.format !== void 0 && { format: p.format },
|
|
43
|
+
...p.excerpt !== void 0 && { excerpt: p.excerpt }
|
|
39
44
|
}));
|
|
40
45
|
}
|
|
41
46
|
function itemKey(item) {
|
|
@@ -13,26 +13,31 @@ function requireEnv(name) {
|
|
|
13
13
|
var APPSYNC_API_ID = requireEnv("APPSYNC_API_ID");
|
|
14
14
|
var TTL_DAYS = 364;
|
|
15
15
|
var handler = async () => {
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
})
|
|
16
|
+
try {
|
|
17
|
+
const { apiKeys } = await client.send(
|
|
18
|
+
new ListApiKeysCommand({ apiId: APPSYNC_API_ID })
|
|
34
19
|
);
|
|
35
|
-
|
|
20
|
+
if (!apiKeys || apiKeys.length === 0) {
|
|
21
|
+
console.warn("[api-key-renewer] no api keys to renew");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const expires = Math.floor(Date.now() / 1e3) + TTL_DAYS * 86400;
|
|
25
|
+
const targetIso = new Date(expires * 1e3).toISOString();
|
|
26
|
+
for (const k of apiKeys) {
|
|
27
|
+
if (!k.id) continue;
|
|
28
|
+
const beforeIso = k.expires ? new Date(k.expires * 1e3).toISOString() : "(none)";
|
|
29
|
+
await client.send(
|
|
30
|
+
new UpdateApiKeyCommand({
|
|
31
|
+
apiId: APPSYNC_API_ID,
|
|
32
|
+
id: k.id,
|
|
33
|
+
expires
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
console.log(`[api-key-renewer] ${k.id}: ${beforeIso} -> ${targetIso}`);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error("[api-key-renewer] failed to renew API keys:", err);
|
|
40
|
+
throw err;
|
|
36
41
|
}
|
|
37
42
|
};
|
|
38
43
|
export {
|
|
@@ -462,7 +462,23 @@ var uploadMediaSchema = {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
};
|
|
465
|
+
var MIME_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$/i;
|
|
466
|
+
var BLOCKED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
467
|
+
"text/html",
|
|
468
|
+
"application/xhtml+xml",
|
|
469
|
+
"application/javascript",
|
|
470
|
+
"text/javascript"
|
|
471
|
+
]);
|
|
472
|
+
function assertSafeMimeType(mimeType) {
|
|
473
|
+
if (!MIME_TYPE_RE.test(mimeType)) {
|
|
474
|
+
throw new Error(`upload_media: invalid mimeType: ${JSON.stringify(mimeType)}`);
|
|
475
|
+
}
|
|
476
|
+
if (BLOCKED_MIME_TYPES.has(mimeType.toLowerCase())) {
|
|
477
|
+
throw new Error(`upload_media: mimeType not allowed for public media: ${mimeType}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
465
480
|
async function uploadMedia(graphql, storage, args) {
|
|
481
|
+
assertSafeMimeType(args.mimeType);
|
|
466
482
|
const body = Buffer.from(args.base64Data, "base64");
|
|
467
483
|
const key = buildMediaKey(args.filename);
|
|
468
484
|
const putResult = await storage.putObject(key, body, args.mimeType);
|
|
@@ -647,13 +663,16 @@ var GET_BY_ID2 = (
|
|
|
647
663
|
}
|
|
648
664
|
`
|
|
649
665
|
);
|
|
650
|
-
var
|
|
666
|
+
var FIND_BY_SRC = (
|
|
651
667
|
/* GraphQL */
|
|
652
668
|
`
|
|
653
|
-
query
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
669
|
+
query FindMediaBySrc($filter: ModelMediaFilterInput!, $nextToken: String) {
|
|
670
|
+
listMedia(filter: $filter, limit: 100, nextToken: $nextToken) {
|
|
671
|
+
items {
|
|
672
|
+
mediaId
|
|
673
|
+
src
|
|
674
|
+
}
|
|
675
|
+
nextToken
|
|
657
676
|
}
|
|
658
677
|
}
|
|
659
678
|
`
|
|
@@ -722,12 +741,18 @@ async function deleteMedia(graphql, storage, args) {
|
|
|
722
741
|
assertMediaPrefix(src);
|
|
723
742
|
}
|
|
724
743
|
} else {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
744
|
+
let cursor;
|
|
745
|
+
do {
|
|
746
|
+
const data = await graphql.query(FIND_BY_SRC, { filter: { src: { eq: args.src } }, nextToken: cursor });
|
|
747
|
+
const row = data.listMedia.items[0];
|
|
748
|
+
if (row) {
|
|
749
|
+
mediaId = row.mediaId;
|
|
750
|
+
src = row.src;
|
|
751
|
+
assertMediaPrefix(src);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
cursor = data.listMedia.nextToken ?? void 0;
|
|
755
|
+
} while (cursor);
|
|
731
756
|
}
|
|
732
757
|
if (!mediaId) {
|
|
733
758
|
if (args.src) {
|
|
@@ -782,7 +807,12 @@ function getSchema() {
|
|
|
782
807
|
slug: { type: "string", required: true, description: "URL slug, unique" },
|
|
783
808
|
title: { type: "string", required: true },
|
|
784
809
|
excerpt: { type: "string" },
|
|
785
|
-
format: {
|
|
810
|
+
format: {
|
|
811
|
+
type: "enum",
|
|
812
|
+
values: ["tiptap", "markdown", "html", "static"],
|
|
813
|
+
required: true,
|
|
814
|
+
description: "`static` is read-only here \u2014 get_post / list_posts can return it, but it is created/edited only via the static-bundle tools (upload_static_bundle, upload_static_file, delete_static_file, commit_static_post), NOT create_post / update_post. See notes.staticFormat."
|
|
815
|
+
},
|
|
786
816
|
body: {
|
|
787
817
|
type: "json",
|
|
788
818
|
description: "tiptap node JSON when format=tiptap; markdown source string when format=markdown; raw HTML string when format=html"
|
|
@@ -821,7 +851,7 @@ function getSchema() {
|
|
|
821
851
|
}
|
|
822
852
|
}
|
|
823
853
|
],
|
|
824
|
-
formats: ["tiptap", "markdown", "html"],
|
|
854
|
+
formats: ["tiptap", "markdown", "html", "static"],
|
|
825
855
|
notes: {
|
|
826
856
|
editorTrust: 'editor stores arbitrary HTML/JS verbatim \u2014 same trust shape as WordPress unfiltered_html capability. See docs/architecture/04-access-layer-mcp.md \xA7"editor \u306E\u4FE1\u983C\u30E2\u30C7\u30EB".',
|
|
827
857
|
tiptapBody: 'When format=tiptap, body is the tiptap document JSON: { type: "doc", content: [...] }. The renderer expects this shape.',
|
|
@@ -1466,8 +1496,10 @@ async function uploadStaticBundle(graphql, storage, args) {
|
|
|
1466
1496
|
}
|
|
1467
1497
|
const prefix = bundlePrefix(slug);
|
|
1468
1498
|
const existing = await storage.listObjects(prefix).catch((err2) => {
|
|
1469
|
-
console.error("[upload_static_bundle] listObjects failed
|
|
1470
|
-
|
|
1499
|
+
console.error("[upload_static_bundle] listObjects failed", err2);
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
`upload_static_bundle: failed to list existing bundle objects for full-prefix wipe: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
1502
|
+
);
|
|
1471
1503
|
});
|
|
1472
1504
|
for (const obj of existing) {
|
|
1473
1505
|
await storage.deleteObject(obj.key);
|
package/dist/index.js
CHANGED
|
@@ -409,6 +409,10 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
409
409
|
slug: a.string().required(),
|
|
410
410
|
title: a.string().required(),
|
|
411
411
|
excerpt: a.string(),
|
|
412
|
+
// Denormalized from the source Post so tag pages render the real
|
|
413
|
+
// format without a second lookup. Maintained by the trusted
|
|
414
|
+
// processor (posttag-sync) on every Post mutation.
|
|
415
|
+
format: a.enum(["tiptap", "markdown", "html", "static"]),
|
|
412
416
|
// Full tag list of the post (for chip rendering on tag pages).
|
|
413
417
|
tags: a.string().array()
|
|
414
418
|
}).identifier(["tag", "publishedAtPostId"]).authorization((allow) => [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/backend",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.81",
|
|
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",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"@smithy/protocol-http": "^5.4.4",
|
|
70
70
|
"@smithy/signature-v4": "^5.4.4",
|
|
71
71
|
"fflate": "^0.8.3",
|
|
72
|
-
"@ampless/mcp-server": "1.0.0-beta.
|
|
73
|
-
"ampless": "1.0.0-beta.
|
|
72
|
+
"@ampless/mcp-server": "1.0.0-beta.60",
|
|
73
|
+
"ampless": "1.0.0-beta.54"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@aws-amplify/backend": "^1.19.0",
|