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

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 +184 -133
  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).
@@ -192,6 +188,22 @@ interface StaticPostBody {
192
188
  uploadedAt: string;
193
189
  }
194
190
  type PostStatus = 'draft' | 'published';
191
+ /**
192
+ * Per-post cache strategy. Middleware computes the response's
193
+ * Cache-Control header from this value (plus `post.updatedAt` for the
194
+ * `auto` case and the project's `cms.config.cache.*` knobs).
195
+ *
196
+ * - `'auto'` (default): cooldown by edit time. Posts updated within
197
+ * the cooldown window (`cms.config.cache.cooldownMs`, default 1h)
198
+ * emit a no-store header so editors see fresh content immediately.
199
+ * Older posts emit a long s-maxage so the CDN serves them cheaply.
200
+ * - `'deep'`: always long-cache (`cms.config.cache.deepTtlSeconds`,
201
+ * default 1h). Use for posts whose content is fixed for the
202
+ * foreseeable future.
203
+ * - `'hot'`: always no-store. Use for posts whose content is rapidly
204
+ * evolving or computed per request.
205
+ */
206
+ type CacheStrategy = 'auto' | 'deep' | 'hot';
195
207
  /**
196
208
  * Free-form per-post metadata. The `metadata` JSON column carries
197
209
  * arbitrary key/value pairs; the runtime / themes / plugins each pick
@@ -200,8 +212,13 @@ type PostStatus = 'draft' | 'published';
200
212
  *
201
213
  * Well-known keys:
202
214
  * - `no_layout`: when true, the public page is served as bare HTML
203
- * (no theme chrome). The runtime's post dispatcher checks this
204
- * before rendering and redirects to the raw route handler.
215
+ * (no theme chrome). Middleware rewrites the request to the
216
+ * unified internal route handler (`/r/<slug>`) for such posts
217
+ * and renders the body verbatim — no Next.js root layout, no
218
+ * theme chrome.
219
+ * - `cache`: see `CacheStrategy`. Overrides the default 'auto'
220
+ * cache strategy for this post. Independent of `no_layout` —
221
+ * applies uniformly to themed, no_layout, and static posts.
205
222
  *
206
223
  * Additional keys are passed through unchanged — themes and plugins
207
224
  * are free to store their own per-post state here (e.g. SEO overrides,
@@ -209,11 +226,11 @@ type PostStatus = 'draft' | 'published';
209
226
  */
210
227
  interface PostMetadata {
211
228
  no_layout?: boolean;
229
+ cache?: CacheStrategy;
212
230
  [key: string]: unknown;
213
231
  }
