ampless 0.2.0-alpha.7 → 1.0.0-alpha.9

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +118 -126
  2. package/dist/index.js +160 -65
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -4,7 +4,6 @@ type SiteSettingsEventType = 'site.settings.updated';
4
4
  type EventType = ContentEventType | MediaEventType | SiteSettingsEventType;
5
5
  /** Minimal projection of a Post item carried in events (no body, to keep payloads small). */
6
6
  interface ContentEventPayload {
7
- siteId: string;
8
7
  postId: string;
9
8
  slug: string;
10
9
  title: string;
@@ -13,18 +12,16 @@ interface ContentEventPayload {
13
12
  tags?: string[];
14
13
  }
15
14
  interface MediaEventPayload {
16
- siteId: string;
17
15
  mediaId: string;
18
16
  src: string;
19
17
  mimeType: string;
20
18
  }
21
19
  /**
22
- * Emitted whenever any setting under `siteconfig:{siteId}` in KvStore
23
- * is created, updated, or removed. Subscribers (built-in or user
24
- * plugins) can rebuild caches, theme assets, etc.
20
+ * Emitted whenever any setting under the `siteconfig:` PK in KvStore is
21
+ * created, updated, or removed. Subscribers (built-in or user plugins)
22
+ * can rebuild caches, theme assets, etc.
25
23
  */
26
24
  interface SiteSettingsEventPayload {
27
- siteId: string;
28
25
  }
29
26
  type EventPayloadOf<T extends EventType> = T extends ContentEventType ? ContentEventPayload : T extends MediaEventType ? MediaEventPayload : T extends SiteSettingsEventType ? SiteSettingsEventPayload : never;
30
27
  interface AmplessEvent<T extends EventType = EventType> {
@@ -89,13 +86,12 @@ type PluginEventHandler<T extends EventType = EventType> = (event: AmplessEvent<
89
86
  * (sandbox, tests) without touching plugin code.
90
87
  */
91
88
  interface PluginRuntimeContext {
92
- siteId: string;
93
89
  /** Read-only view of the cms.config site block (name, url, description). */
94
90
  site: Config['site'];
95
- /** Read all published posts for the site (used by sitemap/RSS). */
91
+ /** Read all published posts (used by sitemap/RSS). */
96
92
  listPublishedPosts(): Promise<Post[]>;
97
93
  /**
98
- * Persist a file under `public/plugins/{pluginName}/{key}` in the site's
94
+ * Persist a file under `public/plugins/{pluginName}/{key}` in the
99
95
  * S3 bucket. Returns the public URL.
100
96
  */
101
97
  writePublicAsset(key: string, body: string | Uint8Array, contentType: string): Promise<string>;
@@ -157,9 +153,9 @@ interface AmplessPlugin {
157
153
  siteMetadata?(site: Config['site']): PluginMetadata;
158
154
  /**
159
155
  * Dynamic OG image renderer. The dispatcher route (e.g.
160
- * `app/site/[siteId]/og/[slug]/route.ts`) reads this and feeds the
161
- * element into Next.js `ImageResponse`. Only one plugin should set this
162
- * — the route resolves the first plugin in `cms.config.plugins` that
156
+ * `app/og/[slug]/route.ts`) reads this and feeds the element into
157
+ * Next.js `ImageResponse`. Only one plugin should set this — the
158
+ * route resolves the first plugin in `cms.config.plugins` that
163
159
  * declares `ogImage`.
164
160
  */
165
161
  ogImage?: OgImageConfig;
@@ -169,9 +165,9 @@ declare function definePlugin(p: AmplessPlugin): AmplessPlugin;
169
165
  type ContentFormat = 'tiptap' | 'markdown' | 'html' | 'static';
170
166
  /**
171
167
  * Body shape for `format: 'static'` posts. The actual asset bytes
172
- * live under S3 at `public/static/<siteId>/<slug>/<files...>` — the
173
- * body here is the manifest describing which entrypoint to serve and
174
- * which files are part of the bundle.
168
+ * live under S3 at `public/static/<slug>/<files...>` — the body here
169
+ * is the manifest describing which entrypoint to serve and which files
170
+ * are part of the bundle.
175
171
  *
176
172
  * Stored as JSON in the `body` column (same encoding pattern as the
177
173
  * tiptap doc / html string / markdown string for the other formats).
@@ -201,7 +197,8 @@ type PostStatus = 'draft' | 'published';
201
197
  * Well-known keys:
202
198
  * - `no_layout`: when true, the public page is served as bare HTML
203
199
  * (no theme chrome). The runtime's post dispatcher checks this
204
- * before rendering and redirects to the raw route handler.
200
+ * before rendering and redirects to the unified `/_/<slug>` route
201
+ * handler.
205
202
  *
206
203
  * Additional keys are passed through unchanged — themes and plugins
207
204
  * are free to store their own per-post state here (e.g. SEO overrides,
@@ -213,7 +210,6 @@ interface PostMetadata {
213
210
  }
214
211
  interface Post {
215
212
  postId: string;
216
- siteId: string;
217
213
  slug: string;
218
214
  title: string;
219
215
  excerpt?: string;
@@ -226,7 +222,6 @@ interface Post {
226
222
  }
227
223
  interface Page {
228
224
  pageId: string;
229
- siteId: string;
230
225
  slug: string;
231
226
  title: string;
232
227
  format: ContentFormat;
@@ -236,7 +231,6 @@ interface Page {
236
231
  }
237
232
  interface Media {
238
233
  mediaId: string;
239
- siteId: string;
240
234
  src: string;
241
235
  mimeType: string;
242
236
  size: number;
@@ -261,21 +255,6 @@ interface MediaProcessingDefaults {
261
255
  /** Use lossless WebP for PNG inputs (default true). */
262
256
  losslessForPng?: boolean;
263
257
  }
264
- /**
265
- * Per-site override. The top-level `Config.site` provides defaults that
266
- * fall through when these are unset. `domains` is required because it's
267
- * how the middleware maps incoming requests to a siteId.
268
- */
269
- interface SiteConfig {
270
- /** Hostnames this site responds to (subdomains, separate apex domains, both fine). */
271
- domains: string[];
272
- /** Override `Config.site.name` for this site. */
273
- name?: string;
274
- /** Override `Config.site.url` (canonical) for this site. */
275
- url?: string;
276
- /** Override `Config.site.description` for this site. */
277
- description?: string;
278
- }
279
258
  interface Config {
280
259
  site: {
281
260
  name: string;
@@ -304,16 +283,9 @@ interface Config {
304
283
  * UI locale for the admin app. The scaffolded project ships
305
284
  * `locales/<code>.json` dictionaries; defaults are `en` and `ja`.
306
285
  * Add a new language by dropping `locales/<code>.json` in and
307
- * updating the dictionary map in `lib/i18n.ts`. Per-site override is
308
- * possible via the `locale` site setting.
286
+ * updating the dictionary map in `lib/i18n.ts`.
309
287
  */
310
288
  locale?: string;
311
- /**
312
- * Multi-site configuration. When 2+ entries are declared, the runtime
313
- * switches to multi-site mode (host-based routing + Cache-Control:
314
- * private). When 0 or 1 entries, single-site mode is used.
315
- */
316
- sites?: Record<string, SiteConfig>;
317
289
  /**
318
290
  * Active plugins. Each entry is the result of a plugin factory call
319
291
  * (e.g. `seoPlugin({ ... })`) or a raw AmplessPlugin object. Strings are
@@ -346,7 +318,6 @@ interface ContentTypeDefinition {
346
318
  declare function defineSchema<T extends Record<string, ContentTypeDefinition>>(schema: T): T;
347
319
 
348
320
  interface ListOptions {
349
- siteId?: string;
350
321
  limit?: number;
351
322
  status?: 'draft' | 'published' | 'all';
352
323
  }
@@ -355,85 +326,20 @@ type CreatePostInput = Omit<Post, 'postId'> & {
355
326
  };
356
327
  interface PostsProvider {
357
328
  list(opts?: ListOptions): Promise<Post[]>;
358
- get(slug: string, opts?: {
359
- siteId?: string;
360
- }): Promise<Post | null>;
361
- getById(postId: string, opts?: {
362
- siteId?: string;
363
- }): Promise<Post | null>;
329
+ get(slug: string): Promise<Post | null>;
330
+ getById(postId: string): Promise<Post | null>;
364
331
  create(data: CreatePostInput): Promise<Post>;
365
- update(postId: string, data: Partial<Post>, opts?: {
366
- siteId?: string;
367
- }): Promise<Post>;
368
- remove(postId: string, opts?: {
369
- siteId?: string;
370
- }): Promise<void>;
332
+ update(postId: string, data: Partial<Post>): Promise<Post>;
333
+ remove(postId: string): Promise<void>;
371
334
  }
372
335
  declare function setPostsProvider(p: PostsProvider): void;
373
336
  declare function hasPostsProvider(): boolean;
374
337
  declare function listPosts(opts?: ListOptions): Promise<Post[]>;
375
- declare function getPost(slug: string, opts?: {
376
- siteId?: string;
377
- }): Promise<Post | null>;
378
- declare function getPostById(postId: string, opts?: {
379
- siteId?: string;
380
- }): Promise<Post | null>;
338
+ declare function getPost(slug: string): Promise<Post | null>;
339
+ declare function getPostById(postId: string): Promise<Post | null>;
381
340
  declare function createPost(data: CreatePostInput): Promise<Post>;
382
- declare function updatePost(postId: string, data: Partial<Post>, opts?: {
383
- siteId?: string;
384
- }): Promise<Post>;
385
- declare function deletePost(postId: string, opts?: {
386
- siteId?: string;
387
- }): Promise<void>;
388
-
389
- /**
390
- * Single-site fallback identifier. Used when `cms.config.sites` is unset
391
- * or empty — keeps existing single-site code paths working without
392
- * special-casing.
393
- */
394
- declare const DEFAULT_SITE_ID = "default";
395
- /**
396
- * Resolve a hostname to a configured siteId.
397
- *
398
- * - Single-site mode (no `sites` defined / empty / exactly one entry):
399
- * returns that site's id regardless of host. The single site catches
400
- * every request. Matches `isMultiSite` (≥2 entries = multi).
401
- * - Multi-site mode (2+ entries): looks up the host in each site's
402
- * `domains` list. Returns `null` if the host is not registered
403
- * (caller should 404).
404
- *
405
- * The host comparison is case-insensitive; ports are not stripped here
406
- * — pass the bare hostname (e.g. `'site-a.example.com'`).
407
- */
408
- declare function resolveSiteId(host: string, config: Config): string | null;
409
- /**
410
- * Multi-site mode iff two or more sites are declared. A single declared
411
- * site is treated as single-site mode (the explicit declaration just lets
412
- * the operator name the site, but no host disambiguation is needed).
413
- */
414
- declare function isMultiSite(config: Config): boolean;
415
- /**
416
- * Site-effective name / url / description for a given siteId. Per-site
417
- * `sites.{id}.{name|url|description}` overrides the top-level `site.*`
418
- * defaults; otherwise falls through.
419
- */
420
- declare function siteFor(siteId: string, config: Config): {
421
- name: string;
422
- url: string;
423
- description?: string;
424
- };
425
- /**
426
- * Build the denormalized GSI key for `bySiteIdStatus`. All Post writes
427
- * (admin client, MCP tools) must set this so the public-read resolvers
428
- * can do a single Query without table-level filtering.
429
- */
430
- declare function composeSiteIdStatus(siteId: string, status: PostStatus): string;
431
- /**
432
- * Build the denormalized GSI key for `bySiteIdSlug`. The public
433
- * `getPublishedPost(slug)` resolver does an O(1) PK lookup against
434
- * this index, so every Post write must set it alongside the slug.
435
- */
436
- declare function composeSiteIdSlug(siteId: string, slug: string): string;
341
+ declare function updatePost(postId: string, data: Partial<Post>): Promise<Post>;
342
+ declare function deletePost(postId: string): Promise<void>;
437
343
 
438
344
  interface KvItem<T = unknown> {
439
345
  pk: string;
@@ -459,15 +365,15 @@ declare function hasKvStore(): boolean;
459
365
  * namespace (e.g. `mcp-tokens`).
460
366
  */
461
367
  declare function getKvStore(): KvStore;
462
- declare const SITE_CONFIG_PK: (siteId: string) => string;
463
- declare function getSiteSetting<T = unknown>(siteId: string, key: string): Promise<T | null>;
464
- declare function setSiteSetting(siteId: string, key: string, value: unknown): Promise<void>;
465
- declare function deleteSiteSetting(siteId: string, key: string): Promise<void>;
368
+ declare const SITE_CONFIG_PK = "siteconfig";
369
+ declare function getSiteSetting<T = unknown>(key: string): Promise<T | null>;
370
+ declare function setSiteSetting(key: string, value: unknown): Promise<void>;
371
+ declare function deleteSiteSetting(key: string): Promise<void>;
466
372
  /**
467
- * Fetch every setting for a site as a flat map (`{ 'site.name': 'My Blog', ... }`).
373
+ * Fetch every site setting as a flat map (`{ 'site.name': 'My Blog', ... }`).
468
374
  * Use `unflattenSettings` to convert to the nested shape if needed.
469
375
  */
470
- declare function listSiteSettings(siteId?: string): Promise<Record<string, unknown>>;
376
+ declare function listSiteSettings(): Promise<Record<string, unknown>>;
471
377
  declare function flattenSettings(obj: Record<string, unknown>, prefix?: string): Record<string, unknown>;
472
378
  declare function unflattenSettings(flat: Record<string, unknown>): Record<string, unknown>;
473
379
 
@@ -551,6 +457,94 @@ declare function formatPublicAssetUrl(bucket: string, region: string, key: strin
551
457
  */
552
458
  declare function extractFirstImageUrl(post: Post): string | null;
553
459
 
460
+ /**
461
+ * Pure helpers for `format: 'static'` post bundles. Kept platform-free
462
+ * (no `File`, no Amplify Storage, no AWS SDK) so both the browser admin
463
+ * uploader and the MCP tools (running in stdio CLI + Lambda HTTP
464
+ * transport) can share the validation / picker / mime logic.
465
+ *
466
+ * The `extractZip(File)` / `uploadBundle` / `deleteBundle` browser
467
+ * helpers stay in `@ampless/admin/lib/static-bundle.js` — they pull in
468
+ * JSZip and `aws-amplify/storage`, neither of which we want in Lambda.
469
+ * The MCP Lambda implements its own zip extractor (`fflate`) on top of
470
+ * the helpers here.
471
+ */
472
+ declare const DEFAULT_ENTRYPOINT = "index.html";
473
+ /**
474
+ * Maximum bundle size (uncompressed) for the browser-side admin
475
+ * pipeline. Above this the in-tab extract / multi-PUT pipeline gets
476
+ * sluggish. The Lambda / MCP side enforces its own limit (Function URL
477
+ * payload cap, around 6 MB base64-inflated per call) — this constant
478
+ * is exported as a reasonable upper bound for any caller that wants
479
+ * a sanity check, not a hard contract for non-browser paths.
480
+ */
481
+ declare const MAX_BUNDLE_BYTES: number;
482
+ declare const TEXT_EXTENSIONS: ReadonlySet<string>;
483
+ declare function mimeTypeFor(path: string): string;
484
+ interface ValidationIssue {
485
+ path: string;
486
+ line?: number;
487
+ reason: string;
488
+ }
489
+ /**
490
+ * Reject paths that would either escape the bundle root (`..`), confuse
491
+ * S3 (`null bytes`), or carry OS-specific cruft from zip tools
492
+ * (`__MACOSX/`, `.DS_Store`). Returns `null` when the path is OK,
493
+ * otherwise the reason to surface to the user.
494
+ */
495
+ declare function validateBundlePath(path: string): string | null;
496
+ /**
497
+ * Scan text files for **absolute** path references. The static-bundle
498
+ * contract is that every reference resolves inside the bundle via
499
+ * relative paths, so any `href="/foo"` would either escape to the
500
+ * Next.js root or break under arbitrary URL prefixes. Protocol-prefixed
501
+ * URLs (`https://…`) and `mailto:` / `tel:` are fine.
502
+ *
503
+ * `data:` and `blob:` URIs also pass (they don't traverse the network).
504
+ * Hash-only (`#anchor`) and empty values pass too.
505
+ */
506
+ declare function findAbsolutePathRefs(path: string, content: string): ValidationIssue[];
507
+ interface ExtractedFile {
508
+ /** Relative path inside the bundle (slash-separated). */
509
+ path: string;
510
+ data: Uint8Array;
511
+ }
512
+ interface BundleExtractResult {
513
+ files: ExtractedFile[];
514
+ issues: ValidationIssue[];
515
+ totalBytes: number;
516
+ }
517
+ /**
518
+ * Scan every text file in the bundle for absolute URL refs. Returns
519
+ * the union of issues across files so the admin UI / MCP tool can
520
+ * render a single list of problems before saving.
521
+ */
522
+ declare function validateBundle(files: ExtractedFile[]): ValidationIssue[];
523
+ declare function bundlePrefix(slug: string): string;
524
+ /**
525
+ * If every file shares the same top-level directory (e.g. macOS Finder
526
+ * zipping wraps the contents in a `MyBundle/` folder), strip that
527
+ * prefix so the bundle's logical root is the entrypoint's parent.
528
+ * Bundles already at root pass through unchanged.
529
+ */
530
+ declare function stripCommonPrefix(files: ExtractedFile[]): ExtractedFile[];
531
+ /**
532
+ * Heuristic for the default entrypoint when the caller didn't override:
533
+ * 1. exact `index.html` at root
534
+ * 2. exact `index.htm` at root
535
+ * 3. the first .html / .htm file at root (alphabetical)
536
+ * 4. the first .html / .htm anywhere
537
+ * 5. fall back to the literal 'index.html' so the validator surfaces
538
+ * a meaningful error to the user ("entrypoint not in bundle")
539
+ *
540
+ * Accepts either extracted files (with `data`) or a plain list of
541
+ * relative paths — the MCP `commit_static_post` flow only has the
542
+ * S3 ListObjects result to work with, no bytes.
543
+ */
544
+ declare function pickDefaultEntrypoint(files: readonly {
545
+ path: string;
546
+ }[]): string;
547
+
554
548
  type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
555
549
  /**
556
550
  * Single entry in a `linkList` field. Stored as part of a JSON array
@@ -695,11 +689,9 @@ interface ThemeModule {
695
689
  */
696
690
  routes?: {
697
691
  feed?: (ctx: {
698
- siteId: string;
699
692
  request: Request;
700
693
  }) => Promise<Response>;
701
694
  sitemap?: (ctx: {
702
- siteId: string;
703
695
  request: Request;
704
696
  }) => Promise<Response>;
705
697
  };
@@ -757,11 +749,11 @@ declare function isTagListUrl(url: string): {
757
749
  /**
758
750
  * Resolve effective values for every manifest field, merging stored
759
751
  * overrides on top of defaults. `stored` is the flat settings map keyed
760
- * by `theme.{key}` — typically the output of `listSiteSettings(siteId)`
752
+ * by `theme.{key}` — typically the output of `listSiteSettings()`
761
753
  * filtered to theme entries.
762
754
  */
763
755
  declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
764
756
 
765
757
  declare const VERSION = "0.0.1";
766
758
 
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 };
759
+ export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, 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 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, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validateThemeValue };
package/dist/index.js CHANGED
@@ -12,7 +12,6 @@ function defineSchema(schema) {
12
12
  var DUMMY_POSTS = [
13
13
  {
14
14
  postId: "post-001",
15
- siteId: "default",
16
15
  slug: "hello-world",
17
16
  title: "Hello, ampless",
18
17
  excerpt: "Welcome to your new ampless-powered blog. This is the first post.",
@@ -24,7 +23,6 @@ var DUMMY_POSTS = [
24
23
  },
25
24
  {
26
25
  postId: "post-002",
27
- siteId: "default",
28
26
  slug: "about-ampless",
29
27
  title: "About ampless",
30
28
  excerpt: "Why ampless exists, and what makes it different from other CMS options.",
@@ -36,7 +34,6 @@ var DUMMY_POSTS = [
36
34
  },
37
35
  {
38
36
  postId: "post-003",
39
- siteId: "default",
40
37
  slug: "getting-started",
41
38
  title: "Getting started",
42
39
  excerpt: "How to set up your ampless site and deploy it to AWS.",
@@ -57,8 +54,8 @@ function hasPostsProvider() {
57
54
  return provider !== null;
58
55
  }
59
56
  function dummyList(opts = {}) {
60
- const { siteId = "default", limit, status = "published" } = opts;
61
- let posts = DUMMY_POSTS.filter((p) => p.siteId === siteId);
57
+ const { limit, status = "published" } = opts;
58
+ let posts = DUMMY_POSTS;
62
59
  if (status !== "all") posts = posts.filter((p) => p.status === status);
63
60
  return limit ? posts.slice(0, limit) : posts;
64
61
  }
@@ -66,60 +63,25 @@ async function listPosts(opts = {}) {
66
63
  if (provider) return provider.list(opts);
67
64
  return dummyList(opts);
68
65
  }
69
- async function getPost(slug, opts = {}) {
70
- if (provider) return provider.get(slug, opts);
71
- const { siteId = "default" } = opts;
72
- return DUMMY_POSTS.find((p) => p.siteId === siteId && p.slug === slug) ?? null;
66
+ async function getPost(slug) {
67
+ if (provider) return provider.get(slug);
68
+ return DUMMY_POSTS.find((p) => p.slug === slug) ?? null;
73
69
  }
74
- async function getPostById(postId, opts = {}) {
75
- if (provider) return provider.getById(postId, opts);
76
- const { siteId = "default" } = opts;
77
- return DUMMY_POSTS.find((p) => p.siteId === siteId && p.postId === postId) ?? null;
70
+ async function getPostById(postId) {
71
+ if (provider) return provider.getById(postId);
72
+ return DUMMY_POSTS.find((p) => p.postId === postId) ?? null;
78
73
  }
79
74
  async function createPost(data) {
80
75
  if (!provider) throw new Error("No posts provider configured. Call setPostsProvider() first.");
81
76
  return provider.create(data);
82
77
  }
83
- async function updatePost(postId, data, opts = {}) {
78
+ async function updatePost(postId, data) {
84
79
  if (!provider) throw new Error("No posts provider configured. Call setPostsProvider() first.");
85
- return provider.update(postId, data, opts);
80
+ return provider.update(postId, data);
86
81
  }
87
- async function deletePost(postId, opts = {}) {
82
+ async function deletePost(postId) {
88
83
  if (!provider) throw new Error("No posts provider configured. Call setPostsProvider() first.");
89
- return provider.remove(postId, opts);
90
- }
91
-
92
- // src/sites.ts
93
- var DEFAULT_SITE_ID = "default";
94
- function resolveSiteId(host, config) {
95
- const sites = config.sites;
96
- if (!sites || Object.keys(sites).length === 0) {
97
- return DEFAULT_SITE_ID;
98
- }
99
- const ids = Object.keys(sites);
100
- if (ids.length === 1) return ids[0];
101
- const lower = host.toLowerCase();
102
- for (const [id, site] of Object.entries(sites)) {
103
- if (site.domains.some((d) => d.toLowerCase() === lower)) return id;
104
- }
105
- return null;
106
- }
107
- function isMultiSite(config) {
108
- return !!config.sites && Object.keys(config.sites).length >= 2;
109
- }
110
- function siteFor(siteId, config) {
111
- const override = config.sites?.[siteId];
112
- return {
113
- name: override?.name ?? config.site.name,
114
- url: override?.url ?? config.site.url,
115
- description: override?.description ?? config.site.description
116
- };
117
- }
118
- function composeSiteIdStatus(siteId, status) {
119
- return `${siteId}#${status}`;
120
- }
121
- function composeSiteIdSlug(siteId, slug) {
122
- return `${siteId}#${slug}`;
84
+ return provider.remove(postId);
123
85
  }
124
86
 
125
87
  // src/kv.ts
@@ -139,18 +101,18 @@ function requireStore() {
139
101
  function getKvStore() {
140
102
  return requireStore();
141
103
  }
142
- var SITE_CONFIG_PK = (siteId) => `siteconfig:${siteId}`;
143
- async function getSiteSetting(siteId, key) {
144
- return requireStore().get(SITE_CONFIG_PK(siteId), key);
104
+ var SITE_CONFIG_PK = "siteconfig";
105
+ async function getSiteSetting(key) {
106
+ return requireStore().get(SITE_CONFIG_PK, key);
145
107
  }
146
- async function setSiteSetting(siteId, key, value) {
147
- return requireStore().put(SITE_CONFIG_PK(siteId), key, value);
108
+ async function setSiteSetting(key, value) {
109
+ return requireStore().put(SITE_CONFIG_PK, key, value);
148
110
  }
149
- async function deleteSiteSetting(siteId, key) {
150
- return requireStore().remove(SITE_CONFIG_PK(siteId), key);
111
+ async function deleteSiteSetting(key) {
112
+ return requireStore().remove(SITE_CONFIG_PK, key);
151
113
  }
152
- async function listSiteSettings(siteId = DEFAULT_SITE_ID) {
153
- const items = await requireStore().query(SITE_CONFIG_PK(siteId));
114
+ async function listSiteSettings() {
115
+ const items = await requireStore().query(SITE_CONFIG_PK);
154
116
  const out = {};
155
117
  for (const item of items) {
156
118
  out[item.sk] = item.value;
@@ -296,6 +258,135 @@ function findHtmlImage(body) {
296
258
  return m ? m[1] : null;
297
259
  }
298
260
 
261
+ // src/static-bundle.ts
262
+ var DEFAULT_ENTRYPOINT = "index.html";
263
+ var MAX_BUNDLE_BYTES = 50 * 1024 * 1024;
264
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
265
+ ".html",
266
+ ".htm",
267
+ ".css",
268
+ ".svg"
269
+ ]);
270
+ var MIME_TYPES = {
271
+ ".html": "text/html; charset=utf-8",
272
+ ".htm": "text/html; charset=utf-8",
273
+ ".css": "text/css; charset=utf-8",
274
+ ".js": "application/javascript; charset=utf-8",
275
+ ".mjs": "application/javascript; charset=utf-8",
276
+ ".json": "application/json; charset=utf-8",
277
+ ".svg": "image/svg+xml",
278
+ ".png": "image/png",
279
+ ".jpg": "image/jpeg",
280
+ ".jpeg": "image/jpeg",
281
+ ".gif": "image/gif",
282
+ ".webp": "image/webp",
283
+ ".avif": "image/avif",
284
+ ".ico": "image/x-icon",
285
+ ".woff": "font/woff",
286
+ ".woff2": "font/woff2",
287
+ ".ttf": "font/ttf",
288
+ ".otf": "font/otf",
289
+ ".eot": "application/vnd.ms-fontobject",
290
+ ".txt": "text/plain; charset=utf-8",
291
+ ".xml": "application/xml; charset=utf-8",
292
+ ".map": "application/json; charset=utf-8",
293
+ ".pdf": "application/pdf"
294
+ };
295
+ function mimeTypeFor(path) {
296
+ const lower = path.toLowerCase();
297
+ const dot = lower.lastIndexOf(".");
298
+ if (dot < 0) return "application/octet-stream";
299
+ return MIME_TYPES[lower.slice(dot)] ?? "application/octet-stream";
300
+ }
301
+ function validateBundlePath(path) {
302
+ if (path === "" || path.endsWith("/")) return "directory entry";
303
+ if (path.includes("\0")) return "contains null byte";
304
+ if (path.startsWith("/") || path.startsWith("\\")) return "absolute path";
305
+ if (path.split(/[/\\]/).some((seg) => seg === "..")) return "parent-directory traversal";
306
+ if (path.startsWith("__MACOSX/") || /(^|\/)\._/.test(path)) return "macOS resource fork";
307
+ if (/(^|\/)\.DS_Store$/.test(path)) return ".DS_Store junk";
308
+ if (/(^|\/)Thumbs\.db$/i.test(path)) return "Thumbs.db junk";
309
+ return null;
310
+ }
311
+ var HTML_URL_ATTR_RE = /\b(?:href|src|action|data|poster|cite|formaction|manifest|srcset)\s*=\s*["']([^"']+)["']/gi;
312
+ var CSS_URL_RE = /url\(\s*["']?([^"')\s]+)["']?\s*\)|@import\s+["']([^"']+)["']/gi;
313
+ function findAbsolutePathRefs(path, content) {
314
+ const ext = path.toLowerCase().slice(path.lastIndexOf("."));
315
+ if (!TEXT_EXTENSIONS.has(ext)) return [];
316
+ const issues = [];
317
+ function check(url, lineHint) {
318
+ const trimmed = url.trim();
319
+ if (!trimmed) return;
320
+ if (trimmed.startsWith("#")) return;
321
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return;
322
+ if (trimmed.startsWith("//")) {
323
+ issues.push({ path, reason: `protocol-relative URL: ${lineHint}` });
324
+ return;
325
+ }
326
+ if (trimmed.startsWith("/")) {
327
+ issues.push({ path, reason: `absolute path: ${lineHint}` });
328
+ return;
329
+ }
330
+ }
331
+ if (ext === ".html" || ext === ".htm" || ext === ".svg") {
332
+ HTML_URL_ATTR_RE.lastIndex = 0;
333
+ let m;
334
+ while ((m = HTML_URL_ATTR_RE.exec(content)) !== null) {
335
+ const val = m[1] ?? "";
336
+ if (/\bsrcset\s*=/i.test(m[0])) {
337
+ for (const candidate of val.split(",")) {
338
+ const urlPart = candidate.trim().split(/\s+/)[0];
339
+ if (urlPart) check(urlPart, candidate.trim());
340
+ }
341
+ } else {
342
+ check(val, m[0]);
343
+ }
344
+ }
345
+ }
346
+ if (ext === ".css" || ext === ".svg") {
347
+ CSS_URL_RE.lastIndex = 0;
348
+ let m;
349
+ while ((m = CSS_URL_RE.exec(content)) !== null) {
350
+ const val = (m[1] ?? m[2] ?? "").trim();
351
+ check(val, m[0]);
352
+ }
353
+ }
354
+ return issues;
355
+ }
356
+ function validateBundle(files) {
357
+ const issues = [];
358
+ const decoder = new TextDecoder("utf-8", { fatal: false });
359
+ for (const f of files) {
360
+ const ext = f.path.toLowerCase().slice(f.path.lastIndexOf("."));
361
+ if (!TEXT_EXTENSIONS.has(ext)) continue;
362
+ const text = decoder.decode(f.data);
363
+ issues.push(...findAbsolutePathRefs(f.path, text));
364
+ }
365
+ return issues;
366
+ }
367
+ function bundlePrefix(slug) {
368
+ return `public/static/${slug}/`;
369
+ }
370
+ function stripCommonPrefix(files) {
371
+ if (files.length === 0) return files;
372
+ const firstSlash = files[0].path.indexOf("/");
373
+ if (firstSlash < 0) return files;
374
+ const prefix = files[0].path.slice(0, firstSlash + 1);
375
+ if (!files.every((f) => f.path.startsWith(prefix))) return files;
376
+ return files.map((f) => ({ ...f, path: f.path.slice(prefix.length) }));
377
+ }
378
+ function pickDefaultEntrypoint(files) {
379
+ const exact = files.find((f) => f.path === DEFAULT_ENTRYPOINT);
380
+ if (exact) return exact.path;
381
+ const altRoot = files.find((f) => f.path === "index.htm");
382
+ if (altRoot) return altRoot.path;
383
+ const htmlRoot = files.filter((f) => /^[^/]+\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path));
384
+ if (htmlRoot.length > 0) return htmlRoot[0].path;
385
+ const htmlAny = files.filter((f) => /\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path));
386
+ if (htmlAny.length > 0) return htmlAny[0].path;
387
+ return DEFAULT_ENTRYPOINT;
388
+ }
389
+
299
390
  // src/events.ts
300
391
  function detectContentEvents(input) {
301
392
  switch (input.eventName) {
@@ -471,11 +562,12 @@ function defaultValueAsString(field) {
471
562
  // src/index.ts
472
563
  var VERSION = "0.0.1";
473
564
  export {
474
- DEFAULT_SITE_ID,
565
+ DEFAULT_ENTRYPOINT,
566
+ MAX_BUNDLE_BYTES,
475
567
  SITE_CONFIG_PK,
568
+ TEXT_EXTENSIONS,
476
569
  VERSION,
477
- composeSiteIdSlug,
478
- composeSiteIdStatus,
570
+ bundlePrefix,
479
571
  createPost,
480
572
  decodeAwsJson,
481
573
  defineConfig,
@@ -489,6 +581,7 @@ export {
489
581
  encodeAwsJson,
490
582
  escapeXml,
491
583
  extractFirstImageUrl,
584
+ findAbsolutePathRefs,
492
585
  flattenSettings,
493
586
  formatColorPair,
494
587
  formatDate,
@@ -499,22 +592,24 @@ export {
499
592
  getSiteSetting,
500
593
  hasKvStore,
501
594
  hasPostsProvider,
502
- isMultiSite,
503
595
  isTagListUrl,
504
596
  listPosts,
505
597
  listSiteSettings,
598
+ mimeTypeFor,
506
599
  parseColorPair,
507
600
  parseLinkList,
601
+ pickDefaultEntrypoint,
508
602
  resolveLocalized,
509
- resolveSiteId,
510
603
  resolveThemeValues,
511
604
  setKvStore,
512
605
  setPostsProvider,
513
606
  setSiteSetting,
514
- siteFor,
515
607
  stringifyLinkList,
608
+ stripCommonPrefix,
516
609
  themeSettingKey,
517
610
  unflattenSettings,
518
611
  updatePost,
612
+ validateBundle,
613
+ validateBundlePath,
519
614
  validateThemeValue
520
615
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ampless",
3
- "version": "0.2.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",