@payloadcms/figma 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (38) 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/oauth/components/LoginButton/index.js +66 -19
  5. package/dist/oauth/components/LoginButton/index.scss +121 -15
  6. package/dist/plugin/build-config.d.ts +4 -2
  7. package/dist/plugin/build-config.js +34 -22
  8. package/dist/plugin/make-permission-access/make-permission-access.d.ts +8 -0
  9. package/dist/plugin/make-permission-access/make-permission-access.js +24 -0
  10. package/dist/storage-content-api/client-uploads/generate.js +2 -1
  11. package/dist/storage-content-api/index.js +1 -0
  12. package/dist/storage-content-api/staticHandler.js +21 -82
  13. package/dist/storage-content-api/types.d.ts +4 -1
  14. package/dist/utils/adapters/nextjs.js +5 -13
  15. package/dist/utils/adapters/nitro.js +39 -23
  16. package/dist/utils/adapters/vite.js +15 -1
  17. package/dist/utils/asset-collection.d.ts +7 -2
  18. package/dist/utils/asset-collection.js +44 -10
  19. package/dist/utils/build-lambda-zip.js +48 -16
  20. package/dist/utils/configureNextjsCache.d.ts +7 -0
  21. package/dist/utils/configureNextjsCache.js +55 -0
  22. package/dist/utils/fs-utils.d.ts +31 -4
  23. package/dist/utils/fs-utils.js +95 -15
  24. package/dist/utils/lambda-runtime/nextjs-cache.d.ts +22 -0
  25. package/dist/utils/lambda-runtime/nextjs-cache.js +162 -0
  26. package/package.json +12 -7
  27. package/dist/storage-content-api/buildBaseMediaResponseHeaders.d.ts +0 -1
  28. package/dist/storage-content-api/buildBaseMediaResponseHeaders.js +0 -6
  29. package/dist/storage-content-api/buildClientUploadFetchBackHeaders.d.ts +0 -10
  30. package/dist/storage-content-api/buildClientUploadFetchBackHeaders.js +0 -21
  31. package/dist/storage-content-api/buildMediaResponseHeaders.d.ts +0 -4
  32. package/dist/storage-content-api/buildMediaResponseHeaders.js +0 -45
  33. package/dist/storage-content-api/getInlineContentType.d.ts +0 -1
  34. package/dist/storage-content-api/getInlineContentType.js +0 -33
  35. package/dist/storage-content-api/hasSupportedContentEncoding.d.ts +0 -1
  36. package/dist/storage-content-api/hasSupportedContentEncoding.js +0 -4
  37. package/dist/storage-content-api/mediaErrorResponse.d.ts +0 -1
  38. 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';
@@ -6,6 +6,17 @@ import { getSearchParam } from '../../utils/getSearchParam.js';
6
6
  import { resolveParentTargetOrigin, setupIframeAutoLogin } from './iframeAutoLogin.js';
7
7
  import './index.scss';
8
8
  const baseClass = 'oauth-login';
9
+ const COLLECTION_SKELETON_KEYS = [
10
+ 'collection-1',
11
+ 'collection-2',
12
+ 'collection-3',
13
+ 'collection-4'
14
+ ];
15
+ const GLOBAL_SKELETON_KEYS = [
16
+ 'global-1',
17
+ 'global-2',
18
+ 'global-3'
19
+ ];
9
20
  // Max time we wait for the parent (figma.com) to respond with a code after
10
21
  // posting `mint-cms-oauth-code`. If we hit this without a reply, we assume
11
22
  // the parent isn't going to respond and fall back to the manual login button.
@@ -49,6 +60,57 @@ const FigmaIcon = ()=>/*#__PURE__*/ _jsx("svg", {
49
60
  fillRule: "evenodd"
50
61
  })
51
62
  });
