@payloadcms/figma 0.0.1-alpha.62 → 0.0.1-alpha.63
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/crypto-utils.d.ts +11 -0
- package/dist/auth/crypto-utils.js +51 -0
- package/dist/auth/oauth-flow.js +3 -3
- package/dist/auth/project-token.d.ts +17 -6
- package/dist/auth/project-token.js +37 -19
- package/dist/auth/token-store-migration.d.ts +58 -0
- package/dist/auth/token-store-migration.js +156 -0
- package/dist/auth/token-store.d.ts +51 -128
- package/dist/auth/token-store.js +326 -187
- package/dist/auth/types.d.ts +8 -1
- package/dist/cli.js +6 -0
- package/dist/commands/debug.js +16 -12
- package/dist/commands/dump-tokens.d.ts +12 -0
- package/dist/commands/dump-tokens.js +69 -0
- package/dist/commands/init.js +7 -3
- package/dist/commands/list-tokens.js +2 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +23 -4
- package/dist/db-content-api/utilities/auth.js +4 -1
- package/dist/oauth/defaults.d.ts +3 -1
- package/dist/oauth/defaults.js +6 -5
- package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/types.d.ts +10 -0
- package/dist/plugin/auth-preflight.d.ts +8 -0
- package/dist/plugin/auth-preflight.js +20 -0
- package/dist/plugin/bootstrap-preflight.d.ts +23 -0
- package/dist/plugin/bootstrap-preflight.js +45 -0
- package/dist/plugin/build-config.js +73 -12
- package/dist/plugin/dev-cookie-names.d.ts +14 -0
- package/dist/plugin/dev-cookie-names.js +19 -0
- package/dist/storage-content-api/client.js +4 -1
- package/dist/utils/token-display.d.ts +5 -1
- package/dist/utils/token-display.js +47 -26
- package/package.json +2 -1
package/dist/commands/debug.js
CHANGED
|
@@ -36,14 +36,14 @@ function appendSystemSection(lines, env) {
|
|
|
36
36
|
lines.push('');
|
|
37
37
|
}
|
|
38
38
|
function appendAuthSection(lines, tokenStore, cwd) {
|
|
39
|
-
const tokens = tokenStore.
|
|
39
|
+
const tokens = tokenStore.getOauthInfo();
|
|
40
40
|
lines.push('Auth:');
|
|
41
41
|
if (!tokens) {
|
|
42
42
|
lines.push(' Status: Not authenticated');
|
|
43
43
|
lines.push('');
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
-
const isValid = tokenStore.
|
|
46
|
+
const isValid = tokenStore.hasValidOauthToken();
|
|
47
47
|
lines.push(` Status: ${isValid ? 'Authenticated' : 'Token expired'}`);
|
|
48
48
|
const email = getEmailFromClaims(tokenStore, cwd);
|
|
49
49
|
if (email) {
|
|
@@ -62,7 +62,7 @@ function appendTokenStoreDiagnostics(lines, tokenStore) {
|
|
|
62
62
|
lines.push(` File: ${filePath}`);
|
|
63
63
|
lines.push(` File Exists: ${fileExists}`);
|
|
64
64
|
lines.push(` File Size: ${fileSize}B`);
|
|
65
|
-
const canRead = tokenStore.
|
|
65
|
+
const canRead = tokenStore.getOauthInfo() !== null || tokenStore.getCurrentRefreshToken() !== null;
|
|
66
66
|
lines.push(` Decryption: ${fileExists ? canRead || fileSize === 0 ? 'OK' : 'FAILED' : 'N/A'}`);
|
|
67
67
|
lines.push(` Key Hash: ${getEncryptionKeyHash()}`);
|
|
68
68
|
lines.push('');
|
|
@@ -85,7 +85,12 @@ function appendProjectSection(lines, tokenStore, cwd) {
|
|
|
85
85
|
lines.push(` Content System ID: ${bootstrapData.contentSystemId}`);
|
|
86
86
|
lines.push(` Tenant Instance ID: ${bootstrapData.tenantInstanceId}`);
|
|
87
87
|
lines.push(` OAuth Client ID: ${maskToken(bootstrapData.oauthClientId)}`);
|
|
88
|
-
const projectToken = tokenStore.getProjectToken(
|
|
88
|
+
const projectToken = tokenStore.getProjectToken({
|
|
89
|
+
projectInfo: {
|
|
90
|
+
environmentName,
|
|
91
|
+
projectId
|
|
92
|
+
}
|
|
93
|
+
});
|
|
89
94
|
if (projectToken) {
|
|
90
95
|
lines.push(` Project Token: ${formatExpiryTime(projectToken.expiresAt)}`);
|
|
91
96
|
}
|
|
@@ -103,25 +108,24 @@ async function appendPackagesSection(lines) {
|
|
|
103
108
|
lines.push(` payload: ${payloadVersion}`);
|
|
104
109
|
}
|
|
105
110
|
/**
|
|
106
|
-
*
|
|
111
|
+
* Resolve email from the current project + environment scoped token when available.
|
|
107
112
|
*/ function getEmailFromClaims(tokenStore, cwd) {
|
|
108
113
|
const projectId = getEnvVarSync(cwd, 'FIGMA_PROJECT_ID');
|
|
109
114
|
const environmentName = getEnvVarSync(cwd, 'FIGMA_ENVIRONMENT_NAME');
|
|
110
115
|
if (projectId && environmentName) {
|
|
111
116
|
const bootstrapData = tokenStore.getBootstrapData(projectId, environmentName);
|
|
112
117
|
if (bootstrapData) {
|
|
113
|
-
const email = tokenStore.
|
|
118
|
+
const email = tokenStore.getProjectToken({
|
|
119
|
+
projectInfo: {
|
|
120
|
+
environmentName,
|
|
121
|
+
projectId
|
|
122
|
+
}
|
|
123
|
+
})?.claims?.email;
|
|
114
124
|
if (email) {
|
|
115
125
|
return email;
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
|
-
for (const id of tokenStore.getAllProjectTokenTenantIds()){
|
|
120
|
-
const email = tokenStore.getProjectTokenClaims(id)?.email;
|
|
121
|
-
if (email) {
|
|
122
|
-
return email;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
129
|
return null;
|
|
126
130
|
}
|
|
127
131
|
function getPayloadVersion() {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle the `@payloadcms/figma dump-tokens` command
|
|
3
|
+
*
|
|
4
|
+
* Exports decrypted versions of encrypted token store files in
|
|
5
|
+
* `oauth/` and `projects/` subdirectories (new structure) plus any legacy
|
|
6
|
+
* per-env directories from older package versions. Creates
|
|
7
|
+
* `.decrypted.json` files next to each encrypted `.json` file.
|
|
8
|
+
*
|
|
9
|
+
* Skips the `payloadcms-figma-crypto.json` salt file at the root.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dumpTokensCommand(): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=dump-tokens.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import envPaths from 'env-paths';
|
|
3
|
+
import fsSync from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { deriveEncryptionKey, exportDecryptedStoreFiles } from '../auth/crypto-utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Handle the `@payloadcms/figma dump-tokens` command
|
|
9
|
+
*
|
|
10
|
+
* Exports decrypted versions of encrypted token store files in
|
|
11
|
+
* `oauth/` and `projects/` subdirectories (new structure) plus any legacy
|
|
12
|
+
* per-env directories from older package versions. Creates
|
|
13
|
+
* `.decrypted.json` files next to each encrypted `.json` file.
|
|
14
|
+
*
|
|
15
|
+
* Skips the `payloadcms-figma-crypto.json` salt file at the root.
|
|
16
|
+
*/ export async function dumpTokensCommand() {
|
|
17
|
+
const encryptionKey = deriveEncryptionKey();
|
|
18
|
+
const newRoot = envPaths('payloadcms-figma').config;
|
|
19
|
+
const stagingLegacyRoot = envPaths('payloadcms-figma-staging').config;
|
|
20
|
+
const candidateDirs = [
|
|
21
|
+
path.join(newRoot, 'oauth'),
|
|
22
|
+
path.join(newRoot, 'projects'),
|
|
23
|
+
stagingLegacyRoot
|
|
24
|
+
];
|
|
25
|
+
const targetDirs = Array.from(new Set(candidateDirs)).filter((dir)=>fsSync.existsSync(dir));
|
|
26
|
+
if (targetDirs.length === 0) {
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.log('No token store directories found.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log(pc.yellow('⚠ dump-tokens writes DECRYPTED token files to disk.'));
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log('Target directories:');
|
|
35
|
+
for (const dir of targetDirs){
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.log(` ${dir}`);
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.log('');
|
|
41
|
+
const confirmed = await p.confirm({
|
|
42
|
+
initialValue: false,
|
|
43
|
+
message: 'Continue? Remember to delete the decrypted files after use.'
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.log('Cancelled.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const decryptedFiles = [];
|
|
51
|
+
for (const dir of targetDirs){
|
|
52
|
+
decryptedFiles.push(...exportDecryptedStoreFiles(dir, encryptionKey));
|
|
53
|
+
}
|
|
54
|
+
if (decryptedFiles.length === 0) {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.log('No encrypted files could be decrypted with the current machine key.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.log(`\nCreated ${decryptedFiles.length} decrypted file(s):`);
|
|
61
|
+
for (const file of decryptedFiles){
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.log(` ${file}`);
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log('\nNote: These files contain sensitive tokens. Delete after use.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=dump-tokens.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -29,12 +29,16 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
29
29
|
* @param tokenStore - Token store instance
|
|
30
30
|
* @param contentSystemId - Content System ID to generate token for
|
|
31
31
|
* @param spinner - Clack spinner instance for status updates (only used in debug mode)
|
|
32
|
-
*/ async function generateProjectTokenWithFeedback(tokenStore, contentSystemId, spinner) {
|
|
32
|
+
*/ async function generateProjectTokenWithFeedback(tokenStore, contentSystemId, spinner, projectInfo) {
|
|
33
33
|
if (isDebug()) {
|
|
34
34
|
spinner.start('Generating project token...');
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
const projectToken = await getValidProjectToken(
|
|
37
|
+
const projectToken = await getValidProjectToken({
|
|
38
|
+
projectInfo,
|
|
39
|
+
tenantId: contentSystemId,
|
|
40
|
+
tokenStore
|
|
41
|
+
});
|
|
38
42
|
if (isDebug()) {
|
|
39
43
|
if (projectToken) {
|
|
40
44
|
spinner.stop(pc.green('✓ Project token generated'));
|
|
@@ -78,7 +82,7 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
78
82
|
p.log.warn(pc.yellow('Skipping authentication (--skip-auth mode)'));
|
|
79
83
|
} else {
|
|
80
84
|
// Check for valid cached tokens first (no API call needed)
|
|
81
|
-
if (!tokenStore.
|
|
85
|
+
if (!tokenStore.hasValidOauthToken()) {
|
|
82
86
|
// Need to refresh or authenticate - show spinner for API call
|
|
83
87
|
s.start('Checking authentication...');
|
|
84
88
|
const credential = await tryGetCredential(tokenStore);
|
|
@@ -26,13 +26,13 @@ const ALL_ENVIRONMENTS = [
|
|
|
26
26
|
}
|
|
27
27
|
function showTokenDetails(env) {
|
|
28
28
|
const tokenStore = getTokenStore(env);
|
|
29
|
-
const tokens = tokenStore.
|
|
29
|
+
const tokens = tokenStore.getOauthInfo();
|
|
30
30
|
if (!tokens) {
|
|
31
31
|
p.log.warn(pc.yellow(`No tokens found${formatEnvSuffix(env, ' for ')}. Not authenticated.`));
|
|
32
32
|
p.note(`Run ${pc.cyan('@payloadcms/figma login')} to authenticate`, 'Tip');
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
const isValid = tokenStore.
|
|
35
|
+
const isValid = tokenStore.hasValidOauthToken();
|
|
36
36
|
const status = isValid ? pc.green('✓ Valid') : pc.red('✗ Expired');
|
|
37
37
|
p.log.step(pc.bold(`Token Status${formatEnvSuffix(env, ' (', ')')}`));
|
|
38
38
|
// Build OAuth token info display
|
package/dist/commands/login.js
CHANGED
|
@@ -22,7 +22,7 @@ import * as log from '../utils/log.js';
|
|
|
22
22
|
try {
|
|
23
23
|
const existingCredential = await getValidCredential(tokenStore);
|
|
24
24
|
if (existingCredential) {
|
|
25
|
-
const tokens = tokenStore.
|
|
25
|
+
const tokens = tokenStore.getOauthInfo();
|
|
26
26
|
p.log.warn(pc.yellow(`Already logged in${formatEnvSuffix(env)}`));
|
|
27
27
|
if (tokens?.userId) {
|
|
28
28
|
log.info(`User ID: ${tokens.userId}`);
|
package/dist/commands/logout.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { getTokenStore } from '../auth/token-store.js';
|
|
4
4
|
import { getInfraEnvironment } from '../constants.js';
|
|
5
|
+
import { getEnvVarSync } from '../utils/env-management.js';
|
|
5
6
|
import { formatEnvSuffix } from '../utils/format-env-suffix.js';
|
|
6
7
|
import * as log from '../utils/log.js';
|
|
7
8
|
const ALL_ENVIRONMENTS = [
|
|
@@ -23,7 +24,7 @@ const ALL_ENVIRONMENTS = [
|
|
|
23
24
|
}
|
|
24
25
|
async function logoutSingleEnvironment(env) {
|
|
25
26
|
const tokenStore = getTokenStore(env);
|
|
26
|
-
const hasTokens = tokenStore.
|
|
27
|
+
const hasTokens = tokenStore.getOauthInfo() !== null;
|
|
27
28
|
if (!hasTokens) {
|
|
28
29
|
p.log.warn(pc.yellow(`No tokens found${formatEnvSuffix(env, ' for ')}. Already logged out.`));
|
|
29
30
|
return;
|
|
@@ -40,7 +41,7 @@ async function logoutSingleEnvironment(env) {
|
|
|
40
41
|
s.start('Clearing tokens...');
|
|
41
42
|
try {
|
|
42
43
|
tokenStore.clearTokens();
|
|
43
|
-
tokenStore
|
|
44
|
+
clearCurrentProjectToken(tokenStore, env);
|
|
44
45
|
s.stop(pc.green(`✓ Logged out${formatEnvSuffix(env, ' from ')}`));
|
|
45
46
|
} catch (error) {
|
|
46
47
|
s.stop(pc.red('✗ Failed to clear tokens'));
|
|
@@ -52,7 +53,7 @@ async function logoutAllEnvironments() {
|
|
|
52
53
|
// Check which environments have tokens
|
|
53
54
|
const envsWithTokens = ALL_ENVIRONMENTS.filter((env)=>{
|
|
54
55
|
const store = getTokenStore(env);
|
|
55
|
-
return store.
|
|
56
|
+
return store.getOauthInfo() !== null;
|
|
56
57
|
});
|
|
57
58
|
if (envsWithTokens.length === 0) {
|
|
58
59
|
p.log.warn(pc.yellow('No tokens found in any environment. Already logged out.'));
|
|
@@ -73,7 +74,7 @@ async function logoutAllEnvironments() {
|
|
|
73
74
|
for (const env of envsWithTokens){
|
|
74
75
|
const store = getTokenStore(env);
|
|
75
76
|
store.clearTokens();
|
|
76
|
-
store
|
|
77
|
+
clearCurrentProjectToken(store, env);
|
|
77
78
|
}
|
|
78
79
|
s.stop(pc.green('✓ Logged out from all environments'));
|
|
79
80
|
} catch (error) {
|
|
@@ -82,5 +83,23 @@ async function logoutAllEnvironments() {
|
|
|
82
83
|
process.exit(1);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
function clearCurrentProjectToken(store, env) {
|
|
87
|
+
const projectId = getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID');
|
|
88
|
+
if (!projectId) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const bootstrapData = store.getBootstrapData(projectId, env);
|
|
92
|
+
if (!bootstrapData?.contentSystemId) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const projectInfo = {
|
|
96
|
+
environmentName: env,
|
|
97
|
+
projectId
|
|
98
|
+
};
|
|
99
|
+
// Clear scoped token for this tenant.
|
|
100
|
+
store.clearProjectToken({
|
|
101
|
+
projectInfo
|
|
102
|
+
});
|
|
103
|
+
}
|
|
85
104
|
|
|
86
105
|
//# sourceMappingURL=logout.js.map
|
|
@@ -25,7 +25,10 @@ export function createAuthMiddleware(opts) {
|
|
|
25
25
|
if (opts.auth.mode === 'apiKey') {
|
|
26
26
|
request.headers.set('X-Api-Key', opts.auth.apiKey);
|
|
27
27
|
} else if (opts.auth.mode === 'tokenStore') {
|
|
28
|
-
const token = await getValidProjectToken(
|
|
28
|
+
const token = await getValidProjectToken({
|
|
29
|
+
tenantId: opts.contentSystemId,
|
|
30
|
+
tokenStore: opts.auth.tokenStore
|
|
31
|
+
});
|
|
29
32
|
if (!token) {
|
|
30
33
|
throw new Error('Authentication required. Run `npx @payloadcms/figma login` to authenticate.');
|
|
31
34
|
}
|
package/dist/oauth/defaults.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CollectionConfig, EmailField, TextField } from 'payload';
|
|
2
2
|
import type { VerifyFunction } from './types.js';
|
|
3
|
+
export declare const DEFAULT_USER_INFO_COOKIE_NAME = "figma-user-info";
|
|
3
4
|
export declare const defaultScope: string[];
|
|
4
5
|
export declare const defaultUsernameField: TextField;
|
|
5
|
-
export declare const defaultVerify: ({ collection, strategyName, usernameField, }: {
|
|
6
|
+
export declare const defaultVerify: ({ collection, strategyName, userInfoCookieName, usernameField, }: {
|
|
6
7
|
collection: CollectionConfig;
|
|
7
8
|
strategyName: string;
|
|
9
|
+
userInfoCookieName?: string;
|
|
8
10
|
usernameField: EmailField | TextField;
|
|
9
11
|
}) => VerifyFunction;
|
|
10
12
|
//# sourceMappingURL=defaults.d.ts.map
|
package/dist/oauth/defaults.js
CHANGED
|
@@ -2,10 +2,11 @@ import { parseCookies } from 'payload';
|
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
3
|
import { hasUserTokenPropsChanged } from './utilities/hasUserTokenPropsChanged.js';
|
|
4
4
|
import { isDuplicateKeyError } from './utilities/isDuplicateKeyError.js';
|
|
5
|
-
|
|
5
|
+
export const DEFAULT_USER_INFO_COOKIE_NAME = 'figma-user-info';
|
|
6
|
+
function getFigmaUserInfo(headers, cookieName) {
|
|
6
7
|
try {
|
|
7
8
|
const cookies = parseCookies(headers);
|
|
8
|
-
const encoded = cookies.get(
|
|
9
|
+
const encoded = cookies.get(cookieName);
|
|
9
10
|
if (!encoded) {
|
|
10
11
|
return null;
|
|
11
12
|
}
|
|
@@ -28,7 +29,7 @@ export const defaultUsernameField = {
|
|
|
28
29
|
},
|
|
29
30
|
unique: true
|
|
30
31
|
};
|
|
31
|
-
export const defaultVerify = ({ collection, strategyName, usernameField })=>async ({ headers, payload, token })=>{
|
|
32
|
+
export const defaultVerify = ({ collection, strategyName, userInfoCookieName = DEFAULT_USER_INFO_COOKIE_NAME, usernameField })=>async ({ headers, payload, token })=>{
|
|
32
33
|
let tokenUsername = 'preferred_username';
|
|
33
34
|
if (usernameField.name !== 'preferredUsername') {
|
|
34
35
|
tokenUsername = usernameField.name;
|
|
@@ -38,12 +39,12 @@ export const defaultVerify = ({ collection, strategyName, usernameField })=>asyn
|
|
|
38
39
|
user: null
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
|
-
const figmaUserInfo = getFigmaUserInfo(headers);
|
|
42
|
+
const figmaUserInfo = getFigmaUserInfo(headers, userInfoCookieName);
|
|
42
43
|
let responseHeaders;
|
|
43
44
|
// Clear the cookie after reading
|
|
44
45
|
if (figmaUserInfo) {
|
|
45
46
|
responseHeaders = new Headers();
|
|
46
|
-
responseHeaders.append('Set-Cookie',
|
|
47
|
+
responseHeaders.append('Set-Cookie', `${userInfoCookieName}=; Max-Age=0; Path=/;`);
|
|
47
48
|
}
|
|
48
49
|
let user = null;
|
|
49
50
|
const depth = typeof collection.auth === 'object' ? collection.auth.depth : undefined;
|
|
@@ -262,7 +262,7 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
262
262
|
})).toString('base64');
|
|
263
263
|
const userInfoCookie = generateCookie({
|
|
264
264
|
...cookieOptions,
|
|
265
|
-
name: 'figma-user-info',
|
|
265
|
+
name: pluginOptions?.userInfoCookieName ?? 'figma-user-info',
|
|
266
266
|
expires: new Date(Date.now() + 60_000),
|
|
267
267
|
returnCookieAsObject: false,
|
|
268
268
|
value: userInfoValue
|
package/dist/oauth/index.js
CHANGED
|
@@ -144,6 +144,7 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
|
|
|
144
144
|
verify: collectionOptions.verify ?? defaultVerify({
|
|
145
145
|
collection: existingCollection,
|
|
146
146
|
strategyName,
|
|
147
|
+
userInfoCookieName: pluginOptions?.userInfoCookieName,
|
|
147
148
|
usernameField
|
|
148
149
|
})
|
|
149
150
|
});
|
package/dist/oauth/types.d.ts
CHANGED
|
@@ -148,6 +148,16 @@ export interface PluginOptions {
|
|
|
148
148
|
* @default 'oauth'
|
|
149
149
|
*/
|
|
150
150
|
strategyName?: string;
|
|
151
|
+
/**
|
|
152
|
+
* The name of the short-lived cookie used to carry Figma user profile
|
|
153
|
+
* info from the login endpoint to `defaultVerify`.
|
|
154
|
+
*
|
|
155
|
+
* Override in local dev when running multiple Payload apps on localhost
|
|
156
|
+
* to avoid cookie collisions (all localhost apps share a cookie jar).
|
|
157
|
+
*
|
|
158
|
+
* @default 'figma-user-info'
|
|
159
|
+
*/
|
|
160
|
+
userInfoCookieName?: string;
|
|
151
161
|
}
|
|
152
162
|
export type CookieOptions = {
|
|
153
163
|
domain?: string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TokenStore } from '../auth/token-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Print a one-shot banner when the plugin is about to use `tokenStore`
|
|
4
|
+
* auth but no valid OAuth credential is present. Keeps the dev loop
|
|
5
|
+
* actionable instead of relying on per-request middleware errors.
|
|
6
|
+
*/
|
|
7
|
+
export declare function logMissingCliAuth(tokenStore: TokenStore): void;
|
|
8
|
+
//# sourceMappingURL=auth-preflight.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
/**
|
|
4
|
+
* Print a one-shot banner when the plugin is about to use `tokenStore`
|
|
5
|
+
* auth but no valid OAuth credential is present. Keeps the dev loop
|
|
6
|
+
* actionable instead of relying on per-request middleware errors.
|
|
7
|
+
*/ export function logMissingCliAuth(tokenStore) {
|
|
8
|
+
if (tokenStore.hasValidOauthToken()) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const lines = [
|
|
12
|
+
'Not logged in to the Figma CLI.',
|
|
13
|
+
'Content API requests will fail until you authenticate.',
|
|
14
|
+
'',
|
|
15
|
+
`Run ${pc.cyan('npx @payloadcms/figma login')} to log in, then restart the dev server.`
|
|
16
|
+
];
|
|
17
|
+
p.note(lines.join('\n'), pc.yellow('Figma authentication required'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=auth-preflight.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reason the Content System ID could not be resolved. Drives the
|
|
3
|
+
* banner's copy so the dev sees the specific remediation step.
|
|
4
|
+
*/
|
|
5
|
+
export type MissingContentSystemIdReason = {
|
|
6
|
+
environmentName: string;
|
|
7
|
+
kind: 'environment-not-found';
|
|
8
|
+
projectId: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'network';
|
|
11
|
+
message: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'no-config';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'unauthenticated';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Print a banner explaining why Content System ID resolution failed and
|
|
19
|
+
* what to run next. Called just before `buildFigmaConfig` throws, so the
|
|
20
|
+
* actionable step is visible above the stack trace.
|
|
21
|
+
*/
|
|
22
|
+
export declare function logMissingContentSystemId(reason: MissingContentSystemIdReason): void;
|
|
23
|
+
//# sourceMappingURL=bootstrap-preflight.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
/**
|
|
4
|
+
* Print a banner explaining why Content System ID resolution failed and
|
|
5
|
+
* what to run next. Called just before `buildFigmaConfig` throws, so the
|
|
6
|
+
* actionable step is visible above the stack trace.
|
|
7
|
+
*/ export function logMissingContentSystemId(reason) {
|
|
8
|
+
const lines = buildBody(reason);
|
|
9
|
+
p.note(lines.join('\n'), pc.yellow('Figma project not configured'));
|
|
10
|
+
}
|
|
11
|
+
function buildBody(reason) {
|
|
12
|
+
const loginCmd = pc.cyan('npx @payloadcms/figma login');
|
|
13
|
+
const initCmd = pc.cyan('npx @payloadcms/figma init');
|
|
14
|
+
const initForceCmd = pc.cyan('npx @payloadcms/figma init --force');
|
|
15
|
+
if (reason.kind === 'no-config') {
|
|
16
|
+
return [
|
|
17
|
+
'No Figma project configuration found.',
|
|
18
|
+
'',
|
|
19
|
+
`Run ${initCmd} to set up this project.`
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
if (reason.kind === 'unauthenticated') {
|
|
23
|
+
return [
|
|
24
|
+
'Project config found, but you are not logged in.',
|
|
25
|
+
'Cannot fetch bootstrap data without a valid session.',
|
|
26
|
+
'',
|
|
27
|
+
`Run ${loginCmd}, then restart the dev server.`
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
if (reason.kind === 'network') {
|
|
31
|
+
return [
|
|
32
|
+
'Could not reach the Figma API to fetch bootstrap data.',
|
|
33
|
+
`Error: ${reason.message}`,
|
|
34
|
+
'',
|
|
35
|
+
'Check your network, then restart the dev server.'
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
`Environment ${pc.bold(reason.environmentName)} not found for project ${pc.bold(reason.projectId)}.`,
|
|
40
|
+
'',
|
|
41
|
+
`Run ${initForceCmd} to reconfigure, or set FIGMA_ENVIRONMENT_NAME to a valid environment.`
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=bootstrap-preflight.js.map
|
|
@@ -15,7 +15,11 @@ import { createStorageClient } from '../storage-content-api/client.js';
|
|
|
15
15
|
import { getGenerateSignedURLHandler } from '../storage-content-api/client-uploads/generateSignedURL.js';
|
|
16
16
|
import { contentApiStorageAdapter } from '../storage-content-api/index.js';
|
|
17
17
|
import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
18
|
+
import { getEnvVarSync } from '../utils/env-management.js';
|
|
18
19
|
import * as log from '../utils/log.js';
|
|
20
|
+
import { logMissingCliAuth } from './auth-preflight.js';
|
|
21
|
+
import { logMissingContentSystemId } from './bootstrap-preflight.js';
|
|
22
|
+
import { getDevCookieNames } from './dev-cookie-names.js';
|
|
19
23
|
/**
|
|
20
24
|
* Fields added to users collection for Figma profile info
|
|
21
25
|
*/ const figmaUserFields = [
|
|
@@ -47,20 +51,58 @@ function missingOAuthCredential(name) {
|
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* Fetch bootstrap data from the API and cache all environments locally.
|
|
50
|
-
* Returns the requested environment's data, or
|
|
54
|
+
* Returns the requested environment's data, or a typed failure reason.
|
|
51
55
|
*/ async function resolveAndCacheBootstrap(store, projectId, environmentName) {
|
|
56
|
+
let credential;
|
|
57
|
+
try {
|
|
58
|
+
credential = await getValidCredential(store);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
reason: {
|
|
63
|
+
kind: 'network',
|
|
64
|
+
message: messageOf(error)
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!credential) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
reason: {
|
|
72
|
+
kind: 'unauthenticated'
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
52
76
|
try {
|
|
53
|
-
const credential = await getValidCredential(store);
|
|
54
|
-
if (!credential) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
77
|
const bootstrapInfo = await getBootstrapInfo(credential, projectId);
|
|
58
78
|
cacheAllEnvironments(store, projectId, bootstrapInfo);
|
|
59
|
-
return store.getBootstrapData(projectId, environmentName);
|
|
60
79
|
} catch (error) {
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: {
|
|
83
|
+
kind: 'network',
|
|
84
|
+
message: messageOf(error)
|
|
85
|
+
}
|
|
86
|
+
};
|
|
63
87
|
}
|
|
88
|
+
const data = store.getBootstrapData(projectId, environmentName);
|
|
89
|
+
if (!data) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: {
|
|
93
|
+
environmentName,
|
|
94
|
+
kind: 'environment-not-found',
|
|
95
|
+
projectId
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
data,
|
|
101
|
+
ok: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function messageOf(error) {
|
|
105
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
64
106
|
}
|
|
65
107
|
export async function buildFigmaConfig(config) {
|
|
66
108
|
const envConfig = getEnvConfig();
|
|
@@ -68,16 +110,27 @@ export async function buildFigmaConfig(config) {
|
|
|
68
110
|
let { contentSystemId } = config.figma;
|
|
69
111
|
contentSystemId ??= process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID;
|
|
70
112
|
let bootstrapData = null;
|
|
113
|
+
let bootstrapReason = null;
|
|
71
114
|
// Local dev: resolve from project ID + bootstrap cache
|
|
115
|
+
// Use getEnvVarSync fallback because Next.js evaluates config before dotenv populates process.env
|
|
72
116
|
if (!contentSystemId) {
|
|
73
|
-
const projectId = process.env.FIGMA_PROJECT_ID;
|
|
74
|
-
const environmentName = process.env.FIGMA_ENVIRONMENT_NAME;
|
|
75
|
-
if (projectId
|
|
117
|
+
const projectId = process.env.FIGMA_PROJECT_ID ?? getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID');
|
|
118
|
+
const environmentName = process.env.FIGMA_ENVIRONMENT_NAME ?? getEnvVarSync(process.cwd(), 'FIGMA_ENVIRONMENT_NAME');
|
|
119
|
+
if (!projectId || !environmentName) {
|
|
120
|
+
bootstrapReason = {
|
|
121
|
+
kind: 'no-config'
|
|
122
|
+
};
|
|
123
|
+
} else {
|
|
76
124
|
const store = getTokenStore();
|
|
77
125
|
bootstrapData = store.getBootstrapData(projectId, environmentName);
|
|
78
126
|
// Runtime fallback: fetch from API if not cached (e.g. environment switch without re-init)
|
|
79
127
|
if (!bootstrapData) {
|
|
80
|
-
|
|
128
|
+
const resolution = await resolveAndCacheBootstrap(store, projectId, environmentName);
|
|
129
|
+
if (resolution.ok) {
|
|
130
|
+
bootstrapData = resolution.data;
|
|
131
|
+
} else {
|
|
132
|
+
bootstrapReason = resolution.reason;
|
|
133
|
+
}
|
|
81
134
|
}
|
|
82
135
|
if (bootstrapData) {
|
|
83
136
|
contentSystemId = bootstrapData.contentSystemId;
|
|
@@ -85,10 +138,17 @@ export async function buildFigmaConfig(config) {
|
|
|
85
138
|
}
|
|
86
139
|
}
|
|
87
140
|
if (!contentSystemId) {
|
|
141
|
+
logMissingContentSystemId(bootstrapReason ?? {
|
|
142
|
+
kind: 'no-config'
|
|
143
|
+
});
|
|
88
144
|
throw new Error('Content System ID could not be resolved. ' + 'Run `npx @payloadcms/figma init` to set up your project.');
|
|
89
145
|
}
|
|
90
146
|
const url = process.env.FIGMA_CONTENT_API_URL || envConfig.contentApiUrl;
|
|
91
147
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
148
|
+
const usesTokenStoreAuth = !process.env.FIGMA_CONTENT_API_ACCESS_KEY && process.env.FIGMA_DEV_JWT !== 'true';
|
|
149
|
+
if (usesTokenStoreAuth) {
|
|
150
|
+
logMissingCliAuth(getTokenStore());
|
|
151
|
+
}
|
|
92
152
|
// Determine database adapter based on environment
|
|
93
153
|
let db;
|
|
94
154
|
if (config.figma.useContentSystem === false) {
|
|
@@ -219,6 +279,7 @@ export async function buildFigmaConfig(config) {
|
|
|
219
279
|
usernameField: 'email'
|
|
220
280
|
}
|
|
221
281
|
],
|
|
282
|
+
...isProduction ? {} : getDevCookieNames(contentSystemId),
|
|
222
283
|
debug: !!process.env.DEBUG,
|
|
223
284
|
disabled: false
|
|
224
285
|
}),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace OAuth cookies per-app in local dev so multiple Payload apps
|
|
3
|
+
* running on localhost (shared cookie jar across ports) don't clobber each
|
|
4
|
+
* other's sessions on logout. Uses projectId so the cookie stays stable when
|
|
5
|
+
* switching environments in the same checkout; falls back to contentSystemId
|
|
6
|
+
* when projectId isn't in `.env`.
|
|
7
|
+
*
|
|
8
|
+
* In production, cookies are isolated by unique hostnames and this is unused.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getDevCookieNames(contentSystemId: string): {
|
|
11
|
+
cookieName: string;
|
|
12
|
+
userInfoCookieName: string;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=dev-cookie-names.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getEnvVarSync } from '../utils/env-management.js';
|
|
2
|
+
/**
|
|
3
|
+
* Namespace OAuth cookies per-app in local dev so multiple Payload apps
|
|
4
|
+
* running on localhost (shared cookie jar across ports) don't clobber each
|
|
5
|
+
* other's sessions on logout. Uses projectId so the cookie stays stable when
|
|
6
|
+
* switching environments in the same checkout; falls back to contentSystemId
|
|
7
|
+
* when projectId isn't in `.env`.
|
|
8
|
+
*
|
|
9
|
+
* In production, cookies are isolated by unique hostnames and this is unused.
|
|
10
|
+
*/ export function getDevCookieNames(contentSystemId) {
|
|
11
|
+
const projectId = process.env.FIGMA_PROJECT_ID ?? getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID') ?? contentSystemId;
|
|
12
|
+
const suffix = projectId.slice(0, 8);
|
|
13
|
+
return {
|
|
14
|
+
cookieName: `figma-${suffix}-oauth-token`,
|
|
15
|
+
userInfoCookieName: `figma-${suffix}-user-info`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=dev-cookie-names.js.map
|