@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
@@ -0,0 +1,162 @@
1
+ import { deserialize, serialize } from 'node:v8';
2
+ const MAX_CACHE_BYTES = 8 * 1024 * 1024;
3
+ // Approximate Map entry and metadata overhead; actual heap use can differ.
4
+ const ESTIMATED_ENTRY_OVERHEAD_BYTES = 64;
5
+ const cache = new Map();
6
+ let cacheBytes = 0;
7
+ // Tag invalidation advances this counter to reject writes from earlier requests.
8
+ let tagVersion = 0;
9
+ /**
10
+ * A process-local, fresh-only Next.js cacheHandler. The CDN owns stale serving.
11
+ * No filesystem access: Lambda's code directory is read-only. Each cold process
12
+ * starts empty; Gatekeeper serves the separately uploaded static assets.
13
+ */ export default class NextjsCacheHandler {
14
+ constructor(){
15
+ this.tagVersion = tagVersion;
16
+ }
17
+ async get(key, context = {}) {
18
+ const cacheKey = getCacheKey(key, context.kind === 'FETCH');
19
+ const entry = cache.get(cacheKey);
20
+ if (!entry) {
21
+ observe('miss', context.kind);
22
+ return null;
23
+ }
24
+ const effectiveTTL = context.kind === 'FETCH' && context.revalidate !== undefined ? Math.min(entry.seconds, lifetime(context.revalidate)) : entry.seconds;
25
+ const ageMs = Math.max(0, Date.now() - entry.lastModified);
26
+ if (ageMs >= effectiveTTL * 1000) {
27
+ if (ageMs >= entry.seconds * 1000) {
28
+ remove(cacheKey);
29
+ }
30
+ observe('expired', context.kind);
31
+ return null;
32
+ }
33
+ // Clone cached buffers/maps so a caller cannot mutate the stored value.
34
+ // V8 can return Buffer views into its input, so copy the serialized bytes.
35
+ const value = deserialize(Buffer.from(entry.data));
36
+ if (value && [
37
+ 'APP_PAGE',
38
+ 'APP_ROUTE',
39
+ 'PAGES'
40
+ ].includes(value.kind)) {
41
+ value.headers = withoutAge(value.headers);
42
+ value.headers.Age = String(Math.floor(ageMs / 1000));
43
+ }
44
+ cache.delete(cacheKey);
45
+ cache.set(cacheKey, entry);
46
+ observe('hit', context.kind);
47
+ return {
48
+ value,
49
+ lastModified: entry.lastModified
50
+ };
51
+ }
52
+ async set(key, value, context = {}) {
53
+ // A render that overlaps invalidation must not restore an old value.
54
+ if (this.tagVersion !== tagVersion) {
55
+ return;
56
+ }
57
+ const cacheKey = getCacheKey(key, context.fetchCache);
58
+ remove(cacheKey);
59
+ const seconds = lifetime(context.fetchCache ? value?.revalidate : context.cacheControl?.revalidate);
60
+ if (seconds === 0) {
61
+ return;
62
+ }
63
+ let data;
64
+ try {
65
+ data = serialize(value);
66
+ } catch {
67
+ observe('unsupported', value?.kind);
68
+ return;
69
+ }
70
+ const size = data.byteLength + Buffer.byteLength(cacheKey) + ESTIMATED_ENTRY_OVERHEAD_BYTES;
71
+ if (size > MAX_CACHE_BYTES) {
72
+ observe('oversized', value?.kind);
73
+ return;
74
+ }
75
+ // Preserve an upstream Age, including a previous cached response that Next
76
+ // attempts to write again after an unsuccessful on-demand revalidation.
77
+ const age = responseAge(value);
78
+ if (age >= seconds) {
79
+ return;
80
+ }
81
+ while(cacheBytes + size > MAX_CACHE_BYTES){
82
+ remove(cache.keys().next().value);
83
+ }
84
+ cache.set(cacheKey, {
85
+ data,
86
+ size,
87
+ seconds,
88
+ lastModified: Date.now() - age * 1000
89
+ });
90
+ cacheBytes += size;
91
+ }
92
+ /**
93
+ * The revalidateTag function allows invalidating cached data on-demand based
94
+ * on the specified tags. For our purposes, since we are currently doing a
95
+ * local in-memory, per-process lambda cache, we can just go ahead and clear
96
+ * the cache. Eventually we will want to support doing tag invalidation at the
97
+ * CDN layer.
98
+ */ async revalidateTag(tags, _durations = {}) {
99
+ const tagsArr = Array.isArray(tags) ? tags : [
100
+ tags
101
+ ];
102
+ const hasValidTag = tagsArr.some((tag)=>typeof tag === 'string' && tag.length);
103
+ if (!hasValidTag) {
104
+ return;
105
+ }
106
+ cache.clear();
107
+ cacheBytes = 0;
108
+ tagVersion += 1;
109
+ observe('invalidated', 'local');
110
+ }
111
+ // Outstanding writes still belong to the request that started before invalidation.
112
+ resetRequestCache() {}
113
+ }
114
+ function getCacheKey(key, fetchCache) {
115
+ return `${fetchCache ? 'fetch' : 'route'}:${key}`;
116
+ }
117
+ function lifetime(revalidate) {
118
+ if (revalidate === false) {
119
+ return Infinity;
120
+ }
121
+ return Number.isSafeInteger(revalidate) && revalidate > 0 ? revalidate : 0;
122
+ }
123
+ function remove(key) {
124
+ const entry = cache.get(key);
125
+ if (entry) {
126
+ cacheBytes -= entry.size;
127
+ }
128
+ cache.delete(key);
129
+ }
130
+ function withoutAge(headers) {
131
+ return Object.fromEntries(Object.entries(headers ?? {}).filter(([name])=>name.toLowerCase() !== 'age'));
132
+ }
133
+ function responseAge(value) {
134
+ if (!value || value.kind === 'FETCH') {
135
+ return 0;
136
+ }
137
+ const age = Object.entries(value.headers ?? {}).find(([name])=>name.toLowerCase() === 'age')?.[1];
138
+ if (age === undefined) {
139
+ return 0;
140
+ }
141
+ if (typeof age !== 'number' && (typeof age !== 'string' || !/^\d+$/.test(age))) {
142
+ return Infinity;
143
+ }
144
+ const seconds = Number(age);
145
+ return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : Infinity;
146
+ }
147
+ function observe(result, kind) {
148
+ if (![
149
+ 'APP_PAGE',
150
+ 'APP_ROUTE',
151
+ 'PAGES',
152
+ 'local'
153
+ ].includes(kind)) {
154
+ return;
155
+ }
156
+ // Bounded fields only: never log cache keys, URLs, values, or tags.
157
+ console.info(JSON.stringify({
158
+ event: 'figma_nextjs_cache',
159
+ result,
160
+ kind
161
+ }));
162
+ }
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.8",
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
- };