@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.
Files changed (27) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +2 -0
  4. package/dist/plugin/build-config.d.ts +4 -2
  5. package/dist/plugin/build-config.js +34 -22
  6. package/dist/plugin/make-permission-access/make-permission-access.d.ts +8 -0
  7. package/dist/plugin/make-permission-access/make-permission-access.js +24 -0
  8. package/dist/storage-content-api/client-uploads/generate.js +2 -1
  9. package/dist/storage-content-api/index.js +1 -0
  10. package/dist/storage-content-api/staticHandler.js +21 -82
  11. package/dist/storage-content-api/types.d.ts +4 -1
  12. package/dist/utils/adapters/nextjs.js +5 -13
  13. package/dist/utils/asset-collection.d.ts +7 -2
  14. package/dist/utils/asset-collection.js +27 -5
  15. package/package.json +12 -7
  16. package/dist/storage-content-api/buildBaseMediaResponseHeaders.d.ts +0 -1
  17. package/dist/storage-content-api/buildBaseMediaResponseHeaders.js +0 -6
  18. package/dist/storage-content-api/buildClientUploadFetchBackHeaders.d.ts +0 -10
  19. package/dist/storage-content-api/buildClientUploadFetchBackHeaders.js +0 -21
  20. package/dist/storage-content-api/buildMediaResponseHeaders.d.ts +0 -4
  21. package/dist/storage-content-api/buildMediaResponseHeaders.js +0 -45
  22. package/dist/storage-content-api/getInlineContentType.d.ts +0 -1
  23. package/dist/storage-content-api/getInlineContentType.js +0 -33
  24. package/dist/storage-content-api/hasSupportedContentEncoding.d.ts +0 -1
  25. package/dist/storage-content-api/hasSupportedContentEncoding.js +0 -4
  26. package/dist/storage-content-api/mediaErrorResponse.d.ts +0 -1
  27. package/dist/storage-content-api/mediaErrorResponse.js +0 -12
package/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2025 Figma
1
+ Copyright (c) 2026 Figma
2
2
 
3
3
  This source code is protected under international copyright law. All rights
4
4
  reserved and protected by the copyright holders.
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?: boolean;
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 = process.env.FIGMA_CONTENT_API_ACCESS_KEY ? {
175
- baseUrl: url,
176
- contentApiKey: process.env.FIGMA_CONTENT_API_ACCESS_KEY
177
- } : process.env.FIGMA_DEV_JWT === 'true' ? {
178
- auth: {
179
- mode: 'devJwt'
180
- },
181
- baseUrl: url,
182
- contentSystemId
183
- } : {
184
- auth: {
185
- mode: 'tokenStore',
186
- tokenStore: getTokenStore()
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
- baseUrl: url,
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 fall back to Payload's
362
- // "logged-in user" default so the admin "Run jobs" UI keeps working.
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
- return !!args.req.user;
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 })=>!!req.user;
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
@@ -28,6 +28,7 @@ export function contentApiStorageAdapter(config) {
28
28
  uploadInstructions: {
29
29
  enabled: true,
30
30
  generate: getGenerateUploadInstructions({
31
+ access: config.uploadInstructionsAccess,
31
32
  client
32
33
  }),
33
34
  useInAdmin: true
@@ -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
- // Lambda responses have a 6 MB limit, so deployed apps redirect to Gatekeeper, which
51
- // validates the signed URL and streams the file. Other runtimes do not have Gatekeeper
52
- // in front of them and must stream the file through Payload instead.
53
- // An image uploadReference marks an internal, one-time fetch-back. Payload core consumes
54
- // the body to size the image, so a redirect would leave it empty even in Lambda.
55
- if (shouldReturnRedirect({
56
- uploadReference
57
- })) {
58
- return Response.redirect(data.url, 302);
59
- }
60
- const requestHeaders = new Headers({
61
- 'Accept-Encoding': 'identity'
62
- });
63
- const range = req.headers.get('Range');
64
- if (range) {
65
- requestHeaders.set('Range', range);
66
- const ifRange = req.headers.get('If-Range');
67
- if (ifRange) {
68
- requestHeaders.set('If-Range', ifRange);
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
- let fileResponse;
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
- // Build route asset keys mapping and extract unique routes
43
- const routeAssetMap = {};
44
- for (const key of ssg.keys){
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 three files:
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<SSGAssets>;
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 three files:
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
- pathMap[s3Key] = path.relative(projectPath, path.join(outputPath, 'server', 'app', `${stripped}${ext}`));
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.6",
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.24",
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.24 <4.0.0-internal",
79
- "@payloadcms/richtext-lexical": ">=4.0.0-canary.24 <4.0.0-internal",
80
- "@payloadcms/translations": ">=4.0.0-canary.24 <4.0.0-internal",
81
- "@payloadcms/ui": ">=4.0.0-canary.24 <4.0.0-internal",
82
- "payload": ">=4.0.0-canary.24 <4.0.0-internal",
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,6 +0,0 @@
1
- export const buildBaseMediaResponseHeaders = ()=>new Headers({
2
- 'Accept-Ranges': 'bytes',
3
- 'Cache-Control': 'private, no-store',
4
- Vary: 'Cookie',
5
- 'X-Content-Type-Options': 'nosniff'
6
- });
@@ -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,4 +0,0 @@
1
- export declare const buildMediaResponseHeaders: ({ filename, upstream, }: {
2
- filename: string;
3
- upstream: Headers;
4
- }) => Headers;
@@ -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,4 +0,0 @@
1
- export const hasSupportedContentEncoding = (headers)=>{
2
- const contentEncoding = headers.get('content-encoding')?.trim().toLowerCase();
3
- return contentEncoding == null || contentEncoding === '' || contentEncoding === 'identity';
4
- };
@@ -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
- };