63
+ const PayloadAdminLoadingShimmer = ()=>/*#__PURE__*/ _jsx("div", {
64
+ "aria-label": "Signing in",
65
+ "aria-live": "polite",
66
+ className: `${baseClass}__admin-shimmer`,
67
+ role: "status",
68
+ children: /*#__PURE__*/ _jsxs("div", {
69
+ "aria-hidden": "true",
70
+ className: `${baseClass}__admin-shimmer-main`,
71
+ children: [
72
+ /*#__PURE__*/ _jsxs("div", {
73
+ className: `${baseClass}__shimmer-heading-row`,
74
+ children: [
75
+ /*#__PURE__*/ _jsx("div", {
76
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-heading-icon`
77
+ }),
78
+ /*#__PURE__*/ _jsx("div", {
79
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-heading-label`
80
+ })
81
+ ]
82
+ }),
83
+ /*#__PURE__*/ _jsxs("div", {
84
+ className: `${baseClass}__shimmer-section`,
85
+ children: [
86
+ /*#__PURE__*/ _jsx("div", {
87
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-section-label`
88
+ }),
89
+ /*#__PURE__*/ _jsx("div", {
90
+ className: `${baseClass}__shimmer-card-grid`,
91
+ children: COLLECTION_SKELETON_KEYS.map((key)=>/*#__PURE__*/ _jsx("div", {
92
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-card`
93
+ }, key))
94
+ })
95
+ ]
96
+ }),
97
+ /*#__PURE__*/ _jsxs("div", {
98
+ className: `${baseClass}__shimmer-section`,
99
+ children: [
100
+ /*#__PURE__*/ _jsx("div", {
101
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-section-label`
102
+ }),
103
+ /*#__PURE__*/ _jsx("div", {
104
+ className: `${baseClass}__shimmer-card-grid`,
105
+ children: GLOBAL_SKELETON_KEYS.map((key)=>/*#__PURE__*/ _jsx("div", {
106
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-card`
107
+ }, key))
108
+ })
109
+ ]
110
+ })
111
+ ]
112
+ })
113
+ });
52
114
  export const DefaultLoginButton = ({ disabled, endpointSlug })=>{
53
115
  const { config: { admin: { user: userSlug }, routes: { api }, serverURL } } = useConfig();
54
116
  const [authorizeURL, setAuthorizeURL] = useState('');
@@ -150,26 +212,11 @@ export const DefaultLoginButton = ({ disabled, endpointSlug })=>{
150
212
  endpointSlug
151
213
  ]);
152
214
  if (!shouldRenderButton) {
153
- // In the iframe auto-login window, show a "Signing in…" hint so the
154
- // user sees something is happening instead of a blank slot until the
155
- // flow either navigates them away or times out into the manual button.
215
+ // In the iframe auto-login window, show a Payload admin-shaped shimmer
216
+ // until the flow either navigates them away or times out into the manual
217
+ // button.
156
218
  if (isInIframe === true && !disabled) {
157
- return /*#__PURE__*/ _jsx("div", {
158
- className: baseClass,
159
- children: /*#__PURE__*/ _jsxs("div", {
160
- className: `${baseClass}__signing-in`,
161
- role: "status",
162
- children: [
163
- /*#__PURE__*/ _jsx("span", {
164
- children: "Signing in"
165
- }),
166
- /*#__PURE__*/ _jsx("div", {
167
- "aria-hidden": "true",
168
- className: `${baseClass}__spinner`
169
- })
170
- ]
171
- })
172
- });
219
+ return /*#__PURE__*/ _jsx(PayloadAdminLoadingShimmer, {});
173
220
  }
174
221
  return null;
175
222
  }
