@ampless/runtime 1.0.0-alpha.21 → 1.0.0-alpha.22

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.
@@ -0,0 +1,123 @@
1
+ // src/stream-s3.ts
2
+ import { getProperties } from "aws-amplify/storage/server";
3
+ import { cookies } from "next/headers";
4
+ var DEFAULT_THRESHOLD_BYTES = 6 * 1024 * 1024;
5
+ var META_CACHE_MAX = 1e3;
6
+ var META_CACHE_TTL_MS = 5 * 6e4;
7
+ var META_CACHE = /* @__PURE__ */ new Map();
8
+ function metaCacheGet(key) {
9
+ const hit = META_CACHE.get(key);
10
+ if (!hit) return void 0;
11
+ if (hit.expires < Date.now()) {
12
+ META_CACHE.delete(key);
13
+ return void 0;
14
+ }
15
+ META_CACHE.delete(key);
16
+ META_CACHE.set(key, hit);
17
+ return hit.meta;
18
+ }
19
+ function metaCacheSet(key, meta) {
20
+ if (META_CACHE.size >= META_CACHE_MAX) {
21
+ const oldest = META_CACHE.keys().next().value;
22
+ if (oldest !== void 0) META_CACHE.delete(oldest);
23
+ }
24
+ META_CACHE.set(key, { meta, expires: Date.now() + META_CACHE_TTL_MS });
25
+ }
26
+ function _resetStreamS3Cache() {
27
+ META_CACHE.clear();
28
+ }
29
+ async function headViaAmplify(ctx, key) {
30
+ try {
31
+ const props = await getProperties(ctx, { path: key });
32
+ const size = typeof props.size === "number" ? props.size : 0;
33
+ const mimeType = props.contentType ?? "application/octet-stream";
34
+ const etag = props.eTag;
35
+ return { size, mimeType, etag };
36
+ } catch (err) {
37
+ console.error(`[stream-s3] getProperties failed for ${key}:`, err);
38
+ return null;
39
+ }
40
+ }
41
+ async function streamS3Object(amplifyServerContext, key, options) {
42
+ const threshold = options.thresholdBytes ?? DEFAULT_THRESHOLD_BYTES;
43
+ let meta = options.meta ?? null;
44
+ if (!meta) {
45
+ const cached = metaCacheGet(key);
46
+ if (cached) {
47
+ meta = cached;
48
+ } else if (options.headFallback) {
49
+ try {
50
+ meta = await options.headFallback();
51
+ } catch (err) {
52
+ console.error(`[stream-s3] headFallback threw for ${key}:`, err);
53
+ meta = null;
54
+ }
55
+ if (meta) metaCacheSet(key, meta);
56
+ }
57
+ }
58
+ if (!meta) {
59
+ const probed = await headViaAmplify(amplifyServerContext, key);
60
+ if (probed) {
61
+ metaCacheSet(key, probed);
62
+ meta = probed;
63
+ }
64
+ }
65
+ if (!meta) {
66
+ return new Response("Not Found", { status: 404 });
67
+ }
68
+ if (meta.size > threshold) {
69
+ const url = await options.presignedUrlFor(key).catch((err) => {
70
+ console.error(`[stream-s3] presign failed for ${key}:`, err);
71
+ return null;
72
+ });
73
+ if (!url) return new Response("Not Found", { status: 404 });
74
+ const headers2 = new Headers({ Location: url });
75
+ if (options.cacheControl) headers2.set("Cache-Control", options.cacheControl);
76
+ return new Response(null, { status: 302, headers: headers2 });
77
+ }
78
+ const presigned = await options.presignedUrlFor(key).catch((err) => {
79
+ console.error(`[stream-s3] presign failed for ${key}:`, err);
80
+ return null;
81
+ });
82
+ if (!presigned) return new Response("Not Found", { status: 404 });
83
+ let upstream;
84
+ try {
85
+ upstream = await fetch(presigned);
86
+ } catch (err) {
87
+ console.error(`[stream-s3] upstream fetch failed for ${key}:`, err);
88
+ return new Response("Bad Gateway", { status: 502 });
89
+ }
90
+ if (upstream.status === 404 || upstream.status === 403) {
91
+ return new Response("Not Found", { status: 404 });
92
+ }
93
+ if (!upstream.ok || !upstream.body) {
94
+ console.error(
95
+ `[stream-s3] upstream returned ${upstream.status} for ${key}`
96
+ );
97
+ return new Response("Bad Gateway", { status: 502 });
98
+ }
99
+ const headers = new Headers();
100
+ headers.set("Content-Type", meta.mimeType);
101
+ const contentLength = upstream.headers.get("Content-Length");
102
+ if (contentLength) {
103
+ headers.set("Content-Length", contentLength);
104
+ } else {
105
+ headers.set("Content-Length", String(meta.size));
106
+ }
107
+ const upstreamEtag = upstream.headers.get("ETag") ?? meta.etag;
108
+ if (upstreamEtag) headers.set("ETag", upstreamEtag);
109
+ if (options.cacheControl) headers.set("Cache-Control", options.cacheControl);
110
+ return new Response(upstream.body, { status: 200, headers });
111
+ }
112
+ async function streamS3ObjectWithRunner(runWithAmplifyServerContext, key, options) {
113
+ return runWithAmplifyServerContext({
114
+ nextServerContext: { cookies },
115
+ operation: (ctx) => streamS3Object(ctx, key, options)
116
+ });
117
+ }
118
+
119
+ export {
120
+ _resetStreamS3Cache,
121
+ streamS3Object,
122
+ streamS3ObjectWithRunner
123
+ };
@@ -2,6 +2,8 @@ import { ReactNode } from 'react';
2
2
  import { Metadata } from 'next';
