@payloadcms/figma 0.0.1-alpha.64 → 0.0.1-alpha.66
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/dist/auth/callback-server.d.ts +19 -7
- package/dist/auth/callback-server.js +72 -31
- package/dist/auth/crypto-utils.d.ts +11 -0
- package/dist/auth/crypto-utils.js +22 -1
- package/dist/auth/oauth-flow.d.ts +3 -1
- package/dist/auth/oauth-flow.js +13 -6
- package/dist/auth/project-token.js +2 -2
- package/dist/auth/token-store.d.ts +2 -0
- package/dist/auth/token-store.js +43 -3
- package/dist/auth/types.d.ts +7 -0
- package/dist/cli.js +6 -1
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +44 -2
- package/dist/config/oauth.d.ts +9 -1
- package/dist/config/oauth.js +13 -1
- package/dist/db-content-api/generated/content-api-types.d.ts +6 -0
- package/dist/db-content-api/index.d.ts +2 -0
- package/dist/db-content-api/index.js +9 -1
- package/dist/lib/download-skill.d.ts +13 -0
- package/dist/lib/download-skill.js +79 -0
- package/dist/oauth/endpoints/getLoginEndpoint.js +26 -107
- package/dist/oauth/endpoints/getTokenLoginEndpoint.d.ts +17 -0
- package/dist/oauth/endpoints/getTokenLoginEndpoint.js +105 -0
- package/dist/oauth/index.js +8 -0
- package/dist/oauth/utilities/establishSession.d.ts +23 -0
- package/dist/oauth/utilities/establishSession.js +82 -0
- package/dist/oauth/utilities/exchangeCodeForAccessToken.d.ts +24 -0
- package/dist/oauth/utilities/exchangeCodeForAccessToken.js +28 -0
- package/dist/oauth/utilities/isAbsoluteURL.d.ts +2 -0
- package/dist/oauth/utilities/isAbsoluteURL.js +3 -0
- package/dist/plugin/build-config.js +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/download-template.d.ts +9 -1
- package/dist/utils/download-template.js +24 -19
- package/dist/utils/messages.js +2 -0
- package/dist/utils/parse-template-spec.d.ts +12 -0
- package/dist/utils/parse-template-spec.js +62 -0
- package/dist/utils/project.d.ts +2 -1
- package/dist/utils/project.js +2 -2
- package/package.json +9 -1
- package/dist/db-content-api/README.md +0 -98
|
@@ -3483,6 +3483,8 @@ export type components = {
|
|
|
3483
3483
|
CreateDocumentRequest: {
|
|
3484
3484
|
/** @example cms-xxxxx-xxxxx */
|
|
3485
3485
|
contentSystemId: string;
|
|
3486
|
+
/** User-defined Payload environment name (e.g. "production"). */
|
|
3487
|
+
environmentName?: string;
|
|
3486
3488
|
/** @example posts */
|
|
3487
3489
|
collection: string;
|
|
3488
3490
|
doc: components['schemas']['DocumentData'];
|
|
@@ -3594,6 +3596,8 @@ export type components = {
|
|
|
3594
3596
|
UpdateDocumentRequest: {
|
|
3595
3597
|
/** @example cms-xxxxx-xxxxx */
|
|
3596
3598
|
contentSystemId: string;
|
|
3599
|
+
/** User-defined Payload environment name (e.g. "production"). */
|
|
3600
|
+
environmentName?: string;
|
|
3597
3601
|
/** @example posts */
|
|
3598
3602
|
collection: string;
|
|
3599
3603
|
/** @example false */
|
|
@@ -3627,6 +3631,8 @@ export type components = {
|
|
|
3627
3631
|
DeleteDocumentRequest: {
|
|
3628
3632
|
/** @example cms-xxxxx-xxxxx */
|
|
3629
3633
|
contentSystemId: string;
|
|
3634
|
+
/** User-defined Payload environment name (e.g. "production"). */
|
|
3635
|
+
environmentName?: string;
|
|
3630
3636
|
/** @example posts */
|
|
3631
3637
|
collection: string;
|
|
3632
3638
|
locale?: components['schemas']['LocaleClause'];
|
|
@@ -6,6 +6,7 @@ type ContentAPIOptions = {
|
|
|
6
6
|
allowIDOnCreate?: boolean;
|
|
7
7
|
auth: AuthMode;
|
|
8
8
|
contentSystemId: string;
|
|
9
|
+
environmentName?: string;
|
|
9
10
|
url: string;
|
|
10
11
|
};
|
|
11
12
|
export type ContentAPIAdapter = {
|
|
@@ -13,6 +14,7 @@ export type ContentAPIAdapter = {
|
|
|
13
14
|
clearDatabase: () => Promise<void>;
|
|
14
15
|
client: ReturnType<typeof createClient<paths>>;
|
|
15
16
|
contentSystemId: string;
|
|
17
|
+
environmentName?: string;
|
|
16
18
|
idType: 'uuid';
|
|
17
19
|
url: string;
|
|
18
20
|
} & BaseDatabaseAdapter;
|
|
@@ -213,7 +213,7 @@ async function createVersion(args) {
|
|
|
213
213
|
contentSystemId: this.contentSystemId,
|
|
214
214
|
versionDoc: {
|
|
215
215
|
createdAt: args.createdAt,
|
|
216
|
-
latest:
|
|
216
|
+
latest: true,
|
|
217
217
|
parent: String(args.parent),
|
|
218
218
|
updatedAt: args.updatedAt,
|
|
219
219
|
version: dataToContentAPI(this.payload, args.collectionSlug, args.versionData, {
|
|
@@ -365,6 +365,7 @@ async function updateMany(args) {
|
|
|
365
365
|
contentSystemId: this.contentSystemId,
|
|
366
366
|
createOnMissing: false,
|
|
367
367
|
doc: dataToContentAPI(this.payload, args.collection, args.data),
|
|
368
|
+
environmentName: this.environmentName,
|
|
368
369
|
limit: args.limit,
|
|
369
370
|
locale,
|
|
370
371
|
returning: {},
|
|
@@ -411,6 +412,7 @@ async function updateOne(args) {
|
|
|
411
412
|
contentSystemId: this.contentSystemId,
|
|
412
413
|
createOnMissing: false,
|
|
413
414
|
doc: dataToContentAPI(this.payload, args.collection, args.data),
|
|
415
|
+
environmentName: this.environmentName,
|
|
414
416
|
locale,
|
|
415
417
|
returning: {},
|
|
416
418
|
where: convertPayloadWhereToContentAPI(where),
|
|
@@ -447,6 +449,7 @@ async function deleteMany(args) {
|
|
|
447
449
|
body: {
|
|
448
450
|
collection: args.collection,
|
|
449
451
|
contentSystemId: this.contentSystemId,
|
|
452
|
+
environmentName: this.environmentName,
|
|
450
453
|
locale,
|
|
451
454
|
where: convertPayloadWhereToContentAPI(args.where),
|
|
452
455
|
...buildMeta(this.payload, {
|
|
@@ -466,6 +469,7 @@ async function deleteOne(args) {
|
|
|
466
469
|
body: {
|
|
467
470
|
collection: args.collection,
|
|
468
471
|
contentSystemId: this.contentSystemId,
|
|
472
|
+
environmentName: this.environmentName,
|
|
469
473
|
locale,
|
|
470
474
|
returning: {},
|
|
471
475
|
where: convertPayloadWhereToContentAPI(args.where),
|
|
@@ -525,6 +529,7 @@ async function create(args) {
|
|
|
525
529
|
}),
|
|
526
530
|
id
|
|
527
531
|
},
|
|
532
|
+
environmentName: this.environmentName,
|
|
528
533
|
locale,
|
|
529
534
|
...buildMeta(this.payload, {
|
|
530
535
|
collection: args.collection,
|
|
@@ -643,6 +648,7 @@ async function upsert(args) {
|
|
|
643
648
|
applyDefaults: true,
|
|
644
649
|
createdAt: true
|
|
645
650
|
}),
|
|
651
|
+
environmentName: this.environmentName,
|
|
646
652
|
locale,
|
|
647
653
|
where: convertPayloadWhereToContentAPI(args.where),
|
|
648
654
|
...buildMeta(this.payload, {
|
|
@@ -708,6 +714,7 @@ async function updateJobs(args) {
|
|
|
708
714
|
contentSystemId: this.contentSystemId,
|
|
709
715
|
createOnMissing: false,
|
|
710
716
|
doc: docData,
|
|
717
|
+
environmentName: this.environmentName,
|
|
711
718
|
...limit != null && {
|
|
712
719
|
limit
|
|
713
720
|
},
|
|
@@ -862,6 +869,7 @@ export const contentAPIAdapter = (opts)=>({
|
|
|
862
869
|
deleteMany: deleteMany,
|
|
863
870
|
deleteOne: deleteOne,
|
|
864
871
|
deleteVersions: deleteVersions,
|
|
872
|
+
environmentName: opts.environmentName,
|
|
865
873
|
find: find,
|
|
866
874
|
findDistinct: findDistinct,
|
|
867
875
|
findGlobal: findGlobal,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DownloadSkillResult = {
|
|
2
|
+
detail?: string;
|
|
3
|
+
ok: false;
|
|
4
|
+
reason: 'already-installed' | 'empty-extract' | 'error' | 'fetch-failed';
|
|
5
|
+
} | {
|
|
6
|
+
ok: true;
|
|
7
|
+
};
|
|
8
|
+
export declare function downloadSkill(args: {
|
|
9
|
+
branch?: string;
|
|
10
|
+
debug?: boolean;
|
|
11
|
+
projectDir: string;
|
|
12
|
+
}): Promise<DownloadSkillResult>;
|
|
13
|
+
//# sourceMappingURL=download-skill.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { x } from 'tar';
|
|
6
|
+
export async function downloadSkill(args) {
|
|
7
|
+
const { branch = 'main', debug, projectDir } = args;
|
|
8
|
+
const destDir = path.join(projectDir, '.claude', 'skills', 'payload');
|
|
9
|
+
if (await pathExists(path.join(destDir, 'SKILL.md'))) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
reason: 'already-installed'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branch}`;
|
|
16
|
+
const filter = `payload-${branch.replace(/^v/, '').replaceAll('/', '-')}/tools/claude-plugin/skills/payload/`;
|
|
17
|
+
if (debug) {
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.log(`[download-skill] url=${url} dest=${destDir}`);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
return {
|
|
25
|
+
detail: `${res.status} ${res.statusText}`,
|
|
26
|
+
ok: false,
|
|
27
|
+
reason: 'fetch-failed'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!res.body) {
|
|
31
|
+
return {
|
|
32
|
+
detail: 'empty response body',
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: 'fetch-failed'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
await fs.mkdir(destDir, {
|
|
38
|
+
recursive: true
|
|
39
|
+
});
|
|
40
|
+
await pipeline(Readable.from(res.body), x({
|
|
41
|
+
cwd: destDir,
|
|
42
|
+
filter: (p)=>p.includes(filter),
|
|
43
|
+
strip: filter.split('/').length - 1
|
|
44
|
+
}));
|
|
45
|
+
if (!await pathExists(path.join(destDir, 'SKILL.md'))) {
|
|
46
|
+
await fs.rm(destDir, {
|
|
47
|
+
force: true,
|
|
48
|
+
recursive: true
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'empty-extract'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: true
|
|
57
|
+
};
|
|
58
|
+
} catch (err) {
|
|
59
|
+
await fs.rm(destDir, {
|
|
60
|
+
force: true,
|
|
61
|
+
recursive: true
|
|
62
|
+
}).catch(()=>{});
|
|
63
|
+
return {
|
|
64
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: 'error'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function pathExists(p) {
|
|
71
|
+
try {
|
|
72
|
+
await fs.access(p);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=download-skill.js.map
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { APIError,
|
|
1
|
+
import { APIError, parseCookies } from 'payload';
|
|
2
2
|
import { formatAdminURL, getSafeRedirect } from 'payload/shared';
|
|
3
|
-
import { getProjectToken, getUserInfo } from '../../api/figma-api.js';
|
|
4
|
-
import { createCookieOptions } from '../utilities/createCookieOptions.js';
|
|
5
3
|
import { createDebugLogger } from '../utilities/createDebugLogger.js';
|
|
4
|
+
import { establishSession } from '../utilities/establishSession.js';
|
|
5
|
+
import { exchangeCodeForAccessToken } from '../utilities/exchangeCodeForAccessToken.js';
|
|
6
6
|
import { extractOrigin } from '../utilities/extractOrigin.js';
|
|
7
|
-
import {
|
|
8
|
-
const isAbsoluteURL = (url)=>/^[a-z][a-z\d+.-]*:/.test(url);
|
|
7
|
+
import { isAbsoluteURL } from '../utilities/isAbsoluteURL.js';
|
|
9
8
|
export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug, pluginOptions, strategy })=>({
|
|
10
9
|
handler: async (req)=>{
|
|
11
10
|
const { config } = req.payload;
|
|
@@ -127,32 +126,21 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
127
126
|
});
|
|
128
127
|
return Response.redirect(failedRedirect);
|
|
129
128
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
redirect_uri: formatAdminURL({
|
|
136
|
-
apiRoute: config.routes?.api || '/api',
|
|
137
|
-
path: `/${collection.slug}/${endpointSlug}/login`,
|
|
138
|
-
serverURL: redirectServerURL
|
|
139
|
-
})
|
|
140
|
-
};
|
|
141
|
-
if (strategy.collectionOptions.clientSecret) {
|
|
142
|
-
body.client_secret = strategy.collectionOptions.clientSecret;
|
|
143
|
-
}
|
|
129
|
+
const redirectUri = formatAdminURL({
|
|
130
|
+
apiRoute: config.routes?.api || '/api',
|
|
131
|
+
path: `/${collection.slug}/${endpointSlug}/login`,
|
|
132
|
+
serverURL: redirectServerURL
|
|
133
|
+
});
|
|
144
134
|
debugLogger.info({
|
|
145
135
|
msg: 'Exchanging code for access token',
|
|
146
|
-
path
|
|
147
|
-
|
|
136
|
+
path
|
|
137
|
+
});
|
|
138
|
+
const tokenRes = await exchangeCodeForAccessToken({
|
|
139
|
+
code,
|
|
140
|
+
codeVerifier: req.payload.decrypt(state.verifier),
|
|
141
|
+
redirectUri,
|
|
142
|
+
strategy
|
|
148
143
|
});
|
|
149
|
-
const tokenRes = await fetch(strategy.meta?.token_endpoint, {
|
|
150
|
-
body: new URLSearchParams(body),
|
|
151
|
-
headers: {
|
|
152
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
153
|
-
},
|
|
154
|
-
method: 'POST'
|
|
155
|
-
}).then((r)=>r.json());
|
|
156
144
|
debugLogger.info({
|
|
157
145
|
msg: 'Token Response',
|
|
158
146
|
path,
|
|
@@ -167,25 +155,6 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
167
155
|
});
|
|
168
156
|
return Response.redirect(failedRedirect);
|
|
169
157
|
}
|
|
170
|
-
// Fetch Figma user info for handle and profile image
|
|
171
|
-
let figmaUserInfo = null;
|
|
172
|
-
try {
|
|
173
|
-
const oauthCredential = {
|
|
174
|
-
type: 'oauth',
|
|
175
|
-
token: access_token
|
|
176
|
-
};
|
|
177
|
-
figmaUserInfo = await getUserInfo(oauthCredential);
|
|
178
|
-
debugLogger.info({
|
|
179
|
-
figmaUserInfo,
|
|
180
|
-
msg: 'Fetched Figma user info'
|
|
181
|
-
});
|
|
182
|
-
} catch (err) {
|
|
183
|
-
// Non-fatal: user will just not have handle/image
|
|
184
|
-
debugLogger.error({
|
|
185
|
-
err,
|
|
186
|
-
msg: 'Failed to fetch Figma user info'
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
158
|
if (!access_token) {
|
|
190
159
|
const errMsg = 'No access_token was returned from the identity provider.';
|
|
191
160
|
req.payload.logger.error({
|
|
@@ -195,30 +164,6 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
195
164
|
});
|
|
196
165
|
throw new APIError(errMsg);
|
|
197
166
|
}
|
|
198
|
-
// Swap for project token
|
|
199
|
-
const oauthCredential = {
|
|
200
|
-
type: 'oauth',
|
|
201
|
-
token: access_token
|
|
202
|
-
};
|
|
203
|
-
const project_token = await getProjectToken(oauthCredential, config.custom.figma.contentSystemId);
|
|
204
|
-
const refresh_token = access_token;
|
|
205
|
-
// Store project token as access_token
|
|
206
|
-
const token = project_token.token;
|
|
207
|
-
const expiresIn = project_token.expiresAt;
|
|
208
|
-
if (!token) {
|
|
209
|
-
const errMsg = `No ${collectionOptions.token || 'access_token'} was returned from the identity provider.`;
|
|
210
|
-
req.payload.logger.error({
|
|
211
|
-
msg: errMsg,
|
|
212
|
-
path
|
|
213
|
-
});
|
|
214
|
-
throw new APIError(errMsg);
|
|
215
|
-
}
|
|
216
|
-
const cookieOptions = createCookieOptions({
|
|
217
|
-
collection,
|
|
218
|
-
headers: req.headers,
|
|
219
|
-
pluginOptions
|
|
220
|
-
});
|
|
221
|
-
// get the payloadRedirect cookie value
|
|
222
167
|
const cookies = parseCookies(req.headers);
|
|
223
168
|
const payloadRedirect = 'payloadRedirect' in cookies ? cookies.payloadRedirect : '';
|
|
224
169
|
if (!req.responseHeaders) {
|
|
@@ -232,43 +177,17 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
232
177
|
}
|
|
233
178
|
req.responseHeaders.append('Set-Cookie', `payloadRedirect=${redirectToUse}; Max-Age=0; Path=/;`);
|
|
234
179
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
req.responseHeaders.append('Set-Cookie', refreshCookie);
|
|
246
|
-
}
|
|
247
|
-
const tokenExpires = cookieOptions?.expires ? getCookieExpiration(cookieOptions.expires) : new Date(expiresIn) // Project token uses epoch time
|
|
248
|
-
;
|
|
249
|
-
const tokenCookie = generateCookie({
|
|
250
|
-
...cookieOptions,
|
|
251
|
-
name: strategy.cookieName,
|
|
252
|
-
expires: tokenExpires,
|
|
253
|
-
returnCookieAsObject: false,
|
|
254
|
-
value: token
|
|
180
|
+
await establishSession({
|
|
181
|
+
accessToken: access_token,
|
|
182
|
+
collection,
|
|
183
|
+
collectionOptions,
|
|
184
|
+
contentSystemId: config.custom.figma.contentSystemId,
|
|
185
|
+
debugLogger,
|
|
186
|
+
pluginOptions,
|
|
187
|
+
refreshTokenExpiresIn: refreshTokenExpires,
|
|
188
|
+
req,
|
|
189
|
+
strategy
|
|
255
190
|
});
|
|
256
|
-
req.responseHeaders.append('Set-Cookie', tokenCookie);
|
|
257
|
-
// Store user info in short-lived cookie for defaultVerify to consume
|
|
258
|
-
if (figmaUserInfo) {
|
|
259
|
-
const userInfoValue = Buffer.from(JSON.stringify({
|
|
260
|
-
handle: figmaUserInfo.handle,
|
|
261
|
-
img_url: figmaUserInfo.img_url
|
|
262
|
-
})).toString('base64');
|
|
263
|
-
const userInfoCookie = generateCookie({
|
|
264
|
-
...cookieOptions,
|
|
265
|
-
name: pluginOptions?.userInfoCookieName ?? 'figma-user-info',
|
|
266
|
-
expires: new Date(Date.now() + 60_000),
|
|
267
|
-
returnCookieAsObject: false,
|
|
268
|
-
value: userInfoValue
|
|
269
|
-
});
|
|
270
|
-
req.responseHeaders.append('Set-Cookie', userInfoCookie);
|
|
271
|
-
}
|
|
272
191
|
return Response.redirect(redirectToUse);
|
|
273
192
|
} catch (err) {
|
|
274
193
|
req.payload.logger.error({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CollectionConfig, Endpoint } from 'payload';
|
|
2
|
+
import type { Strategy } from '../strategy/index.js';
|
|
3
|
+
import type { CollectionOptions, PluginOptions } from '../types.js';
|
|
4
|
+
interface Args {
|
|
5
|
+
collection: CollectionConfig;
|
|
6
|
+
collectionOptions: CollectionOptions;
|
|
7
|
+
endpointSlug: string;
|
|
8
|
+
pluginOptions: PluginOptions;
|
|
9
|
+
strategy: Strategy;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Auto-login endpoint for embedded contexts where the interactive OAuth flow
|
|
13
|
+
* cannot run.
|
|
14
|
+
*/
|
|
15
|
+
export declare const getTokenLoginEndpoint: ({ collection, collectionOptions, endpointSlug, pluginOptions, strategy, }: Args) => Endpoint;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=getTokenLoginEndpoint.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { formatAdminURL, getSafeRedirect } from 'payload/shared';
|
|
2
|
+
import { createDebugLogger } from '../utilities/createDebugLogger.js';
|
|
3
|
+
import { establishSession } from '../utilities/establishSession.js';
|
|
4
|
+
import { exchangeCodeForAccessToken } from '../utilities/exchangeCodeForAccessToken.js';
|
|
5
|
+
import { isAbsoluteURL } from '../utilities/isAbsoluteURL.js';
|
|
6
|
+
/**
|
|
7
|
+
* Auto-login endpoint for embedded contexts where the interactive OAuth flow
|
|
8
|
+
* cannot run.
|
|
9
|
+
*/ export const getTokenLoginEndpoint = ({ collection, collectionOptions, endpointSlug, pluginOptions, strategy })=>({
|
|
10
|
+
handler: async (req)=>{
|
|
11
|
+
const { config } = req.payload;
|
|
12
|
+
const adminRoute = config.routes?.admin || '/admin';
|
|
13
|
+
const debugLogger = createDebugLogger(req.payload, pluginOptions.debug);
|
|
14
|
+
const adminURL = formatAdminURL({
|
|
15
|
+
adminRoute,
|
|
16
|
+
serverURL: config.serverURL
|
|
17
|
+
});
|
|
18
|
+
const failedRedirect = formatAdminURL({
|
|
19
|
+
adminRoute,
|
|
20
|
+
path: '/login',
|
|
21
|
+
serverURL: config.serverURL
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
await strategy.ensureMeta();
|
|
25
|
+
const code = req.query.code;
|
|
26
|
+
const redirectQuery = req.query.redirect;
|
|
27
|
+
let redirectToUse = adminURL;
|
|
28
|
+
if (typeof redirectQuery === 'string' && redirectQuery.length > 0) {
|
|
29
|
+
const safeRedirect = getSafeRedirect({
|
|
30
|
+
allowAbsoluteUrls: false,
|
|
31
|
+
redirectTo: redirectQuery
|
|
32
|
+
});
|
|
33
|
+
if (safeRedirect && !isAbsoluteURL(safeRedirect)) {
|
|
34
|
+
redirectToUse = `${config.serverURL ?? ''}${safeRedirect}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (typeof code !== 'string' || code.length === 0) {
|
|
38
|
+
debugLogger.error({
|
|
39
|
+
msg: 'token_login: missing code'
|
|
40
|
+
});
|
|
41
|
+
return Response.redirect(failedRedirect);
|
|
42
|
+
}
|
|
43
|
+
if (!strategy.meta?.token_endpoint) {
|
|
44
|
+
debugLogger.error({
|
|
45
|
+
msg: 'token_login: no token_endpoint discovered from identity provider'
|
|
46
|
+
});
|
|
47
|
+
return Response.redirect(failedRedirect);
|
|
48
|
+
}
|
|
49
|
+
debugLogger.info({
|
|
50
|
+
msg: 'token_login: exchanging code for access token'
|
|
51
|
+
});
|
|
52
|
+
const tokenRes = await exchangeCodeForAccessToken({
|
|
53
|
+
code,
|
|
54
|
+
redirectUri: formatAdminURL({
|
|
55
|
+
apiRoute: config.routes?.api || '/api',
|
|
56
|
+
path: `/${collection.slug}/${endpointSlug}/token_login`,
|
|
57
|
+
serverURL: config.serverURL
|
|
58
|
+
}),
|
|
59
|
+
strategy
|
|
60
|
+
});
|
|
61
|
+
const { access_token, error, error_description, expires_in } = tokenRes;
|
|
62
|
+
if (error) {
|
|
63
|
+
req.payload.logger.error({
|
|
64
|
+
err: error,
|
|
65
|
+
error_description,
|
|
66
|
+
msg: 'token_login: error exchanging code for access token'
|
|
67
|
+
});
|
|
68
|
+
return Response.redirect(failedRedirect);
|
|
69
|
+
}
|
|
70
|
+
if (typeof access_token !== 'string' || access_token.length === 0) {
|
|
71
|
+
req.payload.logger.error({
|
|
72
|
+
msg: 'token_login: no access_token returned from identity provider'
|
|
73
|
+
});
|
|
74
|
+
return Response.redirect(failedRedirect);
|
|
75
|
+
}
|
|
76
|
+
await establishSession({
|
|
77
|
+
accessToken: access_token,
|
|
78
|
+
collection,
|
|
79
|
+
collectionOptions,
|
|
80
|
+
contentSystemId: config.custom.figma.contentSystemId,
|
|
81
|
+
debugLogger,
|
|
82
|
+
pluginOptions,
|
|
83
|
+
refreshTokenExpiresIn: expires_in,
|
|
84
|
+
req,
|
|
85
|
+
strategy
|
|
86
|
+
});
|
|
87
|
+
return Response.redirect(redirectToUse);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
req.payload.logger.error({
|
|
90
|
+
err,
|
|
91
|
+
msg: 'Error during token_login',
|
|
92
|
+
path: req.pathname
|
|
93
|
+
});
|
|
94
|
+
return Response.json({
|
|
95
|
+
error: 'Error during token_login'
|
|
96
|
+
}, {
|
|
97
|
+
status: 500
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
method: 'get',
|
|
102
|
+
path: `/${endpointSlug}/token_login`
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=getTokenLoginEndpoint.js.map
|
package/dist/oauth/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getLoginEndpoint } from './endpoints/getLoginEndpoint.js';
|
|
|
4
4
|
import { getLogoutEndpoint } from './endpoints/getLogoutEndpoint.js';
|
|
5
5
|
import { getMetaEndpoint } from './endpoints/getMetaEndpoint.js';
|
|
6
6
|
import { getRedirectToLoginEndpoint } from './endpoints/getRedirectToLoginEndpoint.js';
|
|
7
|
+
import { getTokenLoginEndpoint } from './endpoints/getTokenLoginEndpoint.js';
|
|
7
8
|
import { getAfterLogout } from './hooks/afterLogout.js';
|
|
8
9
|
import { getMeHook } from './hooks/me.js';
|
|
9
10
|
import { getRefreshHook } from './hooks/refresh.js';
|
|
@@ -206,6 +207,13 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
|
|
|
206
207
|
endpointSlug,
|
|
207
208
|
pluginOptions,
|
|
208
209
|
strategy
|
|
210
|
+
}),
|
|
211
|
+
getTokenLoginEndpoint({
|
|
212
|
+
collection: existingCollection,
|
|
213
|
+
collectionOptions,
|
|
214
|
+
endpointSlug,
|
|
215
|
+
pluginOptions,
|
|
216
|
+
strategy
|
|
209
217
|
})
|
|
210
218
|
],
|
|
211
219
|
fields,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CollectionConfig, PayloadRequest } from 'payload';
|
|
2
|
+
import type { Strategy } from '../strategy/index.js';
|
|
3
|
+
import type { CollectionOptions, DebugLogger, PluginOptions } from '../types.js';
|
|
4
|
+
interface Args {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
collection: CollectionConfig;
|
|
7
|
+
collectionOptions: CollectionOptions;
|
|
8
|
+
contentSystemId: string;
|
|
9
|
+
debugLogger: DebugLogger;
|
|
10
|
+
pluginOptions: PluginOptions;
|
|
11
|
+
/** From the IdP token response (`expires_in`); used as a refresh-cookie expiry fallback. */
|
|
12
|
+
refreshTokenExpiresIn?: number;
|
|
13
|
+
req: PayloadRequest;
|
|
14
|
+
strategy: Strategy;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Given a valid OAuth access_token: fetch user info (non-fatal), mint a
|
|
18
|
+
* project token, and write the main + refresh + user-info cookies to
|
|
19
|
+
* `req.responseHeaders`. Throws `APIError` if the project token mint fails.
|
|
20
|
+
*/
|
|
21
|
+
export declare const establishSession: ({ accessToken, collection, collectionOptions, contentSystemId, debugLogger, pluginOptions, refreshTokenExpiresIn, req, strategy, }: Args) => Promise<void>;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=establishSession.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { APIError, generateCookie } from 'payload';
|
|
2
|
+
import { getProjectToken, getUserInfo } from '../../api/figma-api.js';
|
|
3
|
+
import { createCookieOptions } from './createCookieOptions.js';
|
|
4
|
+
import { getCookieExpiration } from './getCookieExpiration.js';
|
|
5
|
+
/**
|
|
6
|
+
* Given a valid OAuth access_token: fetch user info (non-fatal), mint a
|
|
7
|
+
* project token, and write the main + refresh + user-info cookies to
|
|
8
|
+
* `req.responseHeaders`. Throws `APIError` if the project token mint fails.
|
|
9
|
+
*/ export const establishSession = async ({ accessToken, collection, collectionOptions, contentSystemId, debugLogger, pluginOptions, refreshTokenExpiresIn, req, strategy })=>{
|
|
10
|
+
const oauthCredential = {
|
|
11
|
+
type: 'oauth',
|
|
12
|
+
token: accessToken
|
|
13
|
+
};
|
|
14
|
+
// Non-fatal: user just won't have handle/image on first request.
|
|
15
|
+
let figmaUserInfo = null;
|
|
16
|
+
try {
|
|
17
|
+
figmaUserInfo = await getUserInfo(oauthCredential);
|
|
18
|
+
debugLogger.info({
|
|
19
|
+
figmaUserInfo,
|
|
20
|
+
msg: 'Fetched Figma user info'
|
|
21
|
+
});
|
|
22
|
+
} catch (err) {
|
|
23
|
+
debugLogger.error({
|
|
24
|
+
err,
|
|
25
|
+
msg: 'Failed to fetch Figma user info'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const projectToken = await getProjectToken(oauthCredential, contentSystemId);
|
|
29
|
+
if (!projectToken.token) {
|
|
30
|
+
const errMsg = 'No project token returned from Figma API';
|
|
31
|
+
req.payload.logger.error({
|
|
32
|
+
msg: errMsg
|
|
33
|
+
});
|
|
34
|
+
throw new APIError(errMsg);
|
|
35
|
+
}
|
|
36
|
+
const cookieOptions = createCookieOptions({
|
|
37
|
+
collection,
|
|
38
|
+
headers: req.headers,
|
|
39
|
+
pluginOptions
|
|
40
|
+
});
|
|
41
|
+
if (!req.responseHeaders) {
|
|
42
|
+
req.responseHeaders = new Headers();
|
|
43
|
+
}
|
|
44
|
+
// The -refresh cookie holds the OAuth access_token so refreshTokens can
|
|
45
|
+
// re-mint project_tokens from it on JWT expiry.
|
|
46
|
+
if (!collectionOptions.usePayloadJWT) {
|
|
47
|
+
const refreshCookie = generateCookie({
|
|
48
|
+
...cookieOptions,
|
|
49
|
+
name: `${strategy.cookieName}-refresh`,
|
|
50
|
+
expires: getCookieExpiration(cookieOptions?.expires ?? refreshTokenExpiresIn ?? 31_556_952),
|
|
51
|
+
returnCookieAsObject: false,
|
|
52
|
+
value: accessToken
|
|
53
|
+
});
|
|
54
|
+
req.responseHeaders.append('Set-Cookie', refreshCookie);
|
|
55
|
+
}
|
|
56
|
+
const tokenExpires = cookieOptions?.expires ? getCookieExpiration(cookieOptions.expires) : new Date(projectToken.expiresAt);
|
|
57
|
+
const tokenCookie = generateCookie({
|
|
58
|
+
...cookieOptions,
|
|
59
|
+
name: strategy.cookieName,
|
|
60
|
+
expires: tokenExpires,
|
|
61
|
+
returnCookieAsObject: false,
|
|
62
|
+
value: projectToken.token
|
|
63
|
+
});
|
|
64
|
+
req.responseHeaders.append('Set-Cookie', tokenCookie);
|
|
65
|
+
// Short-lived handoff cookie consumed by defaultVerify on the next request.
|
|
66
|
+
if (figmaUserInfo) {
|
|
67
|
+
const userInfoValue = Buffer.from(JSON.stringify({
|
|
68
|
+
handle: figmaUserInfo.handle,
|
|
69
|
+
img_url: figmaUserInfo.img_url
|
|
70
|
+
})).toString('base64');
|
|
71
|
+
const userInfoCookie = generateCookie({
|
|
72
|
+
...cookieOptions,
|
|
73
|
+
name: pluginOptions?.userInfoCookieName ?? 'figma-user-info',
|
|
74
|
+
expires: new Date(Date.now() + 60_000),
|
|
75
|
+
returnCookieAsObject: false,
|
|
76
|
+
value: userInfoValue
|
|
77
|
+
});
|
|
78
|
+
req.responseHeaders.append('Set-Cookie', userInfoCookie);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=establishSession.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Strategy } from '../strategy/index.js';
|
|
2
|
+
interface Args {
|
|
3
|
+
/** Authorization code received from the IdP redirect. */
|
|
4
|
+
code: string;
|
|
5
|
+
/** PKCE verifier; omit for non-PKCE flows. */
|
|
6
|
+
codeVerifier?: string;
|
|
7
|
+
/** Must match the redirect_uri the authorization request used. */
|
|
8
|
+
redirectUri: string;
|
|
9
|
+
strategy: Strategy;
|
|
10
|
+
}
|
|
11
|
+
export interface TokenEndpointResponse {
|
|
12
|
+
access_token?: null | string;
|
|
13
|
+
error?: string;
|
|
14
|
+
error_description?: string;
|
|
15
|
+
expires_in?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* POSTs an authorization code to the IdP's `token_endpoint` and returns the
|
|
19
|
+
* raw response body. Callers handle error/missing-token logging in their own
|
|
20
|
+
* style — this helper only owns the request shape.
|
|
21
|
+
*/
|
|
22
|
+
export declare const exchangeCodeForAccessToken: ({ code, codeVerifier, redirectUri, strategy, }: Args) => Promise<TokenEndpointResponse>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=exchangeCodeForAccessToken.d.ts.map
|