@delmaredigital/payload-puck 0.8.2 → 0.9.0
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/README.md +49 -0
- package/dist/ai/plugins/promptApiRoutes.d.ts +8 -0
- package/dist/ai/plugins/promptApiRoutes.js +34 -4
- package/dist/ai/tools/index.js +22 -8
- package/dist/api/createPuckApiRoutes.js +49 -1
- package/dist/api/createPuckApiRoutesVersions.js +25 -2
- package/dist/api/createPuckApiRoutesWithId.js +31 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +2 -0
- package/dist/api/types.d.ts +101 -14
- package/dist/api/utils/access.d.ts +137 -0
- package/dist/api/utils/access.js +180 -0
- package/dist/endpoints/ai.js +17 -1
- package/dist/endpoints/context.js +19 -4
- package/dist/endpoints/index.js +8 -7
- package/dist/endpoints/prompts.js +19 -4
- package/dist/plugin/hooks/isHomepageUnique.js +12 -1
- package/dist/utils/payloadErrors.d.ts +42 -0
- package/dist/utils/payloadErrors.js +69 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +23 -23
package/dist/endpoints/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/ import { APIError } from 'payload';
|
|
10
10
|
import { unsetHomepage, HomepageConflictError } from '../plugin/hooks/isHomepageUnique.js';
|
|
11
11
|
import { resolveLocale } from '../utils/locale.js';
|
|
12
|
+
import { payloadErrorStatus } from '../utils/payloadErrors.js';
|
|
12
13
|
import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProps.js';
|
|
13
14
|
/**
|
|
14
15
|
* Merge a Puck save payload's `data` with the Payload fields derived from its
|
|
@@ -61,7 +62,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
61
62
|
return Response.json({
|
|
62
63
|
error: error instanceof Error ? error.message : 'List failed'
|
|
63
64
|
}, {
|
|
64
|
-
status: 500
|
|
65
|
+
status: payloadErrorStatus(error) ?? 500
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
};
|
|
@@ -103,7 +104,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
103
104
|
return Response.json({
|
|
104
105
|
error: error instanceof Error ? error.message : 'Create failed'
|
|
105
106
|
}, {
|
|
106
|
-
status: 500
|
|
107
|
+
status: payloadErrorStatus(error) ?? 500
|
|
107
108
|
});
|
|
108
109
|
}
|
|
109
110
|
};
|
|
@@ -144,7 +145,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
144
145
|
return Response.json({
|
|
145
146
|
error: error instanceof Error ? error.message : 'Get failed'
|
|
146
147
|
}, {
|
|
147
|
-
status: 500
|
|
148
|
+
status: payloadErrorStatus(error) ?? 500
|
|
148
149
|
});
|
|
149
150
|
}
|
|
150
151
|
};
|
|
@@ -262,7 +263,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
262
263
|
return Response.json({
|
|
263
264
|
error: error instanceof Error ? error.message : 'Update failed'
|
|
264
265
|
}, {
|
|
265
|
-
status: 500
|
|
266
|
+
status: payloadErrorStatus(error) ?? 500
|
|
266
267
|
});
|
|
267
268
|
}
|
|
268
269
|
};
|
|
@@ -297,7 +298,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
297
298
|
return Response.json({
|
|
298
299
|
error: error instanceof Error ? error.message : 'Delete failed'
|
|
299
300
|
}, {
|
|
300
|
-
status: 500
|
|
301
|
+
status: payloadErrorStatus(error) ?? 500
|
|
301
302
|
});
|
|
302
303
|
}
|
|
303
304
|
};
|
|
@@ -343,7 +344,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
343
344
|
return Response.json({
|
|
344
345
|
error: error instanceof Error ? error.message : 'Versions failed'
|
|
345
346
|
}, {
|
|
346
|
-
status: 500
|
|
347
|
+
status: payloadErrorStatus(error) ?? 500
|
|
347
348
|
});
|
|
348
349
|
}
|
|
349
350
|
};
|
|
@@ -390,7 +391,7 @@ import { mapRootPropsToPayloadFields, deepMerge } from '../api/utils/mapRootProp
|
|
|
390
391
|
return Response.json({
|
|
391
392
|
error: error instanceof Error ? error.message : 'Restore failed'
|
|
392
393
|
}, {
|
|
393
|
-
status: 500
|
|
394
|
+
status: payloadErrorStatus(error) ?? 500
|
|
394
395
|
});
|
|
395
396
|
}
|
|
396
397
|
};
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { payloadErrorStatus } from '../utils/payloadErrors.js';
|
|
1
2
|
/**
|
|
3
|
+
* Access control: every handler passes `overrideAccess: false` and `req` to
|
|
4
|
+
* Payload's local API, so the collection's own `access` rules and field-level
|
|
5
|
+
* access are enforced against the calling user. The `if (!req.user)` gate is
|
|
6
|
+
* authentication only — it is not a substitute for authorization, and relying on
|
|
7
|
+
* it alone was the bug in GHSA-rrx7-m589-5wfq.
|
|
8
|
+
*/ /**
|
|
2
9
|
* Collection slug for AI prompts
|
|
3
10
|
* Matches the auto-generated collection from createPuckPlugin
|
|
4
11
|
*/ const COLLECTION = 'puck-ai-prompts';
|
|
@@ -18,6 +25,8 @@
|
|
|
18
25
|
try {
|
|
19
26
|
const result = await req.payload.find({
|
|
20
27
|
collection: COLLECTION,
|
|
28
|
+
req,
|
|
29
|
+
overrideAccess: false,
|
|
21
30
|
sort: 'order',
|
|
22
31
|
limit: 100
|
|
23
32
|
});
|
|
@@ -27,7 +36,7 @@
|
|
|
27
36
|
return Response.json({
|
|
28
37
|
error: e instanceof Error ? e.message : 'Failed to list prompts'
|
|
29
38
|
}, {
|
|
30
|
-
status: 500
|
|
39
|
+
status: payloadErrorStatus(e) ?? 500
|
|
31
40
|
});
|
|
32
41
|
}
|
|
33
42
|
};
|
|
@@ -57,6 +66,8 @@
|
|
|
57
66
|
}
|
|
58
67
|
const doc = await req.payload.create({
|
|
59
68
|
collection: COLLECTION,
|
|
69
|
+
req,
|
|
70
|
+
overrideAccess: false,
|
|
60
71
|
data
|
|
61
72
|
});
|
|
62
73
|
return Response.json(doc);
|
|
@@ -65,7 +76,7 @@
|
|
|
65
76
|
return Response.json({
|
|
66
77
|
error: e instanceof Error ? e.message : 'Failed to create prompt'
|
|
67
78
|
}, {
|
|
68
|
-
status: 500
|
|
79
|
+
status: payloadErrorStatus(e) ?? 500
|
|
69
80
|
});
|
|
70
81
|
}
|
|
71
82
|
};
|
|
@@ -103,6 +114,8 @@
|
|
|
103
114
|
}
|
|
104
115
|
const doc = await req.payload.update({
|
|
105
116
|
collection: COLLECTION,
|
|
117
|
+
req,
|
|
118
|
+
overrideAccess: false,
|
|
106
119
|
id,
|
|
107
120
|
data
|
|
108
121
|
});
|
|
@@ -112,7 +125,7 @@
|
|
|
112
125
|
return Response.json({
|
|
113
126
|
error: e instanceof Error ? e.message : 'Failed to update prompt'
|
|
114
127
|
}, {
|
|
115
|
-
status: 500
|
|
128
|
+
status: payloadErrorStatus(e) ?? 500
|
|
116
129
|
});
|
|
117
130
|
}
|
|
118
131
|
};
|
|
@@ -141,6 +154,8 @@
|
|
|
141
154
|
try {
|
|
142
155
|
await req.payload.delete({
|
|
143
156
|
collection: COLLECTION,
|
|
157
|
+
req,
|
|
158
|
+
overrideAccess: false,
|
|
144
159
|
id
|
|
145
160
|
});
|
|
146
161
|
return Response.json({
|
|
@@ -151,7 +166,7 @@
|
|
|
151
166
|
return Response.json({
|
|
152
167
|
error: e instanceof Error ? e.message : 'Failed to delete prompt'
|
|
153
168
|
}, {
|
|
154
|
-
status: 500
|
|
169
|
+
status: payloadErrorStatus(e) ?? 500
|
|
155
170
|
});
|
|
156
171
|
}
|
|
157
172
|
};
|
|
@@ -50,9 +50,16 @@ import { APIError } from 'payload';
|
|
|
50
50
|
const collectionSlug = options.collectionSlug || collection.slug;
|
|
51
51
|
// Use locale from context (passed by endpoint handler) or fall back to req.locale
|
|
52
52
|
const locale = context?.locale || req.locale;
|
|
53
|
-
// Query for existing homepage (excluding current document)
|
|
53
|
+
// Query for existing homepage (excluding current document).
|
|
54
|
+
//
|
|
55
|
+
// `overrideAccess: true` is deliberate. This hook runs *inside* an operation
|
|
56
|
+
// whose access Payload already checked, and it enforces a global invariant:
|
|
57
|
+
// only one page may be the homepage. Evaluating it against the editing user's
|
|
58
|
+
// read rules would let someone create a second homepage merely because they
|
|
59
|
+
// cannot see the first — a correctness bug, not a safeguard.
|
|
54
60
|
const existingHomepage = await req.payload.find({
|
|
55
61
|
collection: collectionSlug,
|
|
62
|
+
overrideAccess: true,
|
|
56
63
|
...locale ? {
|
|
57
64
|
locale: locale.toString()
|
|
58
65
|
} : {},
|
|
@@ -91,8 +98,12 @@ import { APIError } from 'payload';
|
|
|
91
98
|
* Unsets isHomepage on the specified page.
|
|
92
99
|
* Used when swapping homepages.
|
|
93
100
|
*/ export async function unsetHomepage(payload, collectionSlug, pageId, locale) {
|
|
101
|
+
// Deliberate, for the same reason as the uniqueness query above: unsetting the
|
|
102
|
+
// previous homepage is invariant maintenance on behalf of an already-authorized
|
|
103
|
+
// edit, not a user-initiated write to that document.
|
|
94
104
|
await payload.update({
|
|
95
105
|
collection: collectionSlug,
|
|
106
|
+
overrideAccess: true,
|
|
96
107
|
id: pageId,
|
|
97
108
|
data: {
|
|
98
109
|
isHomepage: false
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping Payload errors onto HTTP status codes.
|
|
3
|
+
*
|
|
4
|
+
* Payload's `APIError` (and its subclasses `Forbidden`, `NotFound`,
|
|
5
|
+
* `ValidationError`, ...) carries the status the response should use. Handlers
|
|
6
|
+
* that flatten everything to 500 turn an authorization *denial* into a server
|
|
7
|
+
* *error*, which hides the access-control layer working correctly, breaks client
|
|
8
|
+
* error handling, and pollutes error monitoring. That matters much more now that
|
|
9
|
+
* access control is actually enforced on these routes.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Extract the HTTP status Payload intends for an error.
|
|
13
|
+
*
|
|
14
|
+
* The `instanceof` check is the fast path. The structural fallback exists
|
|
15
|
+
* because `payload` can legitimately be resolved more than once in a monorepo or
|
|
16
|
+
* under pnpm's linking, and a duplicated class identity would silently defeat
|
|
17
|
+
* `instanceof` — producing exactly the kind of environment-dependent flakiness
|
|
18
|
+
* this mapping is meant to remove.
|
|
19
|
+
*
|
|
20
|
+
* @returns the status code, or `null` if this is not a Payload API error.
|
|
21
|
+
*/
|
|
22
|
+
export declare function payloadErrorStatus(error: unknown): number | null;
|
|
23
|
+
/**
|
|
24
|
+
* True when Payload rejected the operation because access control denied it.
|
|
25
|
+
*
|
|
26
|
+
* Used to keep denial responses free of internal detail — a 403 body should not
|
|
27
|
+
* describe which rule failed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isForbiddenError(error: unknown): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Turn a Payload client error (4xx) into the response it should have produced.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` for anything that is not a Payload 4xx — including genuine 5xx
|
|
34
|
+
* faults — so callers fall through to their existing generic handling. Call it
|
|
35
|
+
* *after* any bespoke handling (`ValidationError`, `HomepageConflictError`) so
|
|
36
|
+
* those keep precedence.
|
|
37
|
+
*
|
|
38
|
+
* Payload marks user-facing errors with `isPublic`. Messages from errors that
|
|
39
|
+
* are not public are replaced with a generic one, so internal detail is never
|
|
40
|
+
* echoed back to the caller.
|
|
41
|
+
*/
|
|
42
|
+
export declare function payloadErrorResponse(error: unknown): Response | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping Payload errors onto HTTP status codes.
|
|
3
|
+
*
|
|
4
|
+
* Payload's `APIError` (and its subclasses `Forbidden`, `NotFound`,
|
|
5
|
+
* `ValidationError`, ...) carries the status the response should use. Handlers
|
|
6
|
+
* that flatten everything to 500 turn an authorization *denial* into a server
|
|
7
|
+
* *error*, which hides the access-control layer working correctly, breaks client
|
|
8
|
+
* error handling, and pollutes error monitoring. That matters much more now that
|
|
9
|
+
* access control is actually enforced on these routes.
|
|
10
|
+
*/ import { APIError } from 'payload';
|
|
11
|
+
/**
|
|
12
|
+
* Extract the HTTP status Payload intends for an error.
|
|
13
|
+
*
|
|
14
|
+
* The `instanceof` check is the fast path. The structural fallback exists
|
|
15
|
+
* because `payload` can legitimately be resolved more than once in a monorepo or
|
|
16
|
+
* under pnpm's linking, and a duplicated class identity would silently defeat
|
|
17
|
+
* `instanceof` — producing exactly the kind of environment-dependent flakiness
|
|
18
|
+
* this mapping is meant to remove.
|
|
19
|
+
*
|
|
20
|
+
* @returns the status code, or `null` if this is not a Payload API error.
|
|
21
|
+
*/ export function payloadErrorStatus(error) {
|
|
22
|
+
if (error instanceof APIError && typeof error.status === 'number') {
|
|
23
|
+
return error.status;
|
|
24
|
+
}
|
|
25
|
+
if (error instanceof Error) {
|
|
26
|
+
const candidate = error;
|
|
27
|
+
if (typeof candidate.status === 'number' && candidate.status >= 400 && candidate.status <= 599 && typeof candidate.isOperational === 'boolean') {
|
|
28
|
+
return candidate.status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True when Payload rejected the operation because access control denied it.
|
|
35
|
+
*
|
|
36
|
+
* Used to keep denial responses free of internal detail — a 403 body should not
|
|
37
|
+
* describe which rule failed.
|
|
38
|
+
*/ export function isForbiddenError(error) {
|
|
39
|
+
return payloadErrorStatus(error) === 403;
|
|
40
|
+
}
|
|
41
|
+
const CLIENT_ERROR_MESSAGES = {
|
|
42
|
+
400: 'Bad request',
|
|
43
|
+
401: 'Unauthorized',
|
|
44
|
+
403: 'Forbidden',
|
|
45
|
+
404: 'Not found',
|
|
46
|
+
409: 'Conflict'
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Turn a Payload client error (4xx) into the response it should have produced.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` for anything that is not a Payload 4xx — including genuine 5xx
|
|
52
|
+
* faults — so callers fall through to their existing generic handling. Call it
|
|
53
|
+
* *after* any bespoke handling (`ValidationError`, `HomepageConflictError`) so
|
|
54
|
+
* those keep precedence.
|
|
55
|
+
*
|
|
56
|
+
* Payload marks user-facing errors with `isPublic`. Messages from errors that
|
|
57
|
+
* are not public are replaced with a generic one, so internal detail is never
|
|
58
|
+
* echoed back to the caller.
|
|
59
|
+
*/ export function payloadErrorResponse(error) {
|
|
60
|
+
const status = payloadErrorStatus(error);
|
|
61
|
+
if (status === null || status < 400 || status >= 500) return null;
|
|
62
|
+
const isPublic = error instanceof Error && error.isPublic === true;
|
|
63
|
+
const message = isPublic && error.message ? error.message : CLIENT_ERROR_MESSAGES[status] ?? 'Request failed';
|
|
64
|
+
return Response.json({
|
|
65
|
+
error: message
|
|
66
|
+
}, {
|
|
67
|
+
status
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.9.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.js - do not edit manually
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.9.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-puck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Puck visual page builder plugin for Payload CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -145,37 +145,37 @@
|
|
|
145
145
|
"@puckeditor/plugin-ai": "^0.8.2",
|
|
146
146
|
"@puckeditor/plugin-heading-analyzer": "^0.23.0",
|
|
147
147
|
"@radix-ui/react-popover": "^1.1.15",
|
|
148
|
-
"@tiptap/core": "^3.
|
|
149
|
-
"@tiptap/extension-color": "^3.
|
|
150
|
-
"@tiptap/extension-highlight": "^3.
|
|
151
|
-
"@tiptap/extension-link": "^3.
|
|
152
|
-
"@tiptap/extension-subscript": "^3.
|
|
153
|
-
"@tiptap/extension-superscript": "^3.
|
|
154
|
-
"@tiptap/extension-text-align": "^3.
|
|
155
|
-
"@tiptap/extension-text-style": "^3.
|
|
156
|
-
"@tiptap/extension-underline": "^3.
|
|
157
|
-
"@tiptap/react": "^3.
|
|
158
|
-
"@tiptap/starter-kit": "^3.
|
|
159
|
-
"html-react-parser": "^
|
|
160
|
-
"lucide-react": "^
|
|
148
|
+
"@tiptap/core": "^3.29.2",
|
|
149
|
+
"@tiptap/extension-color": "^3.29.2",
|
|
150
|
+
"@tiptap/extension-highlight": "^3.29.2",
|
|
151
|
+
"@tiptap/extension-link": "^3.29.2",
|
|
152
|
+
"@tiptap/extension-subscript": "^3.29.2",
|
|
153
|
+
"@tiptap/extension-superscript": "^3.29.2",
|
|
154
|
+
"@tiptap/extension-text-align": "^3.29.2",
|
|
155
|
+
"@tiptap/extension-text-style": "^3.29.2",
|
|
156
|
+
"@tiptap/extension-underline": "^3.29.2",
|
|
157
|
+
"@tiptap/react": "^3.29.2",
|
|
158
|
+
"@tiptap/starter-kit": "^3.29.2",
|
|
159
|
+
"html-react-parser": "^6.1.5",
|
|
160
|
+
"lucide-react": "^1.30.0"
|
|
161
161
|
},
|
|
162
162
|
"devDependencies": {
|
|
163
163
|
"@payloadcms/next": "^3.87.1",
|
|
164
164
|
"@payloadcms/ui": "^3.87.1",
|
|
165
165
|
"@puckeditor/core": "^0.23.0",
|
|
166
|
-
"@swc/cli": "^0.
|
|
167
|
-
"@swc/core": "^1.15.
|
|
168
|
-
"@types/node": "^
|
|
169
|
-
"@types/react": "^19.2.
|
|
170
|
-
"@types/react-dom": "^19.2.
|
|
171
|
-
"@vitest/coverage-v8": "^
|
|
166
|
+
"@swc/cli": "^0.8.1",
|
|
167
|
+
"@swc/core": "^1.15.47",
|
|
168
|
+
"@types/node": "^26.1.2",
|
|
169
|
+
"@types/react": "^19.2.18",
|
|
170
|
+
"@types/react-dom": "^19.2.4",
|
|
171
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
172
172
|
"copyfiles": "^2.4.1",
|
|
173
173
|
"next": "^16.3.0",
|
|
174
174
|
"payload": "^3.87.1",
|
|
175
|
-
"react": "^19.2.
|
|
176
|
-
"react-dom": "^19.2.
|
|
175
|
+
"react": "^19.2.8",
|
|
176
|
+
"react-dom": "^19.2.8",
|
|
177
177
|
"typescript": "^5.9.3",
|
|
178
|
-
"vitest": "^
|
|
178
|
+
"vitest": "^4.1.10"
|
|
179
179
|
},
|
|
180
180
|
"keywords": [
|
|
181
181
|
"payload",
|