214
232
  interface Post {
215
233
  postId: string;
216
- siteId: string;
217
234
  slug: string;
218
235
  title: string;
219
236
  excerpt?: string;
@@ -223,10 +240,18 @@ interface Post {
223
240
  publishedAt?: string;
224
241
  tags?: string[];
225
242
  metadata?: PostMetadata;
243
+ /**
244
+ * DynamoDB auto-managed timestamp (ISO 8601). Surfaced through the
245
+ * `PublicPost` projection so middleware can compute the
246
+ * `metadata.cache='auto'` cooldown without re-fetching the model
247
+ * row. Absent on optimistically-constructed posts (e.g. test
248
+ * fixtures); the cache strategy treats absent values as "very old"
249
+ * and emits a long s-maxage.
250
+ */
251
+ updatedAt?: string;
226
252
  }
227
253
  interface Page {
228
254
  pageId: string;
229
- siteId: string;
230
255
  slug: string;
231
256
  title: string;
232
257
  format: ContentFormat;
@@ -236,7 +261,6 @@ interface Page {
236
261
  }
237
262
  interface Media {
238
263
  mediaId: string;
239
- siteId: string;
240
264
  src: string;
241
265
  mimeType: string;
242
266
  size: number;
@@ -261,21 +285,6 @@ interface MediaProcessingDefaults {
261
285
  /** Use lossless WebP for PNG inputs (default true). */
262
286
  losslessForPng?: boolean;
263
287
  }
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
288
  interface Config {
280
289
  site: {
281
290
  name: string;
@@ -304,16 +313,9 @@ interface Config {
304
313
  * UI locale for the admin app. The scaffolded project ships
305
314
  * `locales/<code>.json` dictionaries; defaults are `en` and `ja`.
306
315
  * 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.
316
+ * updating the dictionary map in `lib/i18n.ts`.
309
317
  */
310
318
  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
319
  /**
318
320
  * Active plugins. Each entry is the result of a plugin factory call
319
321
  * (e.g. `seoPlugin({ ... })`) or a raw AmplessPlugin object. Strings are
@@ -321,6 +323,36 @@ interface Config {
321
323
  * ignored by the runtime.
322
324
  */
323
325
  plugins?: Array<AmplessPlugin | string>;
326
+ /**
327
+ * Cache strategy knobs read by the runtime's middleware when
328
+ * computing `Cache-Control` for post responses. Each field is
329
+ * optional — middleware applies the documented defaults when a
330
+ * field is absent.
331
+ */
332
+ cache?: CacheConfig;
333
+ }
334
+ /**
335
+ * Tunables for middleware-computed `Cache-Control`. See
336
+ * `PostMetadata.cache` / `CacheStrategy` for how the per-post override
337
+ * interacts with these defaults.
338
+ */
339
+ interface CacheConfig {
340
+ /**
341
+ * `cache: 'auto'` cooldown. Posts whose `updatedAt` is younger than
342
+ * this many milliseconds emit a no-store header so editors see
343
+ * fresh content immediately after a save. Default 3,600,000 (1h).
344
+ */
345
+ cooldownMs?: number;
346
+ /**
347
+ * `cache: 'auto'` post-cooldown TTL, in seconds. Applied as both
348
+ * `max-age` and `s-maxage` on the response. Default 300 (5 minutes).
349
+ */
350
+ freshTtlSeconds?: number;
351
+ /**
352
+ * `cache: 'deep'` TTL, in seconds. Applied as both `max-age` and
353
+ * `s-maxage`. Default 3600 (1 hour).
354
+ */
355
+ deepTtlSeconds?: number;
324
356
  }
325
357
  type Role = 'reader' | 'editor' | 'admin';
326
358
  interface AuthContext {
@@ -346,7 +378,6 @@ interface ContentTypeDefinition {
346
378
  declare function defineSchema<T extends Record<string, ContentTypeDefinition>>(schema: T): T;
347
379
 
348
380
  interface ListOptions {
349
- siteId?: string;
350
381
  limit?: number;
351
382
  status?: 'draft' | 'published' | 'all';
352
383
  }
@@ -355,85 +386,20 @@ type CreatePostInput = Omit<Post, 'postId'> & {
355
386
  };
356
387
  interface PostsProvider {
357
388
  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>;
389
+ get(slug: string): Promise<Post | null>;
390
+ getById(postId: string): Promise<Post | null>;
364
391
  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>;
392
+ update(postId: string, data: Partial<Post>): Promise<Post>;
393
+ remove(postId: string): Promise<void>;
371
394
  }
372
395
  declare function setPostsProvider(p: PostsProvider): void;
373
396
  declare function hasPostsProvider(): boolean;
374
397
  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>;
398
+ declare function getPost(slug: string): Promise<Post | null>;
399
+ declare function getPostById(postId: string): Promise<Post | null>;
381
400
  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;
401
+ declare function updatePost(postId: string, data: Partial<Post>): Promise<Post>;
402
+ declare function deletePost(postId: string): Promise<void>;
437
403
 
438
404
  interface KvItem<T = unknown> {
439
405
  pk: string;
@@ -459,15 +425,15 @@ declare function hasKvStore(): boolean;
459
425
  * namespace (e.g. `mcp-tokens`).
460
426
  */
461
427
  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>;
428
+ declare const SITE_CONFIG_PK = "siteconfig";
429
+ declare function getSiteSetting<T = unknown>(key: string): Promise<T | null>;
430
+ declare function setSiteSetting(key: string, value: unknown): Promise<void>;
431
+ declare function deleteSiteSetting(key: string): Promise<void>;
466
432
  /**
467
- * Fetch every setting for a site as a flat map (`{ 'site.name': 'My Blog', ... }`).
433
+ * Fetch every site setting as a flat map (`{ 'site.name': 'My Blog', ... }`).
468
434
  * Use `unflattenSettings` to convert to the nested shape if needed.
469
435
  */
470
- declare function listSiteSettings(siteId?: string): Promise<Record<string, unknown>>;
436
+ declare function listSiteSettings(): Promise<Record<string, unknown>>;
471
437
  declare function flattenSettings(obj: Record<string, unknown>, prefix?: string): Record<string, unknown>;
472
438
  declare function unflattenSettings(flat: Record<string, unknown>): Record<string, unknown>;
473
439
 
@@ -551,6 +517,94 @@ declare function formatPublicAssetUrl(bucket: string, region: string, key: strin
551
517
  */
552
518
  declare function extractFirstImageUrl(post: Post): string | null;
553
519
 
520
+ /**
521
+ * Pure helpers for `format: 'static'` post bundles. Kept platform-free
522
+ * (no `File`, no Amplify Storage, no AWS SDK) so both the browser admin
523
+ * uploader and the MCP tools (running in stdio CLI + Lambda HTTP
524
+ * transport) can share the validation / picker / mime logic.
525
+ *
526
+ * The `extractZip(File)` / `uploadBundle` / `deleteBundle` browser
527
+ * helpers stay in `@ampless/admin/lib/static-bundle.js` — they pull in
528
+ * JSZip and `aws-amplify/storage`, neither of which we want in Lambda.
529
+ * The MCP Lambda implements its own zip extractor (`fflate`) on top of
530
+ * the helpers here.
531
+ */
532
+ declare const DEFAULT_ENTRYPOINT = "index.html";
533
+ /**
534
+ * Maximum bundle size (uncompressed) for the browser-side admin
535
+ * pipeline. Above this the in-tab extract / multi-PUT pipeline gets
536
+ * sluggish. The Lambda / MCP side enforces its own limit (Function URL
537
+ * payload cap, around 6 MB base64-inflated per call) — this constant
538
+ * is exported as a reasonable upper bound for any caller that wants
539
+ * a sanity check, not a hard contract for non-browser paths.
540
+ */
541
+ declare const MAX_BUNDLE_BYTES: number;
542
+ declare const TEXT_EXTENSIONS: ReadonlySet<string>;
543
+ declare function mimeTypeFor(path: string): string;
544
+ interface ValidationIssue {
545
+ path: string;
546
+ line?: number;
547
+ reason: string;
548
+ }
549
+ /**
550
+ * Reject paths that would either escape the bundle root (`..`), confuse
551
+ * S3 (`null bytes`), or carry OS-specific cruft from zip tools
552
+ * (`__MACOSX/`, `.DS_Store`). Returns `null` when the path is OK,
553
+ * otherwise the reason to surface to the user.
554
+ */
555
+ declare function validateBundlePath(path: string): string | null;
556
+ /**
557
+ * Scan text files for **absolute** path references. The static-bundle
558
+ * contract is that every reference resolves inside the bundle via
559
+ * relative paths, so any `href="/foo"` would either escape to the
560
+ * Next.js root or break under arbitrary URL prefixes. Protocol-prefixed
561
+ * URLs (`https://…`) and `mailto:` / `tel:` are fine.
562
+ *
563
+ * `data:` and `blob:` URIs also pass (they don't traverse the network).
564
+ * Hash-only (`#anchor`) and empty values pass too.
565
+ */
566
+ declare function findAbsolutePathRefs(path: string, content: string): ValidationIssue[];
567
+ interface ExtractedFile {
568
+ /** Relative path inside the bundle (slash-separated). */
569
+ path: string;
570
+ data: Uint8Array;
571
+ }
572
+ interface BundleExtractResult {
573
+ files: ExtractedFile[];
574
+ issues: ValidationIssue[];
575
+ totalBytes: number;
576
+ }
577
+ /**
578
+ * Scan every text file in the bundle for absolute URL refs. Returns
579
+ * the union of issues across files so the admin UI / MCP tool can
580
+ * render a single list of problems before saving.
581
+ */
582
+ declare function validateBundle(files: ExtractedFile[]): ValidationIssue[];
583
+ declare function bundlePrefix(slug: string): string;
584
+ /**
585
+ * If every file shares the same top-level directory (e.g. macOS Finder
586
+ * zipping wraps the contents in a `MyBundle/` folder), strip that
587
+ * prefix so the bundle's logical root is the entrypoint's parent.
588
+ * Bundles already at root pass through unchanged.
589
+ */
590
+ declare function stripCommonPrefix(files: ExtractedFile[]): ExtractedFile[];
591
+ /**
592
+ * Heuristic for the default entrypoint when the caller didn't override:
593
+ * 1. exact `index.html` at root
594
+ * 2. exact `index.htm` at root
595
+ * 3. the first .html / .htm file at root (alphabetical)
596
+ * 4. the first .html / .htm anywhere
597
+ * 5. fall back to the literal 'index.html' so the validator surfaces
598
+ * a meaningful error to the user ("entrypoint not in bundle")
599
+ *
600
+ * Accepts either extracted files (with `data`) or a plain list of
601
+ * relative paths — the MCP `commit_static_post` flow only has the
602
+ * S3 ListObjects result to work with, no bytes.
603
+ */
604
+ declare function pickDefaultEntrypoint(files: readonly {
605
+ path: string;
606
+ }[]): string;
607
+
554
608
  type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
555
609
  /**
556
610
  * Single entry in a `linkList` field. Stored as part of a JSON array
@@ -653,9 +707,7 @@ declare function defineTheme(m: ThemeManifest): ThemeManifest;
653
707
  */
654
708
  declare function resolveLocalized(value: LocalizedString | undefined, locale: string, fallback?: string): string;
655
709
  interface ThemeRouteContext<P = Record<string, string>> {
656
- params: Promise<P & {
657
- siteId: string;
658
- }>;
710
+ params: Promise<P>;
659
711
  }
660
712
  interface ThemeModule {
661
713
  /** Stable identifier — must match the directory name and the value
@@ -663,9 +715,10 @@ interface ThemeModule {
663
715
  name: string;
664
716
  manifest: ThemeManifest;
665
717
  /**
666
- * Server components rendered by the dispatcher routes under
667
- * `app/site/[siteId]/`. Each theme MUST provide Home; Post / Tag are
668
- * recommended but optional (dispatcher 404s when missing).
718
+ * Server components rendered by the dispatcher routes
719
+ * (`app/page.tsx`, `app/[slug]/page.tsx`, `app/tag/[tag]/page.tsx`).
720
+ * Each theme MUST provide Home; Post / Tag are recommended but
721
+ * optional (dispatcher 404s when missing).
669
722
  */
670
723
  components: {
671
724
  Home: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
@@ -695,11 +748,9 @@ interface ThemeModule {
695
748
  */
696
749
  routes?: {
697
750
  feed?: (ctx: {
698
- siteId: string;
699
751
  request: Request;
700
752
  }) => Promise<Response>;
701
753
  sitemap?: (ctx: {
702
- siteId: string;
703
754
  request: Request;
704
755
  }) => Promise<Response>;
705
756
  };
@@ -757,11 +808,11 @@ declare function isTagListUrl(url: string): {
757
808
  /**
758
809
  * Resolve effective values for every manifest field, merging stored
759
810
  * overrides on top of defaults. `stored` is the flat settings map keyed
760
- * by `theme.{key}` — typically the output of `listSiteSettings(siteId)`
811
+ * by `theme.{key}` — typically the output of `listSiteSettings()`
761
812
  * filtered to theme entries.
762
813
  */
763
814
  declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
764
815
 
765
816
  declare const VERSION = "0.0.1";
766
817
 
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 };
818
+ export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, 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.10",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",