ampless 0.2.0-alpha.6 → 0.2.0-alpha.8

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/index.d.ts CHANGED
@@ -484,6 +484,51 @@ declare function formatDate(input: Date | string | number, format?: DateFormat,
484
484
 
485
485
  declare function escapeXml(s: string): string;
486
486
 
487
+ /**
488
+ * Encode / decode helpers for AppSync's AWSJSON scalar.
489
+ *
490
+ * Every `a.json()` field in the schema (Post.body, Post.metadata,
491
+ * Page.body, KvStore.value, ...) carries a *JSON-encoded string* on
492
+ * the GraphQL wire — regardless of whether the underlying value is a
493
+ * string, object, or array. That rule holds for both write paths:
494
+ *
495
+ * 1. Amplify-generated client (`generateClient<Schema>().models.X.
496
+ * create({ body: ... })`) — Amplify does NOT auto-stringify, the
497
+ * caller must JSON.stringify before passing the value in.
498
+ * 2. Raw GraphQL fetch (variables map, e.g. the MCP Lambda) — same
499
+ * rule, the variable must be a JSON-encoded string.
500
+ *
501
+ * Skipping the stringify for raw string inputs (a common mistake when
502
+ * `format: 'markdown'`) trips AppSync's variable validator with
503
+ *
504
+ * Variable 'body' has an invalid value.
505
+ *
506
+ * On the read side two wire shapes coexist:
507
+ *
508
+ * - JSON-encoded string — what the auto-generated CRUD resolver and
509
+ * custom resolvers usually return.
510
+ * - Native object / Map — DynamoDBDocumentClient unmarshals
511
+ * AWSJSON-stored maps straight into JS objects when read directly
512
+ * from DynamoDB (the path used by the trusted processor and the
513
+ * MCP Lambda).
514
+ *
515
+ * `decodeAwsJson` tolerates both: pass strings through `JSON.parse`,
516
+ * everything else as-is, fall back to the raw string on parse errors
517
+ * so legacy bare-string rows aren't lost.
518
+ */
519
+ /**
520
+ * Serialise a value for an AWSJSON variable. `undefined` / `null` both
521
+ * collapse to the literal string `"null"` (valid JSON) so AppSync sees
522
+ * a well-formed AWSJSON payload either way.
523
+ */
524
+ declare function encodeAwsJson(value: unknown): string;
525
+ /**
526
+ * Deserialise an AWSJSON value from a GraphQL / DynamoDB read.
527
+ * Tolerates both the wire-string shape and the auto-unmarshalled
528
+ * native value. Returns the raw string when `JSON.parse` rejects it.
529
+ */
530
+ declare function decodeAwsJson(value: unknown): unknown;
531
+
487
532
  /**
488
533
  * Build the public S3 URL for an object key, in the regional virtual-host
489
534
  * style: `https://{bucket}.s3.{region}.amazonaws.com/{key}`. The legacy
@@ -506,6 +551,94 @@ declare function formatPublicAssetUrl(bucket: string, region: string, key: strin
506
551
  */
507
552
  declare function extractFirstImageUrl(post: Post): string | null;
508
553
 
554
+ /**
555
+ * Pure helpers for `format: 'static'` post bundles. Kept platform-free
556
+ * (no `File`, no Amplify Storage, no AWS SDK) so both the browser admin
557
+ * uploader and the MCP tools (running in stdio CLI + Lambda HTTP
558
+ * transport) can share the validation / picker / mime logic.
559
+ *
560
+ * The `extractZip(File)` / `uploadBundle` / `deleteBundle` browser
561
+ * helpers stay in `@ampless/admin/lib/static-bundle.js` — they pull in
562
+ * JSZip and `aws-amplify/storage`, neither of which we want in Lambda.
563
+ * The MCP Lambda implements its own zip extractor (`fflate`) on top of
564
+ * the helpers here.
565
+ */
566
+ declare const DEFAULT_ENTRYPOINT = "index.html";
567
+ /**
568
+ * Maximum bundle size (uncompressed) for the browser-side admin
569
+ * pipeline. Above this the in-tab extract / multi-PUT pipeline gets
570
+ * sluggish. The Lambda / MCP side enforces its own limit (Function URL
571
+ * payload cap, around 6 MB base64-inflated per call) — this constant
572
+ * is exported as a reasonable upper bound for any caller that wants
573
+ * a sanity check, not a hard contract for non-browser paths.
574
+ */
575
+ declare const MAX_BUNDLE_BYTES: number;
576
+ declare const TEXT_EXTENSIONS: ReadonlySet<string>;
577
+ declare function mimeTypeFor(path: string): string;
578
+ interface ValidationIssue {
579
+ path: string;
580
+ line?: number;
581
+ reason: string;
582
+ }
583
+ /**
584
+ * Reject paths that would either escape the bundle root (`..`), confuse
585
+ * S3 (`null bytes`), or carry OS-specific cruft from zip tools
586
+ * (`__MACOSX/`, `.DS_Store`). Returns `null` when the path is OK,
587
+ * otherwise the reason to surface to the user.
588
+ */
589
+ declare function validateBundlePath(path: string): string | null;
590
+ /**
591
+ * Scan text files for **absolute** path references. The static-bundle
592
+ * contract is that every reference resolves inside the bundle via
593
+ * relative paths, so any `href="/foo"` would either escape to the
594
+ * Next.js root or break under arbitrary URL prefixes. Protocol-prefixed
595
+ * URLs (`https://…`) and `mailto:` / `tel:` are fine.
596
+ *
597
+ * `data:` and `blob:` URIs also pass (they don't traverse the network).
598
+ * Hash-only (`#anchor`) and empty values pass too.
599
+ */
600
+ declare function findAbsolutePathRefs(path: string, content: string): ValidationIssue[];
601
+ interface ExtractedFile {
602
+ /** Relative path inside the bundle (slash-separated). */
603
+ path: string;
604
+ data: Uint8Array;
605
+ }
606
+ interface BundleExtractResult {
607
+ files: ExtractedFile[];
608
+ issues: ValidationIssue[];
609
+ totalBytes: number;
610
+ }
611
+ /**
612
+ * Scan every text file in the bundle for absolute URL refs. Returns
613
+ * the union of issues across files so the admin UI / MCP tool can
614
+ * render a single list of problems before saving.
615
+ */
616
+ declare function validateBundle(files: ExtractedFile[]): ValidationIssue[];
617
+ declare function bundlePrefix(siteId: string, slug: string): string;
618
+ /**
619
+ * If every file shares the same top-level directory (e.g. macOS Finder
620
+ * zipping wraps the contents in a `MyBundle/` folder), strip that
621
+ * prefix so the bundle's logical root is the entrypoint's parent.
622
+ * Bundles already at root pass through unchanged.
623
+ */
624
+ declare function stripCommonPrefix(files: ExtractedFile[]): ExtractedFile[];
625
+ /**
626
+ * Heuristic for the default entrypoint when the caller didn't override:
627
+ * 1. exact `index.html` at root
628
+ * 2. exact `index.htm` at root
629
+ * 3. the first .html / .htm file at root (alphabetical)
630
+ * 4. the first .html / .htm anywhere
631
+ * 5. fall back to the literal 'index.html' so the validator surfaces
632
+ * a meaningful error to the user ("entrypoint not in bundle")
633
+ *
634
+ * Accepts either extracted files (with `data`) or a plain list of
635
+ * relative paths — the MCP `commit_static_post` flow only has the
636
+ * S3 ListObjects result to work with, no bytes.
637
+ */
638
+ declare function pickDefaultEntrypoint(files: readonly {
639
+ path: string;
640
+ }[]): string;
641
+
509
642
  type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
510
643
  /**
511
644
  * Single entry in a `linkList` field. Stored as part of a JSON array
@@ -719,4 +852,4 @@ declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<stri
719
852
 
720
853
  declare const VERSION = "0.0.1";
721
854
 
722
- export { type AmplessEvent, type AmplessPlugin, type AuthContext, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_SITE_ID, type DateFormat, type EventPayloadOf, type EventType, type FieldDefinition, type FieldType, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type LocalizedString, type Media, type MediaEventPayload, type MediaEventType, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, type Page, type PluginEventHandler, type PluginMetadata, type PluginRuntimeContext, type Post, type PostMetadata, type PostStatus, type PostsProvider, type Role, SITE_CONFIG_PK, type SiteConfig, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StreamEventName, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TrustLevel, VERSION, composeSiteIdSlug, composeSiteIdStatus, createPost, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, escapeXml, extractFirstImageUrl, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isMultiSite, isTagListUrl, listPosts, listSiteSettings, parseColorPair, parseLinkList, resolveLocalized, resolveSiteId, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, siteFor, stringifyLinkList, themeSettingKey, unflattenSettings, updatePost, validateThemeValue };
855
+ export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, DEFAULT_SITE_ID, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type LocalizedString, MAX_BUNDLE_BYTES, type Media, type MediaEventPayload, type MediaEventType, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, type Page, type PluginEventHandler, type PluginMetadata, type PluginRuntimeContext, type Post, type PostMetadata, type PostStatus, type PostsProvider, type Role, SITE_CONFIG_PK, type SiteConfig, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StreamEventName, TEXT_EXTENSIONS, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TrustLevel, VERSION, type ValidationIssue, bundlePrefix, composeSiteIdSlug, composeSiteIdStatus, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isMultiSite, isTagListUrl, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolveSiteId, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, siteFor, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validateThemeValue };
package/dist/index.js CHANGED
@@ -234,6 +234,19 @@ function escapeXml(s) {
234
234
  return s.replace(/[&<>"']/g, (c) => XML_ESCAPES[c]);
235
235
  }
236
236
 
237
+ // src/awsjson.ts
238
+ function encodeAwsJson(value) {
239
+ return JSON.stringify(value ?? null);
240
+ }
241
+ function decodeAwsJson(value) {
242
+ if (typeof value !== "string") return value;
243
+ try {
244
+ return JSON.parse(value);
245
+ } catch {
246
+ return value;
247
+ }
248
+ }
249
+
237
250
  // src/storage.ts
238
251
  function formatPublicAssetUrl(bucket, region, key) {
239
252
  return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
@@ -283,6 +296,135 @@ function findHtmlImage(body) {
283
296
  return m ? m[1] : null;
284
297
  }
285
298
 
299
+ // src/static-bundle.ts
300
+ var DEFAULT_ENTRYPOINT = "index.html";
301
+ var MAX_BUNDLE_BYTES = 50 * 1024 * 1024;
302
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
303
+ ".html",
304
+ ".htm",
305
+ ".css",
306
+ ".svg"
307
+ ]);
308
+ var MIME_TYPES = {
309
+ ".html": "text/html; charset=utf-8",
310
+ ".htm": "text/html; charset=utf-8",
311
+ ".css": "text/css; charset=utf-8",
312
+ ".js": "application/javascript; charset=utf-8",
313
+ ".mjs": "application/javascript; charset=utf-8",
314
+ ".json": "application/json; charset=utf-8",
315
+ ".svg": "image/svg+xml",
316
+ ".png": "image/png",
317
+ ".jpg": "image/jpeg",
318
+ ".jpeg": "image/jpeg",
319
+ ".gif": "image/gif",
320
+ ".webp": "image/webp",
321
+ ".avif": "image/avif",
322
+ ".ico": "image/x-icon",
323
+ ".woff": "font/woff",
324
+ ".woff2": "font/woff2",
325
+ ".ttf": "font/ttf",
326
+ ".otf": "font/otf",
327
+ ".eot": "application/vnd.ms-fontobject",
328
+ ".txt": "text/plain; charset=utf-8",
329
+ ".xml": "application/xml; charset=utf-8",
330
+ ".map": "application/json; charset=utf-8",
331
+ ".pdf": "application/pdf"
332
+ };
333
+ function mimeTypeFor(path) {
334
+ const lower = path.toLowerCase();
335
+ const dot = lower.lastIndexOf(".");
336
+ if (dot < 0) return "application/octet-stream";
337
+ return MIME_TYPES[lower.slice(dot)] ?? "application/octet-stream";
338
+ }
339
+ function validateBundlePath(path) {
340
+ if (path === "" || path.endsWith("/")) return "directory entry";
341
+ if (path.includes("\0")) return "contains null byte";
342
+ if (path.startsWith("/") || path.startsWith("\\")) return "absolute path";
343
+ if (path.split(/[/\\]/).some((seg) => seg === "..")) return "parent-directory traversal";
344
+ if (path.startsWith("__MACOSX/") || /(^|\/)\._/.test(path)) return "macOS resource fork";
345
+ if (/(^|\/)\.DS_Store$/.test(path)) return ".DS_Store junk";
346
+ if (/(^|\/)Thumbs\.db$/i.test(path)) return "Thumbs.db junk";
347
+ return null;
348
+ }
349
+ var HTML_URL_ATTR_RE = /\b(?:href|src|action|data|poster|cite|formaction|manifest|srcset)\s*=\s*["']([^"']+)["']/gi;
350
+ var CSS_URL_RE = /url\(\s*["']?([^"')\s]+)["']?\s*\)|@import\s+["']([^"']+)["']/gi;
351
+ function findAbsolutePathRefs(path, content) {
352
+ const ext = path.toLowerCase().slice(path.lastIndexOf("."));
353
+ if (!TEXT_EXTENSIONS.has(ext)) return [];
354
+ const issues = [];
355
+ function check(url, lineHint) {
356
+ const trimmed = url.trim();
357
+ if (!trimmed) return;
358
+ if (trimmed.startsWith("#")) return;
359
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return;
360
+ if (trimmed.startsWith("//")) {
361
+ issues.push({ path, reason: `protocol-relative URL: ${lineHint}` });
362
+ return;
363
+ }
364
+ if (trimmed.startsWith("/")) {
365
+ issues.push({ path, reason: `absolute path: ${lineHint}` });
366
+ return;
367
+ }
368
+ }
369
+ if (ext === ".html" || ext === ".htm" || ext === ".svg") {
370
+ HTML_URL_ATTR_RE.lastIndex = 0;
371
+ let m;
372
+ while ((m = HTML_URL_ATTR_RE.exec(content)) !== null) {
373
+ const val = m[1] ?? "";
374
+ if (/\bsrcset\s*=/i.test(m[0])) {
375
+ for (const candidate of val.split(",")) {
376
+ const urlPart = candidate.trim().split(/\s+/)[0];
377
+ if (urlPart) check(urlPart, candidate.trim());
378
+ }
379
+ } else {
380
+ check(val, m[0]);
381
+ }
382
+ }
383
+ }
384
+ if (ext === ".css" || ext === ".svg") {
385
+ CSS_URL_RE.lastIndex = 0;
386
+ let m;
387
+ while ((m = CSS_URL_RE.exec(content)) !== null) {
388
+ const val = (m[1] ?? m[2] ?? "").trim();
389
+ check(val, m[0]);
390
+ }
391
+ }
392
+ return issues;
393
+ }
394
+ function validateBundle(files) {
395
+ const issues = [];
396
+ const decoder = new TextDecoder("utf-8", { fatal: false });
397
+ for (const f of files) {
398
+ const ext = f.path.toLowerCase().slice(f.path.lastIndexOf("."));
399
+ if (!TEXT_EXTENSIONS.has(ext)) continue;
400
+ const text = decoder.decode(f.data);
401
+ issues.push(...findAbsolutePathRefs(f.path, text));
402
+ }
403
+ return issues;
404
+ }
405
+ function bundlePrefix(siteId, slug) {
406
+ return `public/static/${siteId}/${slug}/`;
407
+ }
408
+ function stripCommonPrefix(files) {
409
+ if (files.length === 0) return files;
410
+ const firstSlash = files[0].path.indexOf("/");
411
+ if (firstSlash < 0) return files;
412
+ const prefix = files[0].path.slice(0, firstSlash + 1);
413
+ if (!files.every((f) => f.path.startsWith(prefix))) return files;
414
+ return files.map((f) => ({ ...f, path: f.path.slice(prefix.length) }));
415
+ }
416
+ function pickDefaultEntrypoint(files) {
417
+ const exact = files.find((f) => f.path === DEFAULT_ENTRYPOINT);
418
+ if (exact) return exact.path;
419
+ const altRoot = files.find((f) => f.path === "index.htm");
420
+ if (altRoot) return altRoot.path;
421
+ const htmlRoot = files.filter((f) => /^[^/]+\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path));
422
+ if (htmlRoot.length > 0) return htmlRoot[0].path;
423
+ const htmlAny = files.filter((f) => /\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path));
424
+ if (htmlAny.length > 0) return htmlAny[0].path;
425
+ return DEFAULT_ENTRYPOINT;
426
+ }
427
+
286
428
  // src/events.ts
287
429
  function detectContentEvents(input) {
288
430
  switch (input.eventName) {
@@ -458,12 +600,17 @@ function defaultValueAsString(field) {
458
600
  // src/index.ts
459
601
  var VERSION = "0.0.1";
460
602
  export {
603
+ DEFAULT_ENTRYPOINT,
461
604
  DEFAULT_SITE_ID,
605
+ MAX_BUNDLE_BYTES,
462
606
  SITE_CONFIG_PK,
607
+ TEXT_EXTENSIONS,
463
608
  VERSION,
609
+ bundlePrefix,
464
610
  composeSiteIdSlug,
465
611
  composeSiteIdStatus,
466
612
  createPost,
613
+ decodeAwsJson,
467
614
  defineConfig,
468
615
  definePlugin,
469
616
  defineSchema,
@@ -472,8 +619,10 @@ export {
472
619
  deletePost,
473
620
  deleteSiteSetting,
474
621
  detectContentEvents,
622
+ encodeAwsJson,
475
623
  escapeXml,
476
624
  extractFirstImageUrl,
625
+ findAbsolutePathRefs,
477
626
  flattenSettings,
478
627
  formatColorPair,
479
628
  formatDate,
@@ -488,8 +637,10 @@ export {
488
637
  isTagListUrl,
489
638
  listPosts,
490
639
  listSiteSettings,
640
+ mimeTypeFor,
491
641
  parseColorPair,
492
642
  parseLinkList,
643
+ pickDefaultEntrypoint,
493
644
  resolveLocalized,
494
645
  resolveSiteId,
495
646
  resolveThemeValues,
@@ -498,8 +649,11 @@ export {
498
649
  setSiteSetting,
499
650
  siteFor,
500
651
  stringifyLinkList,
652
+ stripCommonPrefix,
501
653
  themeSettingKey,
502
654
  unflattenSettings,
503
655
  updatePost,
656
+ validateBundle,
657
+ validateBundlePath,
504
658
  validateThemeValue
505
659
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ampless",
3
- "version": "0.2.0-alpha.6",
3
+ "version": "0.2.0-alpha.8",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",