@payloadcms/figma 0.1.0-alpha.6 → 0.1.0-alpha.7
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/LICENSE.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/plugin/build-config.d.ts +4 -2
- package/dist/plugin/build-config.js +34 -22
- package/dist/plugin/make-permission-access/make-permission-access.d.ts +8 -0
- package/dist/plugin/make-permission-access/make-permission-access.js +24 -0
- package/dist/storage-content-api/client-uploads/generate.js +2 -1
- package/dist/storage-content-api/index.js +1 -0
- package/dist/storage-content-api/staticHandler.js +21 -82
- package/dist/storage-content-api/types.d.ts +4 -1
- package/dist/utils/adapters/nextjs.js +5 -13
- package/dist/utils/asset-collection.d.ts +7 -2
- package/dist/utils/asset-collection.js +27 -5
- package/package.json +12 -7
- package/dist/storage-content-api/buildBaseMediaResponseHeaders.d.ts +0 -1
- package/dist/storage-content-api/buildBaseMediaResponseHeaders.js +0 -6
- package/dist/storage-content-api/buildClientUploadFetchBackHeaders.d.ts +0 -10
- package/dist/storage-content-api/buildClientUploadFetchBackHeaders.js +0 -21
- package/dist/storage-content-api/buildMediaResponseHeaders.d.ts +0 -4
- package/dist/storage-content-api/buildMediaResponseHeaders.js +0 -45
- package/dist/storage-content-api/getInlineContentType.d.ts +0 -1
- package/dist/storage-content-api/getInlineContentType.js +0 -33
- package/dist/storage-content-api/hasSupportedContentEncoding.d.ts +0 -1
- package/dist/storage-content-api/hasSupportedContentEncoding.js +0 -4
- package/dist/storage-content-api/mediaErrorResponse.d.ts +0 -1
- package/dist/storage-content-api/mediaErrorResponse.js +0 -12
package/LICENSE.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { figmaContentSystem } from './plugin/adapter.js';
|
|
|
4
4
|
export type { FigmaContentSystemOptions } from './plugin/adapter.js';
|
|
5
5
|
export { buildFigmaConfig } from './plugin/build-config.js';
|
|
6
6
|
export type { FigmaConfig } from './plugin/build-config.js';
|
|
7
|
+
export { passesMakeViewerWriteGate } from './plugin/make-permission-access/make-permission-access.js';
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,5 @@ export { contentAPIAdapter } from './db-content-api/index.js';
|
|
|
4
4
|
export { figmaContentSystem } from './plugin/adapter.js';
|
|
5
5
|
// Export buildConfig wrapper and its config type
|
|
6
6
|
export { buildFigmaConfig } from './plugin/build-config.js';
|
|
7
|
+
// Export the Make permission predicate for slug-specific baseAccess overrides.
|
|
8
|
+
export { passesMakeViewerWriteGate } from './plugin/make-permission-access/make-permission-access.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, SanitizedConfig } from 'payload';
|
|
1
|
+
import type { Config, SanitizedConfig, UploadInstructionsAccess } from 'payload';
|
|
2
2
|
/**
|
|
3
3
|
* Worker-trusted marker injected by Figma's sites-worker after it validates the
|
|
4
4
|
* X-Figma-Job-Queues-Auth-Bypass secret on a Payload jobs endpoint. The default
|
|
@@ -48,7 +48,9 @@ export type FigmaConfig = {
|
|
|
48
48
|
figma: {
|
|
49
49
|
contentSystemId?: string;
|
|
50
50
|
environmentId?: string;
|
|
51
|
-
storage?:
|
|
51
|
+
storage?: {
|
|
52
|
+
uploadInstructionsAccess?: UploadInstructionsAccess;
|
|
53
|
+
} | boolean;
|
|
52
54
|
useContentSystem?: boolean;
|
|
53
55
|
};
|
|
54
56
|
} & Omit<Config, 'db' | 'editor' | 'secret'> & Partial<Pick<Config, 'db'>>;
|
|
@@ -22,6 +22,7 @@ import { logMissingCliAuth } from './auth-preflight.js';
|
|
|
22
22
|
import { logMissingContentSystemId } from './bootstrap-preflight.js';
|
|
23
23
|
import { LIMIT_REACHED_VIEW_KEY, LIMIT_REACHED_VIEW_PATH } from './cloud-limits/routes.js';
|
|
24
24
|
import { getDevCookieNames } from './dev-cookie-names.js';
|
|
25
|
+
import { passesMakeViewerWriteGate, withMakeViewerWriteBaseAccessDefaults } from './make-permission-access/make-permission-access.js';
|
|
25
26
|
import { createSandboxUploadFetchPlugin } from './sandbox-upload-fetch.js';
|
|
26
27
|
/**
|
|
27
28
|
* Worker-trusted marker injected by Figma's sites-worker after it validates the
|
|
@@ -170,23 +171,26 @@ function messageOf(error) {
|
|
|
170
171
|
}
|
|
171
172
|
/**
|
|
172
173
|
* Creates storage configuration for upload collections
|
|
173
|
-
*/ function createStorageConfig(url, contentSystemId) {
|
|
174
|
-
const storageConfig =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
174
|
+
*/ function createStorageConfig(url, contentSystemId, uploadInstructionsAccess) {
|
|
175
|
+
const storageConfig = {
|
|
176
|
+
...process.env.FIGMA_CONTENT_API_ACCESS_KEY ? {
|
|
177
|
+
baseUrl: url,
|
|
178
|
+
contentApiKey: process.env.FIGMA_CONTENT_API_ACCESS_KEY
|
|
179
|
+
} : process.env.FIGMA_DEV_JWT === 'true' ? {
|
|
180
|
+
auth: {
|
|
181
|
+
mode: 'devJwt'
|
|
182
|
+
},
|
|
183
|
+
baseUrl: url,
|
|
184
|
+
contentSystemId
|
|
185
|
+
} : {
|
|
186
|
+
auth: {
|
|
187
|
+
mode: 'tokenStore',
|
|
188
|
+
tokenStore: getTokenStore()
|
|
189
|
+
},
|
|
190
|
+
baseUrl: url,
|
|
191
|
+
contentSystemId
|
|
187
192
|
},
|
|
188
|
-
|
|
189
|
-
contentSystemId
|
|
193
|
+
uploadInstructionsAccess
|
|
190
194
|
};
|
|
191
195
|
const adapter = contentApiStorageAdapter(storageConfig);
|
|
192
196
|
const storageClient = createStorageClient(storageConfig);
|
|
@@ -198,13 +202,13 @@ function messageOf(error) {
|
|
|
198
202
|
/**
|
|
199
203
|
* Plugin that applies cloud storage configuration to all upload-enabled collections.
|
|
200
204
|
* Runs at the end of the plugin chain so it sees collections added by user plugins too.
|
|
201
|
-
*/ function createStoragePlugin(url, contentSystemId) {
|
|
205
|
+
*/ function createStoragePlugin(url, contentSystemId, uploadInstructionsAccess) {
|
|
202
206
|
return (incomingConfig)=>{
|
|
203
207
|
const uploadCollections = (incomingConfig.collections || []).filter((c)=>c.upload);
|
|
204
208
|
if (uploadCollections.length === 0) {
|
|
205
209
|
return incomingConfig;
|
|
206
210
|
}
|
|
207
|
-
const { adapter } = createStorageConfig(url, contentSystemId);
|
|
211
|
+
const { adapter } = createStorageConfig(url, contentSystemId, uploadInstructionsAccess);
|
|
208
212
|
const collectionsMap = uploadCollections.reduce((acc, c)=>{
|
|
209
213
|
acc[c.slug] = {
|
|
210
214
|
adapter,
|
|
@@ -338,9 +342,15 @@ export async function buildFigmaConfig(config) {
|
|
|
338
342
|
]
|
|
339
343
|
}
|
|
340
344
|
},
|
|
345
|
+
// Payload applies this policy during sanitization, after user plugins and
|
|
346
|
+
// generated resources have registered their collections and globals.
|
|
347
|
+
baseAccess: withMakeViewerWriteBaseAccessDefaults(config.baseAccess),
|
|
341
348
|
custom: {
|
|
342
349
|
figma: {
|
|
343
350
|
...config.figma,
|
|
351
|
+
...typeof config.figma.storage === 'object' ? {
|
|
352
|
+
storage: true
|
|
353
|
+
} : {},
|
|
344
354
|
contentSystemId,
|
|
345
355
|
environmentId
|
|
346
356
|
}
|
|
@@ -358,8 +368,8 @@ export async function buildFigmaConfig(config) {
|
|
|
358
368
|
},
|
|
359
369
|
// Gate /api/payload-jobs/run and /api/payload-jobs/handle-schedules. Honor the
|
|
360
370
|
// sites-worker trust marker so Figma infra job runs never break, defer to the
|
|
361
|
-
// tenant's jobs.access.run if they set one, otherwise
|
|
362
|
-
//
|
|
371
|
+
// tenant's jobs.access.run if they set one, otherwise require an authenticated
|
|
372
|
+
// user who is not a known Make viewer.
|
|
363
373
|
jobs: {
|
|
364
374
|
...config.jobs ?? {},
|
|
365
375
|
access: {
|
|
@@ -371,7 +381,9 @@ export async function buildFigmaConfig(config) {
|
|
|
371
381
|
if (userJobsAccessRun) {
|
|
372
382
|
return userJobsAccessRun(args);
|
|
373
383
|
}
|
|
374
|
-
|
|
384
|
+
// Allow undefined Make permissions during rollout, but fail closed for empty,
|
|
385
|
+
// explicit view, and other unrecognized values.
|
|
386
|
+
return Boolean(args.req.user) && passesMakeViewerWriteGate(args.req.user);
|
|
375
387
|
}
|
|
376
388
|
}
|
|
377
389
|
},
|
|
@@ -385,7 +397,7 @@ export async function buildFigmaConfig(config) {
|
|
|
385
397
|
plugins: [
|
|
386
398
|
...config.plugins ?? [],
|
|
387
399
|
...config.figma.storage !== false ? [
|
|
388
|
-
createStoragePlugin(url, contentSystemId)
|
|
400
|
+
createStoragePlugin(url, contentSystemId, typeof config.figma.storage === 'object' ? config.figma.storage.uploadInstructionsAccess : undefined)
|
|
389
401
|
] : [],
|
|
390
402
|
oAuth2Plugin({
|
|
391
403
|
collections: [
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Access, BaseAccess } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* we will still let a user edit if makePermission is null or undefined, because we are
|
|
4
|
+
* specifically restricting users for a figma cloud project backed by make
|
|
5
|
+
*/
|
|
6
|
+
export declare function passesMakeViewerWriteGate(user: unknown): boolean;
|
|
7
|
+
export declare const makeViewerWriteBaseAccess: Access;
|
|
8
|
+
export declare function withMakeViewerWriteBaseAccessDefaults(existingBaseAccess?: BaseAccess): BaseAccess;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* we will still let a user edit if makePermission is null or undefined, because we are
|
|
3
|
+
* specifically restricting users for a figma cloud project backed by make
|
|
4
|
+
*/ export function passesMakeViewerWriteGate(user) {
|
|
5
|
+
const makePermission = user?.makePermission;
|
|
6
|
+
return makePermission === undefined || makePermission === null || makePermission === 'edit';
|
|
7
|
+
}
|
|
8
|
+
export const makeViewerWriteBaseAccess = ({ req })=>passesMakeViewerWriteGate(req.user);
|
|
9
|
+
export function withMakeViewerWriteBaseAccessDefaults(existingBaseAccess = {}) {
|
|
10
|
+
return {
|
|
11
|
+
...existingBaseAccess,
|
|
12
|
+
collections: {
|
|
13
|
+
...existingBaseAccess.collections,
|
|
14
|
+
create: existingBaseAccess.collections?.create ?? makeViewerWriteBaseAccess,
|
|
15
|
+
delete: existingBaseAccess.collections?.delete ?? makeViewerWriteBaseAccess,
|
|
16
|
+
unlock: existingBaseAccess.collections?.unlock ?? makeViewerWriteBaseAccess,
|
|
17
|
+
update: existingBaseAccess.collections?.update ?? makeViewerWriteBaseAccess
|
|
18
|
+
},
|
|
19
|
+
globals: {
|
|
20
|
+
...existingBaseAccess.globals,
|
|
21
|
+
update: existingBaseAccess.globals?.update ?? makeViewerWriteBaseAccess
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { APIError, Forbidden } from 'payload';
|
|
3
|
+
import { passesMakeViewerWriteGate } from '../../plugin/make-permission-access/make-permission-access.js';
|
|
3
4
|
import { getSafeFilename } from '../utilities/index.js';
|
|
4
|
-
const defaultAccess = ({ req })
|
|
5
|
+
const defaultAccess = ({ req })=>Boolean(req.user) && passesMakeViewerWriteGate(req.user);
|
|
5
6
|
/**
|
|
6
7
|
* v4 client-upload entry point. Payload core calls this from
|
|
7
8
|
* `POST /api/upload-instructions`; we sign a Content API upload URL and hand the
|
|
@@ -1,29 +1,7 @@
|
|
|
1
1
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { isImage } from 'payload/shared';
|
|
4
|
-
import { buildClientUploadFetchBackHeaders } from './buildClientUploadFetchBackHeaders.js';
|
|
5
|
-
import { buildMediaResponseHeaders } from './buildMediaResponseHeaders.js';
|
|
6
|
-
import { hasSupportedContentEncoding } from './hasSupportedContentEncoding.js';
|
|
7
|
-
import { mediaErrorResponse } from './mediaErrorResponse.js';
|
|
8
4
|
import { parseUploadReference } from './utilities/index.js';
|
|
9
|
-
const isRunningInLambda = ()=>process.env.AWS_EXECUTION_ENV?.startsWith('AWS_Lambda_') === true;
|
|
10
|
-
const shouldReturnRedirect = ({ uploadReference })=>{
|
|
11
|
-
if (!isRunningInLambda()) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
if (!uploadReference) {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
const { mimeType } = parseUploadReference(uploadReference);
|
|
18
|
-
return Boolean(mimeType && !isImage(mimeType));
|
|
19
|
-
};
|
|
20
|
-
const cancelResponseBody = async (response)=>{
|
|
21
|
-
try {
|
|
22
|
-
await response.body?.cancel();
|
|
23
|
-
} catch {
|
|
24
|
-
// The response will not be consumed after an error; cancellation is best-effort cleanup.
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
5
|
export const getHandler = ({ client, collection })=>{
|
|
28
6
|
return async (req, { params: { filename, uploadReference } })=>{
|
|
29
7
|
try {
|
|
@@ -47,68 +25,29 @@ export const getHandler = ({ client, collection })=>{
|
|
|
47
25
|
status: 500
|
|
48
26
|
});
|
|
49
27
|
}
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
28
|
+
// On a client-upload fetch-back, core reads this response body into req.file.data to
|
|
29
|
+
// size the image; a 302's empty body crashes getImageSize. Return real bytes for
|
|
30
|
+
// images (and unknown mimetypes, to be safe), but keep the redirect otherwise so we
|
|
31
|
+
// don't download large non-images server-side.
|
|
32
|
+
if (uploadReference) {
|
|
33
|
+
const { mimeType } = parseUploadReference(uploadReference);
|
|
34
|
+
if (!mimeType || isImage(mimeType)) {
|
|
35
|
+
const fileResponse = await fetch(data.url);
|
|
36
|
+
if (!fileResponse.ok) {
|
|
37
|
+
req.payload.logger.error(`Failed to fetch file for client-upload buffer fetch-back: ${fileResponse.status}`);
|
|
38
|
+
return new Response('Internal Server Error', {
|
|
39
|
+
status: 500
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const contentType = fileResponse.headers.get('Content-Type') ?? mimeType ?? 'application/octet-stream';
|
|
43
|
+
return new Response(await fileResponse.arrayBuffer(), {
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': contentType
|
|
46
|
+
}
|
|
47
|
+
});
|
|
69
48
|
}
|
|
70
49
|
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
fileResponse = await fetch(data.url, {
|
|
74
|
-
credentials: 'omit',
|
|
75
|
-
headers: requestHeaders,
|
|
76
|
-
redirect: 'manual',
|
|
77
|
-
referrerPolicy: 'no-referrer',
|
|
78
|
-
signal: req.signal
|
|
79
|
-
});
|
|
80
|
-
} catch {
|
|
81
|
-
req.payload.logger.error('Failed to fetch file from storage');
|
|
82
|
-
return mediaErrorResponse(500, 'Internal Server Error');
|
|
83
|
-
}
|
|
84
|
-
if (fileResponse.status === 416 && range) {
|
|
85
|
-
await cancelResponseBody(fileResponse);
|
|
86
|
-
return mediaErrorResponse(416, 'Range Not Satisfiable', fileResponse.headers.get('Content-Range') ?? undefined);
|
|
87
|
-
}
|
|
88
|
-
if (!fileResponse.ok) {
|
|
89
|
-
await cancelResponseBody(fileResponse);
|
|
90
|
-
req.payload.logger.error(`Failed to fetch file from storage: ${fileResponse.status}`);
|
|
91
|
-
return mediaErrorResponse(500, 'Internal Server Error');
|
|
92
|
-
}
|
|
93
|
-
if (!hasSupportedContentEncoding(fileResponse.headers)) {
|
|
94
|
-
await cancelResponseBody(fileResponse);
|
|
95
|
-
req.payload.logger.error('Unsupported Content-Encoding from storage');
|
|
96
|
-
return mediaErrorResponse(502, 'Bad Gateway');
|
|
97
|
-
}
|
|
98
|
-
const clientUploadMimeType = uploadReference ? parseUploadReference(uploadReference).mimeType : undefined;
|
|
99
|
-
const responseHeaders = uploadReference ? buildClientUploadFetchBackHeaders({
|
|
100
|
-
fallbackContentType: clientUploadMimeType,
|
|
101
|
-
filename,
|
|
102
|
-
upstream: fileResponse.headers
|
|
103
|
-
}) : buildMediaResponseHeaders({
|
|
104
|
-
filename,
|
|
105
|
-
upstream: fileResponse.headers
|
|
106
|
-
});
|
|
107
|
-
return new Response(fileResponse.body, {
|
|
108
|
-
headers: responseHeaders,
|
|
109
|
-
status: fileResponse.status,
|
|
110
|
-
statusText: fileResponse.statusText
|
|
111
|
-
});
|
|
50
|
+
return Response.redirect(data.url, 302);
|
|
112
51
|
} catch (err) {
|
|
113
52
|
req.payload.logger.error({
|
|
114
53
|
err,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { UploadInstructionsAccess } from 'payload';
|
|
1
2
|
import type { AuthMode } from '../db-content-api/utilities/auth.js';
|
|
2
3
|
export type StorageClientConfig = {
|
|
4
|
+
uploadInstructionsAccess?: UploadInstructionsAccess;
|
|
5
|
+
} & ({
|
|
3
6
|
auth: AuthMode;
|
|
4
7
|
baseUrl: string;
|
|
5
8
|
contentSystemId: string;
|
|
6
9
|
} | {
|
|
7
10
|
baseUrl: string;
|
|
8
11
|
contentApiKey: string;
|
|
9
|
-
};
|
|
12
|
+
});
|
|
@@ -39,21 +39,13 @@ export class NextjsAdapter {
|
|
|
39
39
|
}
|
|
40
40
|
async collectPages(projectPath, output) {
|
|
41
41
|
const ssg = await collectSSGAssets(projectPath, output);
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Strip extension to get the base, then derive the route
|
|
46
|
-
const base = key.replace(/\.(?:html|rsc|meta)$/, '');
|
|
47
|
-
const route = base === 'index' ? '/' : `/${base}`;
|
|
48
|
-
if (!routeAssetMap[route]) {
|
|
49
|
-
routeAssetMap[route] = [];
|
|
50
|
-
}
|
|
51
|
-
routeAssetMap[route].push(key);
|
|
52
|
-
}
|
|
42
|
+
// collectSSGAssets records each key's manifest route while scanning the
|
|
43
|
+
// output, preserving the existing mapping for HTML/RSC/meta files while
|
|
44
|
+
// supporting segment keys whose nested names do not encode the page route.
|
|
53
45
|
return {
|
|
54
46
|
pathMap: ssg.pathMap,
|
|
55
|
-
routeAssetMap,
|
|
56
|
-
routes: Object.keys(routeAssetMap),
|
|
47
|
+
routeAssetMap: ssg.routeAssetMap,
|
|
48
|
+
routes: Object.keys(ssg.routeAssetMap),
|
|
57
49
|
uploadKeys: ssg.keys
|
|
58
50
|
};
|
|
59
51
|
}
|
|
@@ -23,14 +23,18 @@ export type SSGAssets = {
|
|
|
23
23
|
keys: string[];
|
|
24
24
|
/** Map of S3 key → relative filesystem path from project root */
|
|
25
25
|
pathMap: Record<string, string>;
|
|
26
|
+
/** Map of page route → S3 keys generated for that page */
|
|
27
|
+
routeAssetMap: Record<string, string[]>;
|
|
26
28
|
};
|
|
29
|
+
type StaticMetadataAssets = Omit<SSGAssets, 'routeAssetMap'>;
|
|
27
30
|
/**
|
|
28
31
|
* Collect SSG asset paths by parsing .next/prerender-manifest.json
|
|
29
32
|
*
|
|
30
|
-
* For each page route in the manifest's `routes` object, collects
|
|
33
|
+
* For each page route in the manifest's `routes` object, collects:
|
|
31
34
|
* - {route}.html (full HTML)
|
|
32
35
|
* - {route}.rsc (React Server Components payload)
|
|
33
36
|
* - {route}.meta (response headers JSON)
|
|
37
|
+
* - Nested `.segment.rsc` files under {route}.segments (segment-prefetch payloads)
|
|
34
38
|
*
|
|
35
39
|
* S3 keys use the route path without leading slash.
|
|
36
40
|
* Filesystem paths point to .next/server/app/{route}.{ext}
|
|
@@ -46,7 +50,7 @@ export declare function collectSSGAssets(projectPath: string, output?: string):
|
|
|
46
50
|
* `.next/server/app/shop/icon1.png.body` output.
|
|
47
51
|
* Documentation: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
|
|
48
52
|
*/
|
|
49
|
-
export declare function collectStaticMetadataAssets(projectPath: string, output?: string): Promise<
|
|
53
|
+
export declare function collectStaticMetadataAssets(projectPath: string, output?: string): Promise<StaticMetadataAssets>;
|
|
50
54
|
/**
|
|
51
55
|
* Create Lambda deployment zip
|
|
52
56
|
*
|
|
@@ -56,3 +60,4 @@ export declare function collectStaticMetadataAssets(projectPath: string, output?
|
|
|
56
60
|
* @throws Error if build missing or zip creation fails
|
|
57
61
|
*/
|
|
58
62
|
export declare function createLambdaZip(projectPath: string): Promise<void>;
|
|
63
|
+
export {};
|
|
@@ -77,10 +77,11 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
77
77
|
/**
|
|
78
78
|
* Collect SSG asset paths by parsing .next/prerender-manifest.json
|
|
79
79
|
*
|
|
80
|
-
* For each page route in the manifest's `routes` object, collects
|
|
80
|
+
* For each page route in the manifest's `routes` object, collects:
|
|
81
81
|
* - {route}.html (full HTML)
|
|
82
82
|
* - {route}.rsc (React Server Components payload)
|
|
83
83
|
* - {route}.meta (response headers JSON)
|
|
84
|
+
* - Nested `.segment.rsc` files under {route}.segments (segment-prefetch payloads)
|
|
84
85
|
*
|
|
85
86
|
* S3 keys use the route path without leading slash.
|
|
86
87
|
* Filesystem paths point to .next/server/app/{route}.{ext}
|
|
@@ -97,22 +98,26 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
97
98
|
} catch {
|
|
98
99
|
return {
|
|
99
100
|
keys: [],
|
|
100
|
-
pathMap: {}
|
|
101
|
+
pathMap: {},
|
|
102
|
+
routeAssetMap: {}
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
if (!manifest.routes || typeof manifest.routes !== 'object') {
|
|
104
106
|
return {
|
|
105
107
|
keys: [],
|
|
106
|
-
pathMap: {}
|
|
108
|
+
pathMap: {},
|
|
109
|
+
routeAssetMap: {}
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
const keys = [];
|
|
110
113
|
const pathMap = {};
|
|
114
|
+
const routeAssetMap = {};
|
|
111
115
|
const extensions = [
|
|
112
116
|
'.html',
|
|
113
117
|
'.rsc',
|
|
114
118
|
'.meta'
|
|
115
119
|
];
|
|
120
|
+
const appDir = path.join(outputPath, 'server', 'app');
|
|
116
121
|
for (const [routeKey, route] of Object.entries(manifest.routes)){
|
|
117
122
|
// Current manifests classify app pages explicitly. Older App Router manifests
|
|
118
123
|
// can be identified by their RSC data route. Other entries may be route handlers.
|
|
@@ -122,15 +127,32 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
122
127
|
}
|
|
123
128
|
// Strip leading slash for S3 key; handle root "/" → "index"
|
|
124
129
|
const stripped = routeKey === '/' ? 'index' : routeKey.replace(/^\//, '');
|
|
130
|
+
const routeAssets = [];
|
|
125
131
|
for (const ext of extensions){
|
|
126
132
|
const s3Key = `${stripped}${ext}`;
|
|
127
133
|
keys.push(s3Key);
|
|
128
|
-
|
|
134
|
+
routeAssets.push(s3Key);
|
|
135
|
+
pathMap[s3Key] = path.relative(projectPath, path.join(appDir, s3Key));
|
|
129
136
|
}
|
|
137
|
+
// Next.js stores segment-prefetch payloads beside each prerendered page in a
|
|
138
|
+
// `<page>.segments` tree. Scan the generated tree instead of reconstructing
|
|
139
|
+
// its opaque, potentially nested segment names from route information.
|
|
140
|
+
const segmentFiles = await collectFiles(path.join(appDir, `${stripped}.segments`), appDir);
|
|
141
|
+
for (const segmentFile of segmentFiles.filter((file)=>file.endsWith('.segment.rsc'))){
|
|
142
|
+
const s3Key = segmentFile.split(path.sep).join('/');
|
|
143
|
+
keys.push(s3Key);
|
|
144
|
+
routeAssets.push(s3Key);
|
|
145
|
+
pathMap[s3Key] = path.relative(projectPath, path.join(appDir, segmentFile));
|
|
146
|
+
}
|
|
147
|
+
// Preserve the manifest route association for the original HTML/RSC/meta
|
|
148
|
+
// files and the new segment files; segment keys cannot be mapped back to a
|
|
149
|
+
// page reliably from their filename in NextjsAdapter.
|
|
150
|
+
routeAssetMap[routeKey] = routeAssets;
|
|
130
151
|
}
|
|
131
152
|
return {
|
|
132
153
|
keys,
|
|
133
|
-
pathMap
|
|
154
|
+
pathMap,
|
|
155
|
+
routeAssetMap
|
|
134
156
|
};
|
|
135
157
|
}
|
|
136
158
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payloadcms/figma",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -58,7 +58,11 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@payloadcms/eslint-config": "3.28.0",
|
|
60
60
|
"@payloadcms/eslint-plugin": "3.28.0",
|
|
61
|
-
"@payloadcms/next": "4.0.0-canary.
|
|
61
|
+
"@payloadcms/next": "4.0.0-canary.28",
|
|
62
|
+
"@payloadcms/plugin-cloud-storage": "4.0.0-canary.28",
|
|
63
|
+
"@payloadcms/richtext-lexical": "4.0.0-canary.28",
|
|
64
|
+
"@payloadcms/translations": "4.0.0-canary.28",
|
|
65
|
+
"@payloadcms/ui": "4.0.0-canary.28",
|
|
62
66
|
"@swc/cli": "0.7.7",
|
|
63
67
|
"@types/archiver": "7.0.0",
|
|
64
68
|
"@types/cross-spawn": "6.0.6",
|
|
@@ -69,17 +73,18 @@
|
|
|
69
73
|
"eslint": "9.22.0",
|
|
70
74
|
"next": "^16.2.6",
|
|
71
75
|
"openapi-typescript": "^7.13.0",
|
|
76
|
+
"payload": "4.0.0-canary.28",
|
|
72
77
|
"rimraf": "^6.1.3",
|
|
73
78
|
"tsx": "4.20.6",
|
|
74
79
|
"typescript": "5.7.3",
|
|
75
80
|
"vitest": "4.0.15"
|
|
76
81
|
},
|
|
77
82
|
"peerDependencies": {
|
|
78
|
-
"@payloadcms/plugin-cloud-storage": ">=4.0.0-canary.
|
|
79
|
-
"@payloadcms/richtext-lexical": ">=4.0.0-canary.
|
|
80
|
-
"@payloadcms/translations": ">=4.0.0-canary.
|
|
81
|
-
"@payloadcms/ui": ">=4.0.0-canary.
|
|
82
|
-
"payload": ">=4.0.0-canary.
|
|
83
|
+
"@payloadcms/plugin-cloud-storage": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
84
|
+
"@payloadcms/richtext-lexical": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
85
|
+
"@payloadcms/translations": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
86
|
+
"@payloadcms/ui": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
87
|
+
"payload": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
83
88
|
"react": "^19.0.1 || ^19.1.2 || ^19.2.1"
|
|
84
89
|
},
|
|
85
90
|
"peerDependenciesMeta": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const buildBaseMediaResponseHeaders: () => Headers;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Payload's server-side upload fetch-back uses the response Content-Type to decide whether to
|
|
3
|
-
* inspect image dimensions. For an active image type such as SVG, omit the sanitized type so
|
|
4
|
-
* Payload falls back to the original upload MIME type. Public file requests never use this helper.
|
|
5
|
-
*/
|
|
6
|
-
export declare const buildClientUploadFetchBackHeaders: ({ fallbackContentType, filename, upstream, }: {
|
|
7
|
-
fallbackContentType?: string;
|
|
8
|
-
filename: string;
|
|
9
|
-
upstream: Headers;
|
|
10
|
-
}) => Headers;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { buildMediaResponseHeaders } from './buildMediaResponseHeaders.js';
|
|
2
|
-
import { getInlineContentType } from './getInlineContentType.js';
|
|
3
|
-
/**
|
|
4
|
-
* Payload's server-side upload fetch-back uses the response Content-Type to decide whether to
|
|
5
|
-
* inspect image dimensions. For an active image type such as SVG, omit the sanitized type so
|
|
6
|
-
* Payload falls back to the original upload MIME type. Public file requests never use this helper.
|
|
7
|
-
*/ export const buildClientUploadFetchBackHeaders = ({ fallbackContentType, filename, upstream })=>{
|
|
8
|
-
const effectiveContentType = upstream.get('Content-Type') || fallbackContentType;
|
|
9
|
-
const upstreamWithFallback = new Headers(upstream);
|
|
10
|
-
if (effectiveContentType && !upstream.get('Content-Type')) {
|
|
11
|
-
upstreamWithFallback.set('Content-Type', effectiveContentType);
|
|
12
|
-
}
|
|
13
|
-
const headers = buildMediaResponseHeaders({
|
|
14
|
-
filename,
|
|
15
|
-
upstream: upstreamWithFallback
|
|
16
|
-
});
|
|
17
|
-
if (effectiveContentType && !getInlineContentType(effectiveContentType)) {
|
|
18
|
-
headers.delete('Content-Type');
|
|
19
|
-
}
|
|
20
|
-
return headers;
|
|
21
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { buildBaseMediaResponseHeaders } from './buildBaseMediaResponseHeaders.js';
|
|
2
|
-
import { getInlineContentType } from './getInlineContentType.js';
|
|
3
|
-
const FORWARDED_MEDIA_RESPONSE_HEADERS = [
|
|
4
|
-
'Cache-Control',
|
|
5
|
-
'Content-Length',
|
|
6
|
-
'Content-Range',
|
|
7
|
-
'ETag',
|
|
8
|
-
'Last-Modified'
|
|
9
|
-
];
|
|
10
|
-
const ATTACHMENT_CSP = "sandbox; default-src 'none'; base-uri 'none'; form-action 'none'";
|
|
11
|
-
export const buildMediaResponseHeaders = ({ filename, upstream })=>{
|
|
12
|
-
const headers = buildBaseMediaResponseHeaders();
|
|
13
|
-
for (const name of FORWARDED_MEDIA_RESPONSE_HEADERS){
|
|
14
|
-
const value = upstream.get(name);
|
|
15
|
-
if (value) {
|
|
16
|
-
headers.set(name, value);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
const contentType = getInlineContentType(upstream.get('Content-Type'));
|
|
20
|
-
if (contentType) {
|
|
21
|
-
headers.set('Content-Type', contentType);
|
|
22
|
-
} else {
|
|
23
|
-
headers.set('Content-Type', 'application/octet-stream');
|
|
24
|
-
headers.set('Content-Disposition', attachmentDisposition(filename));
|
|
25
|
-
headers.set('Content-Security-Policy', ATTACHMENT_CSP);
|
|
26
|
-
}
|
|
27
|
-
return headers;
|
|
28
|
-
};
|
|
29
|
-
const attachmentDisposition = (filename)=>{
|
|
30
|
-
let decodedName;
|
|
31
|
-
try {
|
|
32
|
-
decodedName = decodeURIComponent(filename);
|
|
33
|
-
} catch {
|
|
34
|
-
decodedName = 'download';
|
|
35
|
-
}
|
|
36
|
-
const sanitizedName = sanitizeFilename(decodedName);
|
|
37
|
-
const safeName = Array.from(sanitizedName).slice(0, 255).join('') || 'download';
|
|
38
|
-
const asciiName = safeName.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_');
|
|
39
|
-
const utf8Name = encodeURIComponent(safeName).replace(/[!'()*]/g, (character)=>`%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
40
|
-
return `attachment; filename="${asciiName}"; filename*=UTF-8''${utf8Name}`;
|
|
41
|
-
};
|
|
42
|
-
const sanitizeFilename = (filename)=>Array.from(filename, (character)=>{
|
|
43
|
-
const codePoint = character.codePointAt(0);
|
|
44
|
-
return codePoint < 32 || codePoint === 127 || character === '/' || character === '\\' ? '_' : character;
|
|
45
|
-
}).join('');
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const getInlineContentType: (value: null | string | undefined) => string | null;
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Keep this allowlist aligned with Gatekeeper's presignedMediaHeaders.ts. The plugin cannot
|
|
2
|
-
// import the private Gatekeeper service, but both same-origin media proxies need the same policy.
|
|
3
|
-
const INLINE_CONTENT_TYPES = new Set([
|
|
4
|
-
'application/font-woff',
|
|
5
|
-
'audio/aac',
|
|
6
|
-
'audio/flac',
|
|
7
|
-
'audio/mp4',
|
|
8
|
-
'audio/mpeg',
|
|
9
|
-
'audio/ogg',
|
|
10
|
-
'audio/wav',
|
|
11
|
-
'audio/webm',
|
|
12
|
-
'font/otf',
|
|
13
|
-
'font/ttf',
|
|
14
|
-
'font/woff',
|
|
15
|
-
'font/woff2',
|
|
16
|
-
'image/avif',
|
|
17
|
-
'image/bmp',
|
|
18
|
-
'image/gif',
|
|
19
|
-
'image/jpeg',
|
|
20
|
-
'image/png',
|
|
21
|
-
'image/vnd.microsoft.icon',
|
|
22
|
-
'image/webp',
|
|
23
|
-
'image/x-icon',
|
|
24
|
-
'video/mp4',
|
|
25
|
-
'video/mpeg',
|
|
26
|
-
'video/ogg',
|
|
27
|
-
'video/quicktime',
|
|
28
|
-
'video/webm'
|
|
29
|
-
]);
|
|
30
|
-
export const getInlineContentType = (value)=>{
|
|
31
|
-
const normalized = value?.split(';', 1).at(0)?.trim().toLowerCase();
|
|
32
|
-
return normalized && INLINE_CONTENT_TYPES.has(normalized) ? normalized : null;
|
|
33
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const hasSupportedContentEncoding: (headers: Headers) => boolean;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const mediaErrorResponse: (status: number, body: string, contentRange?: string) => Response;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { buildBaseMediaResponseHeaders } from './buildBaseMediaResponseHeaders.js';
|
|
2
|
-
export const mediaErrorResponse = (status, body, contentRange)=>{
|
|
3
|
-
const headers = buildBaseMediaResponseHeaders();
|
|
4
|
-
headers.set('Content-Type', 'text/plain;charset=UTF-8');
|
|
5
|
-
if (contentRange) {
|
|
6
|
-
headers.set('Content-Range', contentRange);
|
|
7
|
-
}
|
|
8
|
-
return new Response(body, {
|
|
9
|
-
headers,
|
|
10
|
-
status
|
|
11
|
-
});
|
|
12
|
-
};
|