ampless 0.2.0-alpha.7 → 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
@@ -551,6 +551,94 @@ declare function formatPublicAssetUrl(bucket: string, region: string, key: strin
551
551
  */
552
552
  declare function extractFirstImageUrl(post: Post): string | null;
553
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
+
554
642
  type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
555
643
  /**
556
644
  * Single entry in a `linkList` field. Stored as part of a JSON array
@@ -764,4 +852,4 @@ declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<stri
764
852
 
765
853
  declare const VERSION = "0.0.1";
766
854
 
767
- 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, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, 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
@@ -296,6 +296,135 @@ function findHtmlImage(body) {
296
296
  return m ? m[1] : null;
297
297
  }
298
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
+
299
428
  // src/events.ts
300
429
  function detectContentEvents(input) {
301
430
  switch (input.eventName) {
@@ -471,9 +600,13 @@ function defaultValueAsString(field) {
471
600
  // src/index.ts
472
601
  var VERSION = "0.0.1";
473
602
  export {
603
+ DEFAULT_ENTRYPOINT,
474
604
  DEFAULT_SITE_ID,
605
+ MAX_BUNDLE_BYTES,
475
606
  SITE_CONFIG_PK,
607
+ TEXT_EXTENSIONS,
476
608
  VERSION,
609
+ bundlePrefix,
477
610
  composeSiteIdSlug,
478
611
  composeSiteIdStatus,
479
612
  createPost,
@@ -489,6 +622,7 @@ export {
489
622
  encodeAwsJson,
490
623
  escapeXml,
491
624
  extractFirstImageUrl,
625
+ findAbsolutePathRefs,
492
626
  flattenSettings,
493
627
  formatColorPair,
494
628
  formatDate,
@@ -503,8 +637,10 @@ export {
503
637
  isTagListUrl,
504
638
  listPosts,
505
639
  listSiteSettings,
640
+ mimeTypeFor,
506
641
  parseColorPair,
507
642
  parseLinkList,
643
+ pickDefaultEntrypoint,
508
644
  resolveLocalized,
509
645
  resolveSiteId,
510
646
  resolveThemeValues,
@@ -513,8 +649,11 @@ export {
513
649
  setSiteSetting,
514
650
  siteFor,
515
651
  stringifyLinkList,
652
+ stripCommonPrefix,
516
653
  themeSettingKey,
517
654
  unflattenSettings,
518
655
  updatePost,
656
+ validateBundle,
657
+ validateBundlePath,
519
658
  validateThemeValue
520
659
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ampless",
3
- "version": "0.2.0-alpha.7",
3
+ "version": "0.2.0-alpha.8",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",