@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 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 uses a **separate audience**, normally `https://cms.example.com/api`; an MCP or Directory token is not a REST credential. Configure `rest.enabled`, `resource`, `admissionScopes`, explicit `collections` / `globals` operation scopes, and optional exact custom `endpoints`. Only document GET, collection POST, document PATCH and global GET/POST are supported. Delete, bulk update, auth, versions and GraphQL routes are denied for REST Bearer tokens.
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 REST 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.
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, trustedUrl, } from '@xignet/core';
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: rest.resource });
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 = new URL(rest.resource).pathname;
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 audience.
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 (new URL(rest.resource).pathname !== (config.routes?.api ?? '/api'))
206
- throw new Error('REST resource must equal the Payload API root');
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xignet/payload",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "type": "module",
5
5
  "description": "Xignet OAuth resource and OIDC integration for Payload v3.",
6
6
  "license": "MIT",