@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.
@@ -39,7 +39,12 @@ interface UserAdminEvent {
39
39
  typeName: string;
40
40
  fieldName: string;
41
41
  arguments: Partial<SetArgs>;
42
- identity?: unknown;
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;
@@ -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(new SendMessageBatchCommand({ QueueUrl: queueUrl, Entries: chunk }));
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
- 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
- })
16
+ try {
17
+ const { apiKeys } = await client.send(
18
+ new ListApiKeysCommand({ apiId: APPSYNC_API_ID })
34
19
  );
35
- console.log(`[api-key-renewer] ${k.id}: ${beforeIso} -> ${targetIso}`);
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 GET_BY_SRC = (
666
+ var FIND_BY_SRC = (
651
667
  /* GraphQL */
652
668
  `
653
- query GetMediaBySrc($src: String!) {
654
- getMediaBySrc(src: $src) {
655
- mediaId
656
- src
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
- const data = await graphql.query(GET_BY_SRC, { src: args.src });
726
- if (data.getMediaBySrc) {
727
- mediaId = data.getMediaBySrc.mediaId;
728
- src = data.getMediaBySrc.src;
729
- assertMediaPrefix(src);
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: { type: "enum", values: ["tiptap", "markdown", "html"], required: true },
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 (proceeding)", err2);
1470
- return [];
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.80",
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.59",
73
- "ampless": "1.0.0-beta.53"
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",