@ampless/runtime 1.0.0-alpha.20 → 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.
- package/dist/chunk-5BLMLMZU.js +123 -0
- package/dist/dispatchers/index.d.ts +2 -0
- package/dist/index.d.ts +104 -2
- package/dist/index.js +56 -0
- package/dist/routes/index.d.ts +6 -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,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
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -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:
|
|
88
|
-
* the
|
|
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
|
|
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
|
|
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.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.
|
|
53
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
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",
|