@xignet/payload 0.1.0-beta.2 → 0.1.0-beta.3
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 +2 -3
- package/dist/rest.d.ts +0 -2
- package/dist/rest.js +7 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ Directory audit records use generated `x-request-id` correlation IDs, local acto
|
|
|
165
165
|
|
|
166
166
|
### REST access tokens and API-key migration
|
|
167
167
|
|
|
168
|
-
REST is opt-in and
|
|
168
|
+
REST is opt-in and shares the CMS `resource` audience with MCP, normally `https://cms.example.com/api/mcp`. Directory has its own audience. Configure `rest.enabled`, `admissionScopes`, explicit `collections` / `globals` operation scopes, and optional exact custom `endpoints`. REST and MCP each enforce their own route, scope and local access policies. Only document GET, collection POST, document PATCH and global GET/POST are supported by the REST adapter. Delete, bulk update, auth, versions and GraphQL routes are denied for REST Bearer tokens.
|
|
169
169
|
|
|
170
170
|
```ts
|
|
171
171
|
const integration = xignetPlugin({
|
|
@@ -173,7 +173,6 @@ const integration = xignetPlugin({
|
|
|
173
173
|
legacyAPIKeys: true, // temporary migration bridge; default is false
|
|
174
174
|
rest: {
|
|
175
175
|
enabled: true,
|
|
176
|
-
resource: 'https://cms.example.com/api',
|
|
177
176
|
admissionScopes: ['cms:access'],
|
|
178
177
|
collections: {
|
|
179
178
|
pages: { read: ['cms:read'], create: ['cms:write', 'cms:publish'], update: ['cms:write', 'cms:publish'] },
|
|
@@ -187,4 +186,4 @@ const integration = xignetPlugin({
|
|
|
187
186
|
|
|
188
187
|
Human subjects resolve through the managed local binding and eligibility policy on every request. Services must have the reserved `service:` subject equal to `client_id` and match the local `rest.services` allowlist. Their principal has collection `xignet-services` and no human role, password or session; this is an in-memory principal, not a new database collection. Existing collection/global access control is preserved. Where appropriate, explicitly allow `isXignetService(req)` in the application's **content** access policy; the adapter still caps access to configured operation scopes. Never add this exception to user management or admin access. Field access rules also remain in force.
|
|
189
188
|
|
|
190
|
-
Keep `legacyAPIKeys: true` only while existing consumers migrate. It does not re-enable passwords. Deploy REST validation first, register environment-specific machine clients for the
|
|
189
|
+
Keep `legacyAPIKeys: true` only while existing consumers migrate. It does not re-enable passwords. Deploy REST validation first, register environment-specific machine clients for the shared CMS audience and least required scopes, migrate consumers, verify their reads/writes/uploads, then set `legacyAPIKeys: false`. No automatic token-to-API-key fallback exists. Machine authorization revocation at the AS is bounded by the access token's lifetime; removing the local service allowlist entry takes effect with the new configuration. No remote introspection is performed.
|
package/dist/rest.d.ts
CHANGED
|
@@ -7,8 +7,6 @@ export interface RestEntityPolicy {
|
|
|
7
7
|
}
|
|
8
8
|
export interface RestOptions {
|
|
9
9
|
readonly enabled: boolean;
|
|
10
|
-
/** Separate audience from MCP, e.g. https://cms.example.com/api. */
|
|
11
|
-
readonly resource: string;
|
|
12
10
|
readonly admissionScopes: readonly string[];
|
|
13
11
|
readonly collections: Readonly<Record<string, RestEntityPolicy>>;
|
|
14
12
|
readonly globals?: Readonly<Record<string, Omit<RestEntityPolicy, 'create'>>>;
|
package/dist/rest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { createTokenVerifier, hasAllScopes,
|
|
3
|
+
import { createTokenVerifier, hasAllScopes, } from '@xignet/core';
|
|
4
4
|
import { validateScopes } from '@xignet/core/oauth';
|
|
5
5
|
const principals = new WeakMap();
|
|
6
6
|
const key = '__xignetRest';
|
|
@@ -54,17 +54,14 @@ export function createRest(options) {
|
|
|
54
54
|
if (!service.subject.startsWith('service:') ||
|
|
55
55
|
service.subject !== service.clientId)
|
|
56
56
|
throw new Error('Xignet service subject must equal its service client ID');
|
|
57
|
-
const resource = trustedUrl(rest.resource, options.allowInsecureLoopback);
|
|
58
|
-
if (resource.origin !== new URL(options.resource).origin ||
|
|
59
|
-
rest.resource === options.resource)
|
|
60
|
-
throw new Error('REST requires a separate audience on the CMS origin');
|
|
61
57
|
}
|
|
62
|
-
const verifier = rest && createTokenVerifier({ ...options, audience:
|
|
58
|
+
const verifier = rest && createTokenVerifier({ ...options, audience: options.resource });
|
|
59
|
+
const apiRoot = new URL(options.resource).pathname.replace(/\/mcp$/, '');
|
|
63
60
|
function route(request) {
|
|
64
61
|
if (!rest)
|
|
65
62
|
return;
|
|
66
63
|
const path = new URL(request.url).pathname;
|
|
67
|
-
const root =
|
|
64
|
+
const root = apiRoot;
|
|
68
65
|
if (!path.startsWith(`${root}/`) || /%|\\|\/\//.test(path))
|
|
69
66
|
return;
|
|
70
67
|
const relative = path.slice(root.length);
|
|
@@ -144,7 +141,7 @@ export function createRest(options) {
|
|
|
144
141
|
};
|
|
145
142
|
return {
|
|
146
143
|
async handle(request, payload, next) {
|
|
147
|
-
// MCP owns its own Bearer validation and
|
|
144
|
+
// MCP owns its own Bearer validation and route policy.
|
|
148
145
|
if (!bearer(request.headers) ||
|
|
149
146
|
new URL(request.url).pathname === new URL(options.resource).pathname)
|
|
150
147
|
return next();
|
|
@@ -202,8 +199,8 @@ export function createRest(options) {
|
|
|
202
199
|
enhance(config) {
|
|
203
200
|
if (!rest?.enabled)
|
|
204
201
|
return config;
|
|
205
|
-
if (
|
|
206
|
-
throw new Error('REST
|
|
202
|
+
if (apiRoot !== (config.routes?.api ?? '/api'))
|
|
203
|
+
throw new Error('REST API root must match the Payload API route');
|
|
207
204
|
for (const slug of Object.keys(rest.collections)) {
|
|
208
205
|
const collection = config.collections?.find((c) => c.slug === slug);
|
|
209
206
|
if (!collection || collection.auth)
|