@@ -1,3 +1,10 @@
1
+ .oauth-login,
2
+ .oauth-login__admin-shimmer {
3
+ --oauth-login-shimmer-bg: var(--theme-bg, var(--theme-elevation-0, #ffffff));
4
+ --oauth-login-shimmer-block: var(--theme-elevation-100, #efefef);
5
+ --oauth-login-shimmer-highlight: var(--theme-elevation-200, #f7f7f7);
6
+ }
7
+
1
8
  .oauth-login {
2
9
  display: flex;
3
10
  justify-content: center;
@@ -55,28 +62,127 @@
55
62
  Oxygen, Ubuntu, Cantarell, sans-serif;
56
63
  }
57
64
 
58
- &__signing-in {
59
- display: flex;
60
- flex-direction: column;
61
- align-items: center;
62
- gap: 0.75rem;
63
- font-size: 1rem;
65
+ &__admin-shimmer {
66
+ position: fixed;
67
+ inset: 0;
68
+ z-index: 1000;
69
+ overflow: hidden;
70
+ background: var(--oauth-login-shimmer-bg);
64
71
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
65
72
  Oxygen, Ubuntu, Cantarell, sans-serif;
66
73
  }
67
74
 
68
- &__spinner {
69
- width: 24px;
70
- height: 24px;
71
- border: 2px solid var(--theme-elevation-200, #e5e5e5);
72
- border-top-color: currentColor;
73
- border-radius: 50%;
74
- animation: oauth-login-spin 0.8s linear infinite;
75
+ &__admin-shimmer-main {
76
+ padding: 2rem 2.5rem;
77
+ }
78
+
79
+ &__shimmer-block {
80
+ position: relative;
81
+ overflow: hidden;
82
+ border-radius: 0.25rem;
83
+ background-color: var(--oauth-login-shimmer-block);
84
+ }
85
+
86
+ &__shimmer-block::after {
87
+ content: '';
88
+ position: absolute;
89
+ inset: 0;
90
+ border-radius: inherit;
91
+ background-image: linear-gradient(
92
+ 100deg,
93
+ transparent 0%,
94
+ transparent 35%,
95
+ var(--oauth-login-shimmer-highlight) 50%,
96
+ transparent 65%,
97
+ transparent 100%
98
+ );
99
+ pointer-events: none;
100
+ transform: translateX(-100%);
101
+ animation: oauth-login-shimmer 1.45s linear infinite;
102
+ }
103
+
104
+ &__shimmer-heading-row {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 0.5rem;
108
+ height: 1.75rem;
109
+ margin-bottom: 1rem;
110
+ }
111
+
112
+ &__shimmer-heading-icon {
113
+ width: 1.5rem;
114
+ height: 1.5rem;
115
+ }
116
+
117
+ &__shimmer-heading-label {
118
+ width: 7.5rem;
119
+ height: 1.25rem;
120
+ }
121
+
122
+ &__shimmer-section {
123
+ margin-bottom: 1.5rem;
124
+ }
125
+
126
+ &__shimmer-section-label {
127
+ width: 7.5rem;
128
+ height: 1.25rem;
129
+ margin-bottom: 0.5rem;
130
+ }
131
+
132
+ &__shimmer-card-grid {
133
+ display: grid;
134
+ grid-template-columns: repeat(4, minmax(0, 1fr));
135
+ gap: 1rem;
136
+ max-width: 51.625rem;
137
+ }
138
+
139
+ &__shimmer-card {
140
+ height: 4rem;
141
+ }
142
+ }
143
+
144
+ @media (prefers-color-scheme: dark) {
145
+ .oauth-login,
146
+ .oauth-login__admin-shimmer {
147
+ --oauth-login-shimmer-bg: var(--theme-bg, var(--theme-elevation-0, #2b2b2b));
148
+ --oauth-login-shimmer-block: var(--theme-elevation-100, #383838);
149
+ --oauth-login-shimmer-highlight: var(--theme-elevation-200, #444444);
150
+ }
151
+ }
152
+
153
+ @media (max-width: 48rem) {
154
+ .oauth-login {
155
+ &__shimmer-card-grid {
156
+ grid-template-columns: repeat(2, minmax(0, 1fr));
157
+ }
75
158
  }
76
159
  }
77
160
 
78
- @keyframes oauth-login-spin {
161
+ @media (max-width: 30rem) {
162
+ .oauth-login {
163
+ &__admin-shimmer-main {
164
+ padding-right: 0.75rem;
165
+ padding-left: 0.75rem;
166
+ }
167
+
168
+ &__shimmer-card-grid {
169
+ grid-template-columns: 1fr;
170
+ }
171
+ }
172
+ }
173
+
174
+ @media (prefers-reduced-motion: reduce) {
175
+ .oauth-login__shimmer-block::after {
176
+ animation: none;
177
+ }
178
+ }
179
+
180
+ @keyframes oauth-login-shimmer {
181
+ from {
182
+ transform: translateX(-100%);
183
+ }
184
+
79
185
  to {
80
- transform: rotate(360deg);
186
+ transform: translateX(100%);
81
187
  }
82
188
  }
@@ -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
  }