@ampless/runtime 1.0.0-alpha.21 → 1.0.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5BLMLMZU.js +123 -0
- package/dist/dispatchers/index.d.ts +2 -0
- package/dist/index.d.ts +122 -2
- package/dist/index.js +336 -1
- package/dist/routes/index.d.ts +7 -6
- package/dist/routes/index.js +33 -20
- package/package.json +3 -3
|
@@ -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
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { Post, ThemeModule, ThemeManifest
|
|
1
|
+
import { Post, MediaMetadata, Config, ThemeModule, ThemeManifest } from 'ampless';
|
|
2
2
|
export { Config, Post, ThemeManifest } from 'ampless';
|
|
3
3
|
import { Metadata } from 'next';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
import { getProperties } from 'aws-amplify/storage/server';
|
|
6
|
+
import { cookies } from 'next/headers';
|
|
4
7
|
|
|
5
8
|
interface StorageOutput {
|
|
6
9
|
bucket_name: string;
|
|
@@ -68,6 +71,30 @@ interface PostsApi {
|
|
|
68
71
|
listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/** Public projection returned by the `getMediaBySrc` custom query. */
|
|
75
|
+
interface PublicMediaShape {
|
|
76
|
+
src: string;
|
|
77
|
+
size?: number | null;
|
|
78
|
+
mimeType?: string | null;
|
|
79
|
+
metadata?: unknown;
|
|
80
|
+
}
|
|
81
|
+
/** Decoded form handed to callers. */
|
|
82
|
+
interface ResolvedMedia {
|
|
83
|
+
src: string;
|
|
84
|
+
size: number | null;
|
|
85
|
+
mimeType: string | null;
|
|
86
|
+
metadata: MediaMetadata | null;
|
|
87
|
+
}
|
|
88
|
+
interface MediaApi {
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the Media DynamoDB row for the given S3 key. Returns
|
|
91
|
+
* `null` when no row matches (orphan / legacy asset) or when the
|
|
92
|
+
* AppSync call fails — the caller should treat both the same and
|
|
93
|
+
* fall back to a HEAD lookup. Failures are logged.
|
|
94
|
+
*/
|
|
95
|
+
getMediaBySrc(src: string): Promise<ResolvedMedia | null>;
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
interface EffectiveSiteSettings {
|
|
72
99
|
site: {
|
|
73
100
|
name: string;
|
|
@@ -96,6 +123,20 @@ interface SeoApi {
|
|
|
96
123
|
siteMetadata(): Promise<Metadata>;
|
|
97
124
|
}
|
|
98
125
|
|
|
126
|
+
interface PluginHeadApi {
|
|
127
|
+
/** React children safe to drop into `<head>`. */
|
|
128
|
+
renderHead(): ReactNode;
|
|
129
|
+
/** React children safe to drop just before `</body>`. */
|
|
130
|
+
renderBodyEnd(): ReactNode;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create the head/body renderer for a `Config`. The constructor-time
|
|
134
|
+
* pass logs a single dev warning when two plugins share an
|
|
135
|
+
* `instanceId ?? name`; everything else happens at render time so
|
|
136
|
+
* descriptors reflect per-request site config.
|
|
137
|
+
*/
|
|
138
|
+
declare function createPluginHead(cmsConfig: Config): PluginHeadApi;
|
|
139
|
+
|
|
99
140
|
interface ThemesRegistry {
|
|
100
141
|
/** Map of theme name → loaded theme module. */
|
|
101
142
|
themes: Record<string, ThemeModule>;
|
|
@@ -214,6 +255,75 @@ declare function tiptapToMarkdown(doc: unknown): string;
|
|
|
214
255
|
*/
|
|
215
256
|
declare function htmlToMarkdown(html: string): string;
|
|
216
257
|
|
|
258
|
+
type AmplifyServerContext = Parameters<typeof getProperties>[0];
|
|
259
|
+
/** Metadata sufficient to choose between stream-back and 302 fallback. */
|
|
260
|
+
interface ResolvedAssetMeta {
|
|
261
|
+
size: number;
|
|
262
|
+
mimeType: string;
|
|
263
|
+
/** S3 ETag (when known). Passed through to the response so clients can revalidate. */
|
|
264
|
+
etag?: string;
|
|
265
|
+
}
|
|
266
|
+
interface StreamS3Options {
|
|
267
|
+
/**
|
|
268
|
+
* Override the Cache-Control header on the streamed response.
|
|
269
|
+
* Leave undefined to let the route's middleware overlay
|
|
270
|
+
* (`computeCacheControl`) decide — that's the right answer for
|
|
271
|
+
* static-post asset bytes, since the strategy lives on
|
|
272
|
+
* `post.metadata.cache`. Media uploads pass an immutable header
|
|
273
|
+
* because their S3 keys carry a timestamp prefix.
|
|
274
|
+
*/
|
|
275
|
+
cacheControl?: string;
|
|
276
|
+
/** Default 6 MiB — matches Amplify SSR Lambda's buffered response cap. */
|
|
277
|
+
thresholdBytes?: number;
|
|
278
|
+
/**
|
|
279
|
+
* Caller-supplied metadata (from DynamoDB). Preferred path: when
|
|
280
|
+
* present, no HEAD round-trip is issued.
|
|
281
|
+
*/
|
|
282
|
+
meta?: ResolvedAssetMeta;
|
|
283
|
+
/**
|
|
284
|
+
* Caller-provided HEAD substitute. Invoked only when `meta` is
|
|
285
|
+
* omitted; result is memoised in the in-process LRU so repeat
|
|
286
|
+
* misses don't all hit S3.
|
|
287
|
+
*/
|
|
288
|
+
headFallback?: () => Promise<ResolvedAssetMeta | null>;
|
|
289
|
+
/**
|
|
290
|
+
* Required for the 302 fallback path (files larger than the
|
|
291
|
+
* threshold, or stream-fetch errors). Returns a presigned URL or
|
|
292
|
+
* null when the object cannot be signed.
|
|
293
|
+
*/
|
|
294
|
+
presignedUrlFor: (key: string) => Promise<string | null>;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Exported for tests. Production callers should never need to
|
|
298
|
+
* touch the module-scope cache directly.
|
|
299
|
+
*/
|
|
300
|
+
declare function _resetStreamS3Cache(): void;
|
|
301
|
+
/**
|
|
302
|
+
* Stream the S3 object at `key` back through the Lambda response.
|
|
303
|
+
* Returns a `Response` whose body is the object bytes (for small
|
|
304
|
+
* files) or a 302 redirect to a presigned URL (for files larger
|
|
305
|
+
* than `thresholdBytes`). Missing objects → 404.
|
|
306
|
+
*
|
|
307
|
+
* The Amplify server context is required so the helper can mint a
|
|
308
|
+
* presigned URL through the same auth context the caller already
|
|
309
|
+
* uses (Cognito identity / public role). No extra IAM grant is
|
|
310
|
+
* needed on the Lambda execution role.
|
|
311
|
+
*/
|
|
312
|
+
declare function streamS3Object(amplifyServerContext: AmplifyServerContext, key: string, options: StreamS3Options): Promise<Response>;
|
|
313
|
+
/**
|
|
314
|
+
* Convenience wrapper that pulls in the standard Amplify SSR cookie
|
|
315
|
+
* adapter so route handlers don't have to duplicate the
|
|
316
|
+
* `runWithAmplifyServerContext` plumbing. Pass the runner from
|
|
317
|
+
* `createServerRunner({ config: outputs })` and the rest is the same
|
|
318
|
+
* shape as `streamS3Object`.
|
|
319
|
+
*/
|
|
320
|
+
declare function streamS3ObjectWithRunner(runWithAmplifyServerContext: <T>(args: {
|
|
321
|
+
nextServerContext: {
|
|
322
|
+
cookies: typeof cookies;
|
|
323
|
+
};
|
|
324
|
+
operation: (ctx: AmplifyServerContext) => Promise<T>;
|
|
325
|
+
}) => Promise<T>, key: string, options: StreamS3Options): Promise<Response>;
|
|
326
|
+
|
|
217
327
|
interface CreateAmplessOpts {
|
|
218
328
|
outputs: AmplessOutputs;
|
|
219
329
|
cmsConfig: Config;
|
|
@@ -223,6 +333,12 @@ interface Ampless {
|
|
|
223
333
|
listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
|
|
224
334
|
getPublishedPost(slug: string): Promise<Post | null>;
|
|
225
335
|
listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
|
|
336
|
+
/**
|
|
337
|
+
* Look up the Media DynamoDB row by S3 key. Returns `null` for
|
|
338
|
+
* orphan / legacy assets (which the media-proxy route falls back
|
|
339
|
+
* to a HEAD lookup for). Failures are logged and surface as null.
|
|
340
|
+
*/
|
|
341
|
+
getMediaBySrc(src: string): Promise<ResolvedMedia | null>;
|
|
226
342
|
loadSiteSettings(): Promise<EffectiveSiteSettings>;
|
|
227
343
|
resolveActiveTheme(): Promise<ResolvedTheme>;
|
|
228
344
|
/**
|
|
@@ -235,6 +351,8 @@ interface Ampless {
|
|
|
235
351
|
loadThemeConfig(): Promise<EffectiveThemeConfig>;
|
|
236
352
|
postMetadata(post: Post): Promise<Metadata>;
|
|
237
353
|
siteMetadata(): Promise<Metadata>;
|
|
354
|
+
publicHead(): ReactNode;
|
|
355
|
+
publicBodyEnd(): ReactNode;
|
|
238
356
|
renderBody(post: Post): string;
|
|
239
357
|
renderThemeCss(cssVars: Record<string, string>): string;
|
|
240
358
|
publicAssetUrl(key: string): string;
|
|
@@ -243,11 +361,13 @@ interface Ampless {
|
|
|
243
361
|
readonly cmsConfig: Config;
|
|
244
362
|
readonly themes: ThemesRegistry;
|
|
245
363
|
readonly posts: PostsApi;
|
|
364
|
+
readonly media: MediaApi;
|
|
246
365
|
readonly settings: SiteSettingsApi;
|
|
247
366
|
readonly seo: SeoApi;
|
|
248
367
|
readonly themeActive: ThemeActiveApi;
|
|
249
368
|
readonly themeConfig: ThemeConfigApi;
|
|
250
369
|
readonly storageApi: StorageApi;
|
|
370
|
+
readonly pluginHead: PluginHeadApi;
|
|
251
371
|
}
|
|
252
372
|
/**
|
|
253
373
|
* Wire up the ampless runtime from user-supplied config blobs. The
|
|
@@ -258,4 +378,4 @@ interface Ampless {
|
|
|
258
378
|
*/
|
|
259
379
|
declare function createAmpless(opts: CreateAmplessOpts): Ampless;
|
|
260
380
|
|
|
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 };
|
|
381
|
+
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 PluginHeadApi, 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, createPluginHead, 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) {
|
|
@@ -175,6 +225,280 @@ function createSeo(cmsConfig, settingsApi) {
|
|
|
175
225
|
};
|
|
176
226
|
}
|
|
177
227
|
|
|
228
|
+
// src/plugin-head.ts
|
|
229
|
+
import {
|
|
230
|
+
Fragment,
|
|
231
|
+
createElement
|
|
232
|
+
} from "react";
|
|
233
|
+
function isPlugin2(p) {
|
|
234
|
+
return typeof p === "object" && p !== null && "apiVersion" in p;
|
|
235
|
+
}
|
|
236
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set([
|
|
237
|
+
"crossorigin",
|
|
238
|
+
"referrerpolicy",
|
|
239
|
+
"integrity",
|
|
240
|
+
"fetchpriority",
|
|
241
|
+
// `nonce` is intentionally NOT in the allowlist for Phase 1. CSP
|
|
242
|
+
// nonce propagation is scoped out of Phase 1 (see spec §7); attrs
|
|
243
|
+
// shouldn't let plugins smuggle nonces past the design decision.
|
|
244
|
+
// The CSP nonce RFP will reintroduce it through `cspNonce` on
|
|
245
|
+
// PluginPublicRenderContext + `inlineScript.nonce: 'auto'`, not via
|
|
246
|
+
// the `attrs` bag.
|
|
247
|
+
"loading",
|
|
248
|
+
// iframe lazy-loading
|
|
249
|
+
"sandbox",
|
|
250
|
+
// iframe sandbox attribute
|
|
251
|
+
"allow",
|
|
252
|
+
// iframe permissions policy
|
|
253
|
+
"allowfullscreen"
|
|
254
|
+
// iframe fullscreen
|
|
255
|
+
]);
|
|
256
|
+
function isAllowedAttr(name) {
|
|
257
|
+
if (name.startsWith("data-")) return true;
|
|
258
|
+
return ALLOWED_ATTRS.has(name.toLowerCase());
|
|
259
|
+
}
|
|
260
|
+
function isSafeUrl(value) {
|
|
261
|
+
const trimmed = value.trim();
|
|
262
|
+
if (trimmed.length === 0) return false;
|
|
263
|
+
const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
264
|
+
if (!schemeMatch) return true;
|
|
265
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
266
|
+
return scheme === "http" || scheme === "https";
|
|
267
|
+
}
|
|
268
|
+
function isDev() {
|
|
269
|
+
const env = typeof process !== "undefined" && process.env ? process.env.NODE_ENV : void 0;
|
|
270
|
+
return env !== "production";
|
|
271
|
+
}
|
|
272
|
+
function warn(message) {
|
|
273
|
+
if (!isDev()) return;
|
|
274
|
+
console.warn(`[ampless plugin-head] ${message}`);
|
|
275
|
+
}
|
|
276
|
+
function applyAttrs(target, attrs, ownerLabel) {
|
|
277
|
+
if (!attrs) return;
|
|
278
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
279
|
+
if (!isAllowedAttr(k)) {
|
|
280
|
+
warn(
|
|
281
|
+
`${ownerLabel}: attr "${k}" not in allowlist (data-* / crossorigin / referrerpolicy / integrity / fetchpriority / loading / sandbox / allow / allowfullscreen). skipping.`
|
|
282
|
+
);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
target[k] = v;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function renderHeadDescriptor(descriptor, pluginLabel, index) {
|
|
289
|
+
switch (descriptor.type) {
|
|
290
|
+
case "script": {
|
|
291
|
+
if (!isSafeUrl(descriptor.src)) {
|
|
292
|
+
warn(
|
|
293
|
+
`${pluginLabel}: script descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
294
|
+
);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const props = {
|
|
298
|
+
src: descriptor.src
|
|
299
|
+
};
|
|
300
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
301
|
+
const hasAsync = typeof descriptor.async === "boolean";
|
|
302
|
+
const hasDefer = typeof descriptor.defer === "boolean";
|
|
303
|
+
if (hasAsync) props.async = descriptor.async;
|
|
304
|
+
if (hasDefer) props.defer = descriptor.defer;
|
|
305
|
+
if (!hasAsync && !hasDefer) {
|
|
306
|
+
if (descriptor.strategy === "lazyOnload") {
|
|
307
|
+
props.defer = true;
|
|
308
|
+
} else {
|
|
309
|
+
props.async = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
applyAttrs(props, descriptor.attrs, `${pluginLabel} script#${descriptor.id ?? index}`);
|
|
313
|
+
return {
|
|
314
|
+
id: descriptor.id ?? null,
|
|
315
|
+
element: createElement("script", props)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
case "inlineScript": {
|
|
319
|
+
if (!descriptor.id) {
|
|
320
|
+
warn(
|
|
321
|
+
`${pluginLabel}: inlineScript descriptor #${index} dropped \u2014 missing required "id".`
|
|
322
|
+
);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const props = {
|
|
326
|
+
id: descriptor.id,
|
|
327
|
+
dangerouslySetInnerHTML: { __html: descriptor.body }
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
id: descriptor.id,
|
|
331
|
+
element: createElement("script", props)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
case "meta": {
|
|
335
|
+
const props = { content: descriptor.content };
|
|
336
|
+
if (descriptor.name) props.name = descriptor.name;
|
|
337
|
+
if (descriptor.property) props.property = descriptor.property;
|
|
338
|
+
return {
|
|
339
|
+
// meta has no id channel in the descriptor. Don't derive a
|
|
340
|
+
// dedup id from name/property — multiple `<meta name=...>`
|
|
341
|
+
// entries with the same name are legitimate (e.g. theme-color
|
|
342
|
+
// media variants, and two plugins emitting overlapping names
|
|
343
|
+
// is a real case the runtime shouldn't silently collapse).
|
|
344
|
+
// Position-based React keys handle the stable-key requirement.
|
|
345
|
+
id: null,
|
|
346
|
+
element: createElement("meta", props)
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
case "link": {
|
|
350
|
+
if (!isSafeUrl(descriptor.href)) {
|
|
351
|
+
warn(
|
|
352
|
+
`${pluginLabel}: link descriptor #${index} dropped \u2014 unsafe href "${descriptor.href}".`
|
|
353
|
+
);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const props = {
|
|
357
|
+
rel: descriptor.rel,
|
|
358
|
+
href: descriptor.href
|
|
359
|
+
};
|
|
360
|
+
if (descriptor.as) props.as = descriptor.as;
|
|
361
|
+
if (descriptor.typeAttr) props.type = descriptor.typeAttr;
|
|
362
|
+
return {
|
|
363
|
+
id: null,
|
|
364
|
+
element: createElement("link", props)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
case "noscript": {
|
|
368
|
+
const props = {
|
|
369
|
+
dangerouslySetInnerHTML: { __html: descriptor.html }
|
|
370
|
+
};
|
|
371
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
372
|
+
return {
|
|
373
|
+
id: descriptor.id ?? null,
|
|
374
|
+
element: createElement("noscript", props)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function renderBodyDescriptor(descriptor, pluginLabel, index) {
|
|
380
|
+
if (descriptor.type === "iframe") {
|
|
381
|
+
if (!isSafeUrl(descriptor.src)) {
|
|
382
|
+
warn(
|
|
383
|
+
`${pluginLabel}: iframe descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
384
|
+
);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const props = {
|
|
388
|
+
src: descriptor.src
|
|
389
|
+
};
|
|
390
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
391
|
+
if (descriptor.title) props.title = descriptor.title;
|
|
392
|
+
if (typeof descriptor.width === "number") props.width = descriptor.width;
|
|
393
|
+
if (typeof descriptor.height === "number") props.height = descriptor.height;
|
|
394
|
+
applyAttrs(props, descriptor.attrs, `${pluginLabel} iframe#${descriptor.id ?? index}`);
|
|
395
|
+
return {
|
|
396
|
+
id: descriptor.id ?? null,
|
|
397
|
+
element: createElement("iframe", props)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return renderHeadDescriptor(descriptor, pluginLabel, index);
|
|
401
|
+
}
|
|
402
|
+
function dedupeAndKey(entries) {
|
|
403
|
+
const lastIndexById = /* @__PURE__ */ new Map();
|
|
404
|
+
for (let i = 0; i < entries.length; i++) {
|
|
405
|
+
const id = entries[i].id;
|
|
406
|
+
if (id === null) continue;
|
|
407
|
+
if (lastIndexById.has(id)) {
|
|
408
|
+
warn(`duplicate descriptor id "${id}" \u2014 keeping the last occurrence.`);
|
|
409
|
+
}
|
|
410
|
+
lastIndexById.set(id, i);
|
|
411
|
+
}
|
|
412
|
+
const kept = [];
|
|
413
|
+
for (let i = 0; i < entries.length; i++) {
|
|
414
|
+
const { id, element } = entries[i];
|
|
415
|
+
if (id !== null && lastIndexById.get(id) !== i) continue;
|
|
416
|
+
const key = id ?? `__pos-${i}`;
|
|
417
|
+
const existingProps = element.props;
|
|
418
|
+
kept.push(createElement(element.type, { ...existingProps, key }));
|
|
419
|
+
}
|
|
420
|
+
return kept;
|
|
421
|
+
}
|
|
422
|
+
function collectFor(plugins, ctx, surface, renderOne) {
|
|
423
|
+
const entries = [];
|
|
424
|
+
for (const plugin of plugins) {
|
|
425
|
+
const factory = surface(plugin);
|
|
426
|
+
if (!factory) continue;
|
|
427
|
+
let descriptors;
|
|
428
|
+
try {
|
|
429
|
+
descriptors = factory.call(plugin, ctx) ?? [];
|
|
430
|
+
} catch (err) {
|
|
431
|
+
warn(
|
|
432
|
+
`plugin "${plugin.instanceId ?? plugin.name}" threw inside descriptor callback: ${err instanceof Error ? err.message : String(err)}`
|
|
433
|
+
);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
437
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
438
|
+
const entry = renderOne(descriptors[i], label, i);
|
|
439
|
+
if (entry) entries.push(entry);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (entries.length === 0) return null;
|
|
443
|
+
const keyed = dedupeAndKey(entries);
|
|
444
|
+
return createElement(Fragment, null, ...keyed);
|
|
445
|
+
}
|
|
446
|
+
function createPluginHead(cmsConfig) {
|
|
447
|
+
const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
|
|
448
|
+
const seenNamespaces = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const plugin of plugins) {
|
|
450
|
+
const ns = plugin.instanceId ?? plugin.name;
|
|
451
|
+
const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
|
|
452
|
+
if (seenNamespaces.has(ns)) {
|
|
453
|
+
warn(
|
|
454
|
+
`duplicate plugin namespace "${ns}" detected in cms.config.plugins. Set distinct \`instanceId\` on each instance to disambiguate.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
seenNamespaces.add(ns);
|
|
458
|
+
const caps = plugin.capabilities;
|
|
459
|
+
if (caps) {
|
|
460
|
+
if (caps.includes("publicHead") && !plugin.publicHead) {
|
|
461
|
+
warn(
|
|
462
|
+
`${label}: declares capability "publicHead" but no \`publicHead\` implementation. Drop the capability or add the function.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (caps.includes("publicBody") && !plugin.publicBodyEnd) {
|
|
466
|
+
warn(
|
|
467
|
+
`${label}: declares capability "publicBody" but no \`publicBodyEnd\` implementation. Drop the capability or add the function.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (plugin.publicHead && !caps.includes("publicHead")) {
|
|
471
|
+
warn(
|
|
472
|
+
`${label}: implements \`publicHead\` but "publicHead" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (plugin.publicBodyEnd && !caps.includes("publicBody")) {
|
|
476
|
+
warn(
|
|
477
|
+
`${label}: implements \`publicBodyEnd\` but "publicBody" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
renderHead() {
|
|
484
|
+
return collectFor(
|
|
485
|
+
plugins,
|
|
486
|
+
{ site: cmsConfig.site },
|
|
487
|
+
(p) => p.publicHead,
|
|
488
|
+
renderHeadDescriptor
|
|
489
|
+
);
|
|
490
|
+
},
|
|
491
|
+
renderBodyEnd() {
|
|
492
|
+
return collectFor(
|
|
493
|
+
plugins,
|
|
494
|
+
{ site: cmsConfig.site },
|
|
495
|
+
(p) => p.publicBodyEnd,
|
|
496
|
+
renderBodyDescriptor
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
178
502
|
// src/theme-active.ts
|
|
179
503
|
import { headers } from "next/headers";
|
|
180
504
|
function createThemeActive(registry, storage) {
|
|
@@ -604,20 +928,25 @@ function createAmpless(opts) {
|
|
|
604
928
|
const { outputs, cmsConfig, themes } = opts;
|
|
605
929
|
const storage = createStorage(outputs);
|
|
606
930
|
const posts = createPostsApi(outputs);
|
|
931
|
+
const media = createMediaApi(outputs);
|
|
607
932
|
const settings = createSiteSettings(cmsConfig, storage);
|
|
608
933
|
const seo = createSeo(cmsConfig, settings);
|
|
934
|
+
const pluginHead = createPluginHead(cmsConfig);
|
|
609
935
|
const themeActive = createThemeActive(themes, storage);
|
|
610
936
|
const themeConfig = createThemeConfig(themeActive, storage);
|
|
611
937
|
return {
|
|
612
938
|
listPublishedPosts: (o) => posts.listPublishedPosts(o),
|
|
613
939
|
getPublishedPost: (slug) => posts.getPublishedPost(slug),
|
|
614
940
|
listPostsByTag: (tag, o) => posts.listPostsByTag(tag, o),
|
|
941
|
+
getMediaBySrc: (src) => media.getMediaBySrc(src),
|
|
615
942
|
loadSiteSettings: () => settings.loadSiteSettings(),
|
|
616
943
|
resolveActiveTheme: () => themeActive.resolveActiveTheme(),
|
|
617
944
|
readStoredActiveThemeFresh: () => themeActive.readStoredActiveThemeFresh(),
|
|
618
945
|
loadThemeConfig: () => themeConfig.loadThemeConfig(),
|
|
619
946
|
postMetadata: (post) => seo.postMetadata(post),
|
|
620
947
|
siteMetadata: () => seo.siteMetadata(),
|
|
948
|
+
publicHead: () => pluginHead.renderHead(),
|
|
949
|
+
publicBodyEnd: () => pluginHead.renderBodyEnd(),
|
|
621
950
|
renderBody: (post) => renderBody(post),
|
|
622
951
|
renderThemeCss: (cssVars) => renderThemeCss(cssVars),
|
|
623
952
|
publicAssetUrl: (key) => storage.publicAssetUrl(key),
|
|
@@ -626,21 +955,27 @@ function createAmpless(opts) {
|
|
|
626
955
|
cmsConfig,
|
|
627
956
|
themes,
|
|
628
957
|
posts,
|
|
958
|
+
media,
|
|
629
959
|
settings,
|
|
630
960
|
seo,
|
|
631
961
|
themeActive,
|
|
632
962
|
themeConfig,
|
|
633
|
-
storageApi: storage
|
|
963
|
+
storageApi: storage,
|
|
964
|
+
pluginHead
|
|
634
965
|
};
|
|
635
966
|
}
|
|
636
967
|
export {
|
|
637
968
|
COLOR_SCHEME_SETTING_KEY,
|
|
638
969
|
DEFAULT_COLOR_SCHEME,
|
|
970
|
+
_resetStreamS3Cache,
|
|
639
971
|
createAmpless,
|
|
972
|
+
createPluginHead,
|
|
640
973
|
htmlToMarkdown,
|
|
641
974
|
markdownToHtml,
|
|
642
975
|
renderBody,
|
|
643
976
|
renderThemeCss,
|
|
977
|
+
streamS3Object,
|
|
978
|
+
streamS3ObjectWithRunner,
|
|
644
979
|
tiptapToHtml,
|
|
645
980
|
tiptapToMarkdown,
|
|
646
981
|
validateColorScheme
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Ampless } from '../index.js';
|
|
2
2
|
import 'ampless';
|
|
3
3
|
import 'next';
|
|
4
|
+
import 'react';
|
|
5
|
+
import 'aws-amplify/storage/server';
|
|
6
|
+
import 'next/headers';
|
|
4
7
|
|
|
5
8
|
interface Ctx$4 {
|
|
6
9
|
params: Promise<{
|
|
@@ -84,15 +87,13 @@ type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
|
84
87
|
* the bundle resolve correctly — `<img src="img.png">` must resolve
|
|
85
88
|
* to `/<slug>/img.png`, not the site root.
|
|
86
89
|
*
|
|
87
|
-
* Trust model:
|
|
88
|
-
* the
|
|
90
|
+
* Trust model: the bucket stays private. Small files (<=6 MB) are
|
|
91
|
+
* streamed back through the Lambda response so CloudFront caches
|
|
92
|
+
* them; larger files fall back to a short-lived S3 presigned redirect.
|
|
89
93
|
*
|
|
90
94
|
* Cache-Control: deliberately omitted from the responses here.
|
|
91
95
|
* Middleware computes the strategy from `metadata.cache` +
|
|
92
|
-
* `post.updatedAt` and
|
|
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.
|
|
96
|
+
* `post.updatedAt` and overlays it on the rewritten response.
|
|
96
97
|
*/
|
|
97
98
|
declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
|
|
98
99
|
|
package/dist/routes/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
3
|
+
"version": "1.0.0-alpha.23",
|
|
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.
|
|
53
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
52
|
+
"ampless": "1.0.0-alpha.17",
|
|
53
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.17"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": "^15 || ^16",
|