3
3
  import { Ampless } from '../index.js';
4
4
  import 'ampless';
5
+ import 'aws-amplify/storage/server';
6
+ import 'next/headers';
5
7
 
6
8
  interface Props$2 {
7
9
  params: Promise<Record<string, never>>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { Post, ThemeModule, ThemeManifest, Config } from 'ampless';
1
+ import { Post, MediaMetadata, ThemeModule, ThemeManifest, Config } from 'ampless';
2
2
  export { Config, Post, ThemeManifest } from 'ampless';
3
3
  import { Metadata } from 'next';
4
+ import { getProperties } from 'aws-amplify/storage/server';
5
+ import { cookies } from 'next/headers';
4
6
 
5
7
  interface StorageOutput {
6
8
  bucket_name: string;
@@ -68,6 +70,30 @@ interface PostsApi {
68
70
  listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
69
71
  }
70
72
 
73
+ /** Public projection returned by the `getMediaBySrc` custom query. */
74
+ interface PublicMediaShape {
75
+ src: string;
76
+ size?: number | null;
77
+ mimeType?: string | null;
78
+ metadata?: unknown;
79
+ }
80
+ /** Decoded form handed to callers. */
81
+ interface ResolvedMedia {
82
+ src: string;
83
+ size: number | null;
84
+ mimeType: string | null;
85
+ metadata: MediaMetadata | null;
86
+ }
87
+ interface MediaApi {
88
+ /**
89
+ * Resolve the Media DynamoDB row for the given S3 key. Returns
90
+ * `null` when no row matches (orphan / legacy asset) or when the
91
+ * AppSync call fails — the caller should treat both the same and
92
+ * fall back to a HEAD lookup. Failures are logged.
93
+ */
94
+ getMediaBySrc(src: string): Promise<ResolvedMedia | null>;
95
+ }
96
+
71
97
  interface EffectiveSiteSettings {
72
98
  site: {
73
99
  name: string;
@@ -214,6 +240,75 @@ declare function tiptapToMarkdown(doc: unknown): string;
214
240
  */
215
241
  declare function htmlToMarkdown(html: string): string;
216
242
 
243
+ type AmplifyServerContext = Parameters<typeof getProperties>[0];
244
+ /** Metadata sufficient to choose between stream-back and 302 fallback. */
245
+ interface ResolvedAssetMeta {
246
+ size: number;
247
+ mimeType: string;
248
+ /** S3 ETag (when known). Passed through to the response so clients can revalidate. */
249
+ etag?: string;
250
+ }
251
+ interface StreamS3Options {
252
+ /**
253
+ * Override the Cache-Control header on the streamed response.
254
+ * Leave undefined to let the route's middleware overlay
255
+ * (`computeCacheControl`) decide — that's the right answer for
256
+ * static-post asset bytes, since the strategy lives on
257
+ * `post.metadata.cache`. Media uploads pass an immutable header
258
+ * because their S3 keys carry a timestamp prefix.
259
+ */
260
+ cacheControl?: string;
261
+ /** Default 6 MiB — matches Amplify SSR Lambda's buffered response cap. */
262
+ thresholdBytes?: number;
263
+ /**
264
+ * Caller-supplied metadata (from DynamoDB). Preferred path: when
265
+ * present, no HEAD round-trip is issued.
266
+ */
267
+ meta?: ResolvedAssetMeta;
268
+ /**
269
+ * Caller-provided HEAD substitute. Invoked only when `meta` is
270
+ * omitted; result is memoised in the in-process LRU so repeat
271
+ * misses don't all hit S3.
272
+ */
273
+ headFallback?: () => Promise<ResolvedAssetMeta | null>;
274
+ /**
275
+ * Required for the 302 fallback path (files larger than the
276
+ * threshold, or stream-fetch errors). Returns a presigned URL or
277
+ * null when the object cannot be signed.
278
+ */
279
+ presignedUrlFor: (key: string) => Promise<string | null>;
280
+ }
281
+ /**
282
+ * Exported for tests. Production callers should never need to
283
+ * touch the module-scope cache directly.
284
+ */
285
+ declare function _resetStreamS3Cache(): void;
286
+ /**
287
+ * Stream the S3 object at `key` back through the Lambda response.
288
+ * Returns a `Response` whose body is the object bytes (for small
289
+ * files) or a 302 redirect to a presigned URL (for files larger
290
+ * than `thresholdBytes`). Missing objects → 404.
291
+ *
292
+ * The Amplify server context is required so the helper can mint a
293
+ * presigned URL through the same auth context the caller already
294
+ * uses (Cognito identity / public role). No extra IAM grant is
295
+ * needed on the Lambda execution role.
296
+ */
297
+ declare function streamS3Object(amplifyServerContext: AmplifyServerContext, key: string, options: StreamS3Options): Promise<Response>;
298
+ /**
299
+ * Convenience wrapper that pulls in the standard Amplify SSR cookie
300
+ * adapter so route handlers don't have to duplicate the
301
+ * `runWithAmplifyServerContext` plumbing. Pass the runner from
302
+ * `createServerRunner({ config: outputs })` and the rest is the same
303
+ * shape as `streamS3Object`.
304
+ */
305
+ declare function streamS3ObjectWithRunner(runWithAmplifyServerContext: <T>(args: {
306
+ nextServerContext: {
307
+ cookies: typeof cookies;
308
+ };
309
+ operation: (ctx: AmplifyServerContext) => Promise<T>;
310
+ }) => Promise<T>, key: string, options: StreamS3Options): Promise<Response>;
311
+
217
312
  interface CreateAmplessOpts {
218
313
  outputs: AmplessOutputs;
219
314
  cmsConfig: Config;
@@ -223,6 +318,12 @@ interface Ampless {
223
318
  listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
224
319
  getPublishedPost(slug: string): Promise<Post | null>;
225
320
  listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
321
+ /**
322
+ * Look up the Media DynamoDB row by S3 key. Returns `null` for
323
+ * orphan / legacy assets (which the media-proxy route falls back
324
+ * to a HEAD lookup for). Failures are logged and surface as null.
325
+ */
326
+ getMediaBySrc(src: string): Promise<ResolvedMedia | null>;
226
327
  loadSiteSettings(): Promise<EffectiveSiteSettings>;
227
328
  resolveActiveTheme(): Promise<ResolvedTheme>;
228
329
  /**
@@ -243,6 +344,7 @@ interface Ampless {
243
344
  readonly cmsConfig: Config;
244
345
  readonly themes: ThemesRegistry;
245
346
  readonly posts: PostsApi;
347
+ readonly media: MediaApi;
246
348
  readonly settings: SiteSettingsApi;
247
349
  readonly seo: SeoApi;
248
350
  readonly themeActive: ThemeActiveApi;
@@ -258,4 +360,4 @@ interface Ampless {
258
360
  */
259
361
  declare function createAmpless(opts: CreateAmplessOpts): Ampless;
260
362
 
261
- export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type PostsApi, type PublicPostConnectionShape, type PublicPostShape, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, createAmpless, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
363
+ export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ import {
2
+ _resetStreamS3Cache,
3
+ streamS3Object,
4
+ streamS3ObjectWithRunner
5
+ } from "./chunk-5BLMLMZU.js";
6
+
1
7
  // src/storage.ts
2
8
  import { formatPublicAssetUrl } from "ampless";
3
9
  function createStorage(outputs) {
@@ -81,6 +87,50 @@ function createPostsApi(outputs) {
81
87
  };
82
88
  }
83
89
 
90
+ // src/media.ts
91
+ import { cookies as cookies2 } from "next/headers";
92
+ import { generateServerClientUsingCookies as generateServerClientUsingCookies2 } from "@aws-amplify/adapter-nextjs/api";
93
+ import { decodeAwsJson as decodeAwsJson2 } from "ampless";
94
+ function decodeMediaMetadata(value) {
95
+ if (value === null || value === void 0) return null;
96
+ const parsed = decodeAwsJson2(value);
97
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
98
+ return parsed;
99
+ }
100
+ return null;
101
+ }
102
+ function createMediaApi(outputs) {
103
+ const client = generateServerClientUsingCookies2({
104
+ config: outputs,
105
+ cookies: cookies2,
106
+ authMode: "apiKey"
107
+ });
108
+ return {
109
+ async getMediaBySrc(src) {
110
+ try {
111
+ const { data, errors } = await client.queries.getMediaBySrc({ src });
112
+ if (errors && errors.length > 0) {
113
+ console.error(
114
+ `[media-api] getMediaBySrc errors for ${src}`,
115
+ errors.map((e) => e.message)
116
+ );
117
+ return null;
118
+ }
119
+ if (!data) return null;
120
+ return {
121
+ src: data.src,
122
+ size: data.size ?? null,
123
+ mimeType: data.mimeType ?? null,
124
+ metadata: decodeMediaMetadata(data.metadata)
125
+ };
126
+ } catch (err) {
127
+ console.error(`[media-api] getMediaBySrc threw for ${src}`, err);
128
+ return null;
129
+ }
130
+ }
131
+ };
132
+ }
133
+
84
134
  // src/site-settings.ts
85
135
  import { unflattenSettings } from "ampless";
86
136
  function createSiteSettings(cmsConfig, storage) {
@@ -604,6 +654,7 @@ function createAmpless(opts) {
604
654
  const { outputs, cmsConfig, themes } = opts;
605
655
  const storage = createStorage(outputs);
606
656
  const posts = createPostsApi(outputs);
657
+ const media = createMediaApi(outputs);
607
658
  const settings = createSiteSettings(cmsConfig, storage);
608
659
  const seo = createSeo(cmsConfig, settings);
609
660
  const themeActive = createThemeActive(themes, storage);
@@ -612,6 +663,7 @@ function createAmpless(opts) {
612
663
  listPublishedPosts: (o) => posts.listPublishedPosts(o),
613
664
  getPublishedPost: (slug) => posts.getPublishedPost(slug),
614
665
  listPostsByTag: (tag, o) => posts.listPostsByTag(tag, o),
666
+ getMediaBySrc: (src) => media.getMediaBySrc(src),
615
667
  loadSiteSettings: () => settings.loadSiteSettings(),
616
668
  resolveActiveTheme: () => themeActive.resolveActiveTheme(),
617
669
  readStoredActiveThemeFresh: () => themeActive.readStoredActiveThemeFresh(),
@@ -626,6 +678,7 @@ function createAmpless(opts) {
626
678
  cmsConfig,
627
679
  themes,
628
680
  posts,
681
+ media,
629
682
  settings,
630
683
  seo,
631
684
  themeActive,
@@ -636,11 +689,14 @@ function createAmpless(opts) {
636
689
  export {
637
690
  COLOR_SCHEME_SETTING_KEY,
638
691
  DEFAULT_COLOR_SCHEME,
692
+ _resetStreamS3Cache,
639
693
  createAmpless,
640
694
  htmlToMarkdown,
641
695
  markdownToHtml,
642
696
  renderBody,
643
697
  renderThemeCss,
698
+ streamS3Object,
699
+ streamS3ObjectWithRunner,
644
700
  tiptapToHtml,
645
701
  tiptapToMarkdown,
646
702
  validateColorScheme
@@ -1,6 +1,8 @@
1
1
  import { Ampless } from '../index.js';
2
2
  import 'ampless';
3
3
  import 'next';
4
+ import 'aws-amplify/storage/server';
5
+ import 'next/headers';
4
6
 
5
7
  interface Ctx$4 {
6
8
  params: Promise<{
@@ -84,15 +86,13 @@ type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
84
86
  * the bundle resolve correctly — `<img src="img.png">` must resolve
85
87
  * to `/<slug>/img.png`, not the site root.
86
88
  *
87
- * Trust model: assets are served via short-lived S3 presigned URLs;
88
- * the bucket stays private.
89
+ * Trust model: the bucket stays private. Small files (<=6 MB) are
90
+ * streamed back through the Lambda response so CloudFront caches
91
+ * them; larger files fall back to a short-lived S3 presigned redirect.
89
92
  *
90
93
  * Cache-Control: deliberately omitted from the responses here.
91
94
  * Middleware computes the strategy from `metadata.cache` +
92
- * `post.updatedAt` and sets it on the rewritten response. The 302
93
- * presigned redirects also rely on S3's own headers for the actual
94
- * asset bytes — adding a stale-while-revalidate window on the 302
95
- * would risk serving an expired presign to repeat visitors.
95
+ * `post.updatedAt` and overlays it on the rewritten response.
96
96
  */
97
97
  declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
98
98
 
@@ -1,3 +1,7 @@
1
+ import {
2
+ streamS3Object
3
+ } from "../chunk-5BLMLMZU.js";
4
+
1
5
  // src/routes/og.ts
2
6
  import { ImageResponse } from "next/og";
3
7
  import { loadImageForOg } from "@ampless/plugin-og-image/load-image";
@@ -104,6 +108,8 @@ function createStaticRouteHandler(ampless) {
104
108
  if (post.format !== "static") {
105
109
  return new Response("Not Found", { status: 404 });
106
110
  }
111
+ const metadata = post.metadata;
112
+ const filesMeta = metadata && typeof metadata.files === "object" && metadata.files !== null ? metadata.files : void 0;
107
113
  if (restSegments.length === 0) {
108
114
  const url = new URL(request.url);
109
115
  if (!url.pathname.endsWith("/")) {
@@ -113,15 +119,12 @@ function createStaticRouteHandler(ampless) {
113
119
  }
114
120
  const body2 = post.body ?? null;
115
121
  const entrypoint = typeof body2?.entrypoint === "string" && body2.entrypoint ? body2.entrypoint : "index.html";
116
- const presignedUrl2 = await signStaticAsset({
122
+ return serveStaticAsset({
117
123
  runWithAmplifyServerContext,
118
124
  slug,
119
- rest: entrypoint
125
+ rest: entrypoint,
126
+ filesMeta
120
127
  });
121
- if (!presignedUrl2) {
122
- return new Response("Not Found", { status: 404 });
123
- }
124
- return Response.redirect(presignedUrl2, 302);
125
128
  }
126
129
  if (restSegments.some(
127
130
  (s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
@@ -134,39 +137,49 @@ function createStaticRouteHandler(ampless) {
134
137
  if (fileList.length > 0 && !fileList.includes(rest)) {
135
138
  return new Response("Not Found", { status: 404 });
136
139
  }
137
- const presignedUrl = await signStaticAsset({
140
+ return serveStaticAsset({
138
141
  runWithAmplifyServerContext,
139
142
  slug,
140
- rest
143
+ rest,
144
+ filesMeta
141
145
  });
142
- if (!presignedUrl) {
143
- return new Response("Not Found", { status: 404 });
144
- }
145
- return Response.redirect(presignedUrl, 302);
146
146
  };
147
147
  }
148
- async function signStaticAsset({
148
+ async function serveStaticAsset({
149
149
  runWithAmplifyServerContext,
150
150
  slug,
151
- rest
151
+ rest,
152
+ filesMeta
153
+ }) {
154
+ const key = `public/static/${slug}/${rest}`;
155
+ const persisted = filesMeta?.[rest];
156
+ const meta = persisted ? { size: persisted.size, mimeType: persisted.mimeType } : void 0;
157
+ const opts = {
158
+ meta,
159
+ presignedUrlFor: (k) => signStaticAsset({ runWithAmplifyServerContext, key: k })
160
+ };
161
+ return runWithAmplifyServerContext({
162
+ nextServerContext: { cookies },
163
+ operation: (ctx) => streamS3Object(ctx, key, opts)
164
+ });
165
+ }
166
+ async function signStaticAsset({
167
+ runWithAmplifyServerContext,
168
+ key
152
169
  }) {
153
- const objectPath = `public/static/${slug}/${rest}`;
154
170
  try {
155
171
  return await runWithAmplifyServerContext({
156
172
  nextServerContext: { cookies },
157
173
  operation: async (amplifyContext) => {
158
174
  const result = await getUrl(amplifyContext, {
159
- path: objectPath,
175
+ path: key,
160
176
  options: { expiresIn: 60 * 60 }
161
177
  });
162
178
  return result.url.toString();
163
179
  }
164
180
  });
165
181
  } catch (err) {
166
- console.error(
167
- `[static-route] presign failed for ${objectPath}:`,
168
- err
169
- );
182
+ console.error(`[static-route] presign failed for ${key}:`, err);
170
183
  return null;
171
184
  }
172
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.22",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,8 +49,8 @@
49
49
  "lucide-react": "^1.16.0",
50
50
  "marked": "^18.0.4",
51
51
  "tailwind-merge": "^3.6.0",
52
- "ampless": "1.0.0-alpha.15",
53
- "@ampless/plugin-og-image": "0.2.0-alpha.15"
52
+ "ampless": "1.0.0-alpha.16",
53
+ "@ampless/plugin-og-image": "0.2.0-alpha.16"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "next": "^15 || ^16",