@xignet/payload 0.0.0-stage → 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +118 -2
- package/dist/login.d.ts +7 -0
- package/dist/login.js +312 -0
- package/dist/native-session.d.ts +26 -0
- package/dist/native-session.js +305 -0
- package/dist/next.d.ts +10 -0
- package/dist/next.js +24 -0
- package/dist/rpc-audit.d.ts +3 -0
- package/dist/rpc-audit.js +115 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +1 -0
- package/dist/v3.d.ts +5 -0
- package/dist/v3.js +513 -0
- package/package.json +51 -3
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { errors, jwtVerify } from 'jose';
|
|
2
|
+
import { APIError, JWT_AUTH_VERSION, createLocalReq, getFieldsToSign, jwtSign, } from 'payload';
|
|
3
|
+
import { parseCookies } from 'payload/shared';
|
|
4
|
+
export const SESSION_COLLECTION = 'xignet-sessions';
|
|
5
|
+
export const SESSION_CLAIM = '__xignet_sso';
|
|
6
|
+
// Match Payload 3.93's credential precedence and CSRF rules. extractJWT is
|
|
7
|
+
// not a public export, so the pinned adapter tests native browser auth.
|
|
8
|
+
function selectedToken(req) {
|
|
9
|
+
for (const method of req.payload.config.auth.jwtOrder) {
|
|
10
|
+
if (method === 'Bearer' || method === 'JWT') {
|
|
11
|
+
const header = req.headers.get('authorization');
|
|
12
|
+
if (header?.startsWith(`${method} `))
|
|
13
|
+
return header.slice(method.length + 1);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const token = parseCookies(req.headers).get(`${req.payload.config.cookiePrefix}-token`);
|
|
17
|
+
if (!token)
|
|
18
|
+
continue;
|
|
19
|
+
const origin = req.headers.get('origin');
|
|
20
|
+
const csrf = req.payload.config.csrf;
|
|
21
|
+
if (origin
|
|
22
|
+
? csrf.length === 0 || csrf.includes(origin)
|
|
23
|
+
: csrf.length === 0 ||
|
|
24
|
+
['same-origin', 'same-site', 'none'].includes(req.headers.get('sec-fetch-site') ?? ''))
|
|
25
|
+
return token;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
async function reference(req) {
|
|
31
|
+
const token = selectedToken(req);
|
|
32
|
+
if (!token)
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const result = await jwtVerify(token, new TextEncoder().encode(req.payload.secret), { algorithms: ['HS256'], requiredClaims: ['exp'] });
|
|
36
|
+
return result.protectedHeader.authVersion === JWT_AUTH_VERSION &&
|
|
37
|
+
result.payload[SESSION_CLAIM] === true
|
|
38
|
+
? result.payload
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof errors.JOSEError)
|
|
43
|
+
return undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function createNativeSessions(options) {
|
|
48
|
+
const maxAge = options.login?.session.maxAgeSeconds ?? 7200;
|
|
49
|
+
const absoluteMaxAge = options.login?.session.absoluteMaxAgeSeconds ?? 7200;
|
|
50
|
+
if (![maxAge, absoluteMaxAge].every((n) => Number.isInteger(n) && n > 0 && n <= 604800) ||
|
|
51
|
+
maxAge > absoluteMaxAge)
|
|
52
|
+
throw new Error('Invalid SSO session lifetimes');
|
|
53
|
+
const record = async (req, sid) => typeof sid === 'string'
|
|
54
|
+
? req.payload.db.findOne({
|
|
55
|
+
collection: SESSION_COLLECTION,
|
|
56
|
+
req,
|
|
57
|
+
where: { sid: { equals: sid } },
|
|
58
|
+
})
|
|
59
|
+
: null;
|
|
60
|
+
const allowed = async (req, entry, userId) => {
|
|
61
|
+
if (entry.user !== userId ||
|
|
62
|
+
entry.resource !== options.resource ||
|
|
63
|
+
entry.identity?.issuer !== options.issuer ||
|
|
64
|
+
!Number.isFinite(Date.parse(entry.absoluteExpiresAt)) ||
|
|
65
|
+
Date.parse(entry.absoluteExpiresAt) <= Date.now())
|
|
66
|
+
return false;
|
|
67
|
+
const resolved = await options.resolveUser({
|
|
68
|
+
identity: entry.identity,
|
|
69
|
+
channel: 'session',
|
|
70
|
+
payload: req.payload,
|
|
71
|
+
});
|
|
72
|
+
return (resolved.status === 'allowed' &&
|
|
73
|
+
resolved.user.id === userId &&
|
|
74
|
+
resolved.user.collection === options.userCollection);
|
|
75
|
+
};
|
|
76
|
+
const collection = {
|
|
77
|
+
slug: SESSION_COLLECTION,
|
|
78
|
+
admin: { hidden: true },
|
|
79
|
+
access: {
|
|
80
|
+
create: () => false,
|
|
81
|
+
read: () => false,
|
|
82
|
+
update: () => false,
|
|
83
|
+
delete: () => false,
|
|
84
|
+
},
|
|
85
|
+
fields: [
|
|
86
|
+
{ name: 'sid', type: 'text', required: true, unique: true },
|
|
87
|
+
{ name: 'resource', type: 'text', required: true },
|
|
88
|
+
{
|
|
89
|
+
name: 'user',
|
|
90
|
+
type: 'relationship',
|
|
91
|
+
relationTo: options.userCollection,
|
|
92
|
+
required: true,
|
|
93
|
+
index: true,
|
|
94
|
+
},
|
|
95
|
+
{ name: 'absoluteExpiresAt', type: 'date', required: true, index: true },
|
|
96
|
+
{ name: 'identity', type: 'json', required: true },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
const revokeUserSessions = async ({ payload, userId, req: existingReq, }) => {
|
|
100
|
+
const req = existingReq ?? (await createLocalReq({}, payload));
|
|
101
|
+
const entries = await payload.find({
|
|
102
|
+
collection: SESSION_COLLECTION,
|
|
103
|
+
req,
|
|
104
|
+
overrideAccess: true,
|
|
105
|
+
pagination: false,
|
|
106
|
+
depth: 0,
|
|
107
|
+
where: { user: { equals: userId } },
|
|
108
|
+
});
|
|
109
|
+
const ids = new Set(entries.docs.map((doc) => doc.sid));
|
|
110
|
+
const user = (await payload.db.findOne({
|
|
111
|
+
collection: options.userCollection,
|
|
112
|
+
req,
|
|
113
|
+
where: { id: { equals: userId } },
|
|
114
|
+
}));
|
|
115
|
+
if (user)
|
|
116
|
+
await payload.db.updateOne({
|
|
117
|
+
collection: options.userCollection,
|
|
118
|
+
id: userId,
|
|
119
|
+
req,
|
|
120
|
+
returning: false,
|
|
121
|
+
data: {
|
|
122
|
+
sessions: (user.sessions ?? []).filter((s) => !ids.has(s.id)),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
await payload.delete({
|
|
126
|
+
collection: SESSION_COLLECTION,
|
|
127
|
+
req,
|
|
128
|
+
overrideAccess: true,
|
|
129
|
+
where: { user: { equals: userId } },
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
const revokeSession = async (req, userId, sid) => {
|
|
133
|
+
const current = (await req.payload.db.findOne({
|
|
134
|
+
collection: options.userCollection,
|
|
135
|
+
req,
|
|
136
|
+
where: { id: { equals: userId } },
|
|
137
|
+
}));
|
|
138
|
+
if (current)
|
|
139
|
+
await req.payload.db.updateOne({
|
|
140
|
+
collection: options.userCollection,
|
|
141
|
+
id: userId,
|
|
142
|
+
req,
|
|
143
|
+
returning: false,
|
|
144
|
+
data: {
|
|
145
|
+
sessions: (current.sessions ?? []).filter((s) => s.id !== sid),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
await req.payload.delete({
|
|
149
|
+
collection: SESSION_COLLECTION,
|
|
150
|
+
req,
|
|
151
|
+
overrideAccess: true,
|
|
152
|
+
where: { sid: { equals: sid } },
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const protectSession = async ({ doc, req }) => {
|
|
156
|
+
const ref = await reference(req);
|
|
157
|
+
if (!ref || ref.collection !== options.userCollection || ref.id !== doc.id)
|
|
158
|
+
return doc;
|
|
159
|
+
if (!(doc.sessions ?? []).some((s) => s.id === ref.sid))
|
|
160
|
+
return doc;
|
|
161
|
+
try {
|
|
162
|
+
const entry = await record(req, ref.sid);
|
|
163
|
+
if (entry && (await allowed(req, entry, doc.id)))
|
|
164
|
+
return doc;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Payload's native JWT strategy catches auth failures. Log the service
|
|
168
|
+
// failure here so an outage cannot silently look like an expired login.
|
|
169
|
+
req.payload.logger.error({
|
|
170
|
+
event: 'xignet_session_verification_failed',
|
|
171
|
+
category: error instanceof Error ? error.name : 'unknown',
|
|
172
|
+
});
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
// Revocation must survive re-enabling/rebinding the account. Merely
|
|
176
|
+
// filtering this one auth response would let an old cookie revive later.
|
|
177
|
+
await revokeSession(req, doc.id, ref.sid);
|
|
178
|
+
req.payload.logger.info({
|
|
179
|
+
event: 'xignet_session_revoked',
|
|
180
|
+
userId: doc.id,
|
|
181
|
+
reason: 'expired_disabled_or_unbound',
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
...doc,
|
|
185
|
+
sessions: (doc.sessions ?? []).filter((s) => s.id !== ref.sid),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
const enhanceUser = (config) => {
|
|
189
|
+
const auth = typeof config.auth === 'object' ? config.auth : {};
|
|
190
|
+
if (!config.auth ||
|
|
191
|
+
auth.disableLocalStrategy ||
|
|
192
|
+
auth.useSessions === false) {
|
|
193
|
+
if (options.login?.enabled)
|
|
194
|
+
throw new Error('SSO requires native Payload auth with sessions');
|
|
195
|
+
return config;
|
|
196
|
+
}
|
|
197
|
+
const previousRefresh = config.hooks?.refresh ?? [];
|
|
198
|
+
const refresh = async (args) => {
|
|
199
|
+
const ref = await reference(args.args.req);
|
|
200
|
+
if (!ref) {
|
|
201
|
+
for (const hook of previousRefresh) {
|
|
202
|
+
const result = await hook(args);
|
|
203
|
+
if (result)
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const req = args.args.req;
|
|
209
|
+
const entry = await record(req, ref.sid);
|
|
210
|
+
if (!entry || !(await allowed(req, entry, args.user.id)))
|
|
211
|
+
throw new APIError('SSO session expired or revoked', 403);
|
|
212
|
+
let prior;
|
|
213
|
+
for (const hook of previousRefresh) {
|
|
214
|
+
prior = await hook(args);
|
|
215
|
+
if (prior)
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
const user = prior?.user ?? args.user;
|
|
219
|
+
if (user.id !== entry.user)
|
|
220
|
+
throw new APIError('SSO user mismatch', 403);
|
|
221
|
+
const ttl = Math.min(maxAge, Math.floor(Date.parse(entry.absoluteExpiresAt) / 1000) -
|
|
222
|
+
Math.floor(Date.now() / 1000));
|
|
223
|
+
if (ttl < 1)
|
|
224
|
+
throw new APIError('SSO session expired', 403);
|
|
225
|
+
const signed = await jwtSign({
|
|
226
|
+
fieldsToSign: {
|
|
227
|
+
...getFieldsToSign({
|
|
228
|
+
collectionConfig: args.args.collection.config,
|
|
229
|
+
user,
|
|
230
|
+
email: user.email,
|
|
231
|
+
sid: entry.sid,
|
|
232
|
+
}),
|
|
233
|
+
[SESSION_CLAIM]: true,
|
|
234
|
+
},
|
|
235
|
+
secret: req.payload.secret,
|
|
236
|
+
tokenExpiration: ttl,
|
|
237
|
+
});
|
|
238
|
+
const current = (await req.payload.db.findOne({
|
|
239
|
+
collection: options.userCollection,
|
|
240
|
+
req,
|
|
241
|
+
where: { id: { equals: user.id } },
|
|
242
|
+
}));
|
|
243
|
+
if (!current?.sessions?.some((s) => s.id === entry.sid))
|
|
244
|
+
throw new APIError('SSO session revoked', 403);
|
|
245
|
+
await req.payload.db.updateOne({
|
|
246
|
+
collection: options.userCollection,
|
|
247
|
+
id: user.id,
|
|
248
|
+
req,
|
|
249
|
+
returning: false,
|
|
250
|
+
data: {
|
|
251
|
+
sessions: current.sessions.map((s) => s.id === entry.sid
|
|
252
|
+
? { ...s, expiresAt: new Date(signed.exp * 1000) }
|
|
253
|
+
: s),
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
// A refresh hook runs before Payload applies user read/field access.
|
|
257
|
+
return {
|
|
258
|
+
...prior,
|
|
259
|
+
exp: signed.exp,
|
|
260
|
+
refreshedToken: signed.token,
|
|
261
|
+
user,
|
|
262
|
+
setCookie: true,
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
...config,
|
|
267
|
+
auth: {
|
|
268
|
+
...auth,
|
|
269
|
+
useSessions: true,
|
|
270
|
+
cookies: {
|
|
271
|
+
...auth.cookies,
|
|
272
|
+
...(options.login?.enabled &&
|
|
273
|
+
new URL(options.resource).protocol === 'https:'
|
|
274
|
+
? { secure: true }
|
|
275
|
+
: {}),
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
hooks: {
|
|
279
|
+
...config.hooks,
|
|
280
|
+
afterRead: [...(config.hooks?.afterRead ?? []), protectSession],
|
|
281
|
+
refresh: [refresh],
|
|
282
|
+
afterLogout: [
|
|
283
|
+
...(config.hooks?.afterLogout ?? []),
|
|
284
|
+
async ({ req }) => {
|
|
285
|
+
if (req.user?._sid)
|
|
286
|
+
await req.payload.delete({
|
|
287
|
+
collection: SESSION_COLLECTION,
|
|
288
|
+
req,
|
|
289
|
+
overrideAccess: true,
|
|
290
|
+
where: { sid: { equals: req.user._sid } },
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
collection,
|
|
299
|
+
revokeSession,
|
|
300
|
+
enhanceUser,
|
|
301
|
+
revokeUserSessions,
|
|
302
|
+
maxAge,
|
|
303
|
+
absoluteMaxAge,
|
|
304
|
+
};
|
|
305
|
+
}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { XignetPayloadIntegration } from './types.js';
|
|
3
|
+
/** Standard Request/Response handlers for Next.js server route modules. */
|
|
4
|
+
export declare function createXignetRouteHandlers(integration: XignetPayloadIntegration, options?: {
|
|
5
|
+
getPayload: () => Promise<Payload>;
|
|
6
|
+
}): {
|
|
7
|
+
metadata: () => Promise<Response>;
|
|
8
|
+
login: (request: Request) => Promise<Response>;
|
|
9
|
+
callback: (request: Request) => Promise<Response>;
|
|
10
|
+
};
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Standard Request/Response handlers for Next.js server route modules. */
|
|
2
|
+
export function createXignetRouteHandlers(integration, options) {
|
|
3
|
+
if (integration.loginEnabled && !options)
|
|
4
|
+
throw new Error('SSO routes require a getPayload provider');
|
|
5
|
+
return {
|
|
6
|
+
metadata: async () => integration.metadata(),
|
|
7
|
+
login: (request) => integration.login(request),
|
|
8
|
+
callback: async (request) => {
|
|
9
|
+
if (!integration.loginEnabled || !options)
|
|
10
|
+
return new Response(null, { status: 404 });
|
|
11
|
+
const payload = await options.getPayload();
|
|
12
|
+
if (!payload.config.serverURL)
|
|
13
|
+
return integration.callback(request, payload);
|
|
14
|
+
// Standalone Next requests may use an internal hostname and port.
|
|
15
|
+
// Only configured serverURL supplies the public origin, never headers.
|
|
16
|
+
const incoming = new URL(request.url);
|
|
17
|
+
const callback = new URL(payload.config.serverURL);
|
|
18
|
+
callback.pathname = incoming.pathname;
|
|
19
|
+
callback.search = incoming.search;
|
|
20
|
+
callback.hash = incoming.hash;
|
|
21
|
+
return integration.callback(new Request(callback, request), payload);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function inspectRpc(message: Request): Promise<Record<string, unknown> | undefined>;
|
|
2
|
+
export declare function observeRpc(response: Response, complete: (rpc: Record<string, unknown> | undefined, completed: boolean) => void): ReadableStream<Uint8Array> | null;
|
|
3
|
+
export declare function rpcMethod(value: unknown): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const byteLimit = 64 * 1024;
|
|
2
|
+
function parseRpc(chunks, contentType) {
|
|
3
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
4
|
+
const json = contentType?.includes('text/event-stream')
|
|
5
|
+
? text
|
|
6
|
+
.split(/\r?\n/)
|
|
7
|
+
.filter((line) => line.startsWith('data:'))
|
|
8
|
+
.map((line) => line.slice(5).trimStart())
|
|
9
|
+
.join('\n')
|
|
10
|
+
: text;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(json);
|
|
13
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
14
|
+
? parsed
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Inspect a bounded request clone without consuming the transport's body.
|
|
22
|
+
export async function inspectRpc(message) {
|
|
23
|
+
const reader = message.body?.getReader();
|
|
24
|
+
if (!reader)
|
|
25
|
+
return;
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let size = 0;
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
const { done, value } = await reader.read();
|
|
31
|
+
if (done)
|
|
32
|
+
break;
|
|
33
|
+
size += value.byteLength;
|
|
34
|
+
if (size > byteLimit) {
|
|
35
|
+
// Awaiting one tee branch's cancellation can wait for the other one.
|
|
36
|
+
void reader.cancel().catch(() => undefined);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
chunks.push(value);
|
|
40
|
+
}
|
|
41
|
+
return parseRpc(chunks, message.headers.get('content-type'));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
reader.releaseLock();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Observe only bytes the consumer requests. Forward every chunk immediately;
|
|
51
|
+
// EOF, source error and cancellation each produce exactly one terminal record.
|
|
52
|
+
export function observeRpc(response, complete) {
|
|
53
|
+
const reader = response.body?.getReader();
|
|
54
|
+
if (!reader) {
|
|
55
|
+
complete(undefined, true);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let chunks = [];
|
|
59
|
+
let size = 0;
|
|
60
|
+
let finished = false;
|
|
61
|
+
const finish = (valid) => {
|
|
62
|
+
if (finished)
|
|
63
|
+
return;
|
|
64
|
+
finished = true;
|
|
65
|
+
complete(valid && size <= byteLimit
|
|
66
|
+
? parseRpc(chunks, response.headers.get('content-type'))
|
|
67
|
+
: undefined, valid);
|
|
68
|
+
chunks = [];
|
|
69
|
+
};
|
|
70
|
+
return new ReadableStream({
|
|
71
|
+
async pull(controller) {
|
|
72
|
+
try {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) {
|
|
75
|
+
finish(true);
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
controller.close();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
size += value.byteLength;
|
|
81
|
+
if (size <= byteLimit)
|
|
82
|
+
chunks.push(value);
|
|
83
|
+
else
|
|
84
|
+
chunks = [];
|
|
85
|
+
controller.enqueue(value);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
finish(false);
|
|
89
|
+
reader.releaseLock();
|
|
90
|
+
controller.error(error);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async cancel(reason) {
|
|
94
|
+
finish(false);
|
|
95
|
+
try {
|
|
96
|
+
await reader.cancel(reason);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
reader.releaseLock();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function rpcMethod(value) {
|
|
105
|
+
return typeof value === 'string' &&
|
|
106
|
+
[
|
|
107
|
+
'initialize',
|
|
108
|
+
'ping',
|
|
109
|
+
'tools/list',
|
|
110
|
+
'tools/call',
|
|
111
|
+
'notifications/initialized',
|
|
112
|
+
].includes(value)
|
|
113
|
+
? value
|
|
114
|
+
: 'unknown';
|
|
115
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { TokenVerifierOptions, VerifiedIdentity } from '@xignet/core';
|
|
2
|
+
import type { CollectionSlug, Payload, PayloadRequest, Plugin, TypedUser } from 'payload';
|
|
3
|
+
export type UserResolution<TUser extends TypedUser = TypedUser> = {
|
|
4
|
+
readonly status: 'allowed';
|
|
5
|
+
readonly user: TUser;
|
|
6
|
+
} | {
|
|
7
|
+
readonly status: 'denied';
|
|
8
|
+
readonly reason: 'unbound' | 'disabled' | 'not_allowed';
|
|
9
|
+
};
|
|
10
|
+
export type ResolveUser<TUser extends TypedUser = TypedUser> = (input: {
|
|
11
|
+
identity: VerifiedIdentity;
|
|
12
|
+
channel: 'login' | 'mcp' | 'session';
|
|
13
|
+
payload: Payload;
|
|
14
|
+
login?: {
|
|
15
|
+
idTokenClaims: Readonly<Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<UserResolution<TUser>>;
|
|
18
|
+
export type MCPOperation = 'find' | 'create' | 'update';
|
|
19
|
+
export interface OperationPolicy {
|
|
20
|
+
readonly scopes: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export interface MutationInput {
|
|
23
|
+
readonly operation: 'create' | 'update';
|
|
24
|
+
readonly data: Record<string, unknown>;
|
|
25
|
+
readonly originalDoc?: Record<string, unknown>;
|
|
26
|
+
readonly req: PayloadRequest;
|
|
27
|
+
readonly identity: VerifiedIdentity;
|
|
28
|
+
}
|
|
29
|
+
/** Return additional required scopes, or false to refuse the mutation. */
|
|
30
|
+
export type MutationPolicy = (input: MutationInput) => readonly string[] | false | Promise<readonly string[] | false>;
|
|
31
|
+
export interface EntityPolicy {
|
|
32
|
+
readonly find?: OperationPolicy;
|
|
33
|
+
readonly create?: OperationPolicy;
|
|
34
|
+
readonly update?: OperationPolicy;
|
|
35
|
+
readonly mutationPolicy?: MutationPolicy;
|
|
36
|
+
}
|
|
37
|
+
export interface XignetPayloadOptions<TUser extends TypedUser = TypedUser> extends Omit<TokenVerifierOptions, 'audience'> {
|
|
38
|
+
readonly resource: string;
|
|
39
|
+
readonly admissionScopes: readonly string[];
|
|
40
|
+
readonly userCollection: CollectionSlug;
|
|
41
|
+
readonly resolveUser: ResolveUser<TUser>;
|
|
42
|
+
readonly login?: {
|
|
43
|
+
readonly enabled: boolean;
|
|
44
|
+
readonly clientId: string;
|
|
45
|
+
readonly redirectUri: string;
|
|
46
|
+
readonly scopes: readonly string[];
|
|
47
|
+
readonly transactionSecret: string;
|
|
48
|
+
readonly successRedirect: string;
|
|
49
|
+
readonly session: {
|
|
50
|
+
readonly maxAgeSeconds: number;
|
|
51
|
+
readonly absoluteMaxAgeSeconds: number;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
readonly mcp: {
|
|
55
|
+
readonly enabled: boolean;
|
|
56
|
+
/** Disable mutation capabilities without changing configured schemas. Defaults to true. */
|
|
57
|
+
readonly writesEnabled?: boolean;
|
|
58
|
+
readonly collections: Readonly<Record<string, EntityPolicy>>;
|
|
59
|
+
readonly globals?: Readonly<Record<string, Omit<EntityPolicy, 'create'>>>;
|
|
60
|
+
/** Include scopes returned dynamically by mutationPolicy. */
|
|
61
|
+
readonly scopesSupported?: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface XignetPayloadIntegration {
|
|
65
|
+
readonly loginEnabled: boolean;
|
|
66
|
+
plugin(): Plugin;
|
|
67
|
+
/** HTTP metadata response; mount at the resource's well-known path. */
|
|
68
|
+
metadata(): Response;
|
|
69
|
+
login(request: Request): Promise<Response>;
|
|
70
|
+
callback(request: Request, payload: Payload): Promise<Response>;
|
|
71
|
+
revokeUserSessions(input: {
|
|
72
|
+
payload: Payload;
|
|
73
|
+
userId: string | number;
|
|
74
|
+
req?: PayloadRequest;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/v3.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type TypedUser } from 'payload';
|
|
2
|
+
import type { XignetPayloadIntegration, XignetPayloadOptions } from './types.js';
|
|
3
|
+
export type * from './types.js';
|
|
4
|
+
/** Compose official v3 MCP transport with request-scoped Xignet authorization. */
|
|
5
|
+
export declare function createXignetPayload<TUser extends TypedUser>(input: XignetPayloadOptions<TUser>): XignetPayloadIntegration;
|