@payloadcms/figma 0.0.1-alpha.61 → 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/api/control-plane.d.ts +4 -0
- package/dist/api/control-plane.js +11 -4
- 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 +9 -1
- package/dist/commands/debug.js +16 -12
- package/dist/commands/deploy.js +2 -0
- package/dist/commands/dump-tokens.d.ts +12 -0
- package/dist/commands/dump-tokens.js +69 -0
- package/dist/commands/init.js +8 -4
- package/dist/commands/list-tokens.js +2 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +23 -4
- package/dist/config/oauth.d.ts +10 -2
- package/dist/config/oauth.js +18 -4
- package/dist/constants.d.ts +4 -2
- package/dist/constants.js +19 -1
- package/dist/db-content-api/index.js +21 -0
- package/dist/db-content-api/utilities/auth.js +4 -1
- package/dist/db-content-api/utilities/data/validateRelationships.d.ts +7 -0
- package/dist/db-content-api/utilities/data/validateRelationships.js +102 -0
- 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/storage-content-api/staticHandler.js +1 -18
- package/dist/utils/build-lambda-zip.d.ts +4 -2
- package/dist/utils/build-lambda-zip.js +4 -7
- package/dist/utils/messages.js +1 -1
- package/dist/utils/token-display.d.ts +5 -1
- package/dist/utils/token-display.js +47 -26
- package/dist/utils/version-check.js +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import pc from 'picocolors';
|
|
|
4
4
|
import { buildLambdaZipCommand } from './commands/build-lambda-zip.js';
|
|
5
5
|
import { debugCommand } from './commands/debug.js';
|
|
6
6
|
import { deployCommand } from './commands/deploy.js';
|
|
7
|
+
import { dumpTokensCommand } from './commands/dump-tokens.js';
|
|
7
8
|
import { envCommand } from './commands/env.js';
|
|
8
9
|
import { initCommand } from './commands/init.js';
|
|
9
10
|
import { listTokensCommand } from './commands/list-tokens.js';
|
|
@@ -68,9 +69,11 @@ class Main {
|
|
|
68
69
|
setInfraEnvironment('production');
|
|
69
70
|
} else if (infraEnvArg === 'staging') {
|
|
70
71
|
setInfraEnvironment('staging');
|
|
72
|
+
} else if (infraEnvArg === 'devbox') {
|
|
73
|
+
setInfraEnvironment('devbox');
|
|
71
74
|
} else {
|
|
72
75
|
// eslint-disable-next-line no-console
|
|
73
|
-
console.error(`Invalid --infra-env value: ${this.args['--infra-env']}. Use 'production' or '
|
|
76
|
+
console.error(`Invalid --infra-env value: ${this.args['--infra-env']}. Use 'production', 'staging', or 'devbox'.`);
|
|
74
77
|
process.exit(1);
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -99,6 +102,11 @@ class Main {
|
|
|
99
102
|
await debugCommand();
|
|
100
103
|
process.exit(0);
|
|
101
104
|
}
|
|
105
|
+
// dump-tokens outputs plain text
|
|
106
|
+
if (subcommand === 'dump-tokens') {
|
|
107
|
+
await dumpTokensCommand();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
102
110
|
// eslint-disable-next-line no-console
|
|
103
111
|
console.log('\n');
|
|
104
112
|
p.intro(pc.bgCyan(pc.black(' @payloadcms/figma ')));
|
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() {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -241,6 +241,8 @@ import { loginCommand } from './login.js';
|
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
243
|
const createResponse = await createDeployment(credential, tenantInstanceId, {
|
|
244
|
+
adapter: adapter.name,
|
|
245
|
+
fallback: adapter.fallback,
|
|
244
246
|
pages: pagesPayload,
|
|
245
247
|
staticAssets: assets.uploadKeys
|
|
246
248
|
});
|
|
@@ -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'));
|
|
@@ -67,7 +71,7 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
67
71
|
*
|
|
68
72
|
* @param options - Command options
|
|
69
73
|
*/ export async function initCommand(options) {
|
|
70
|
-
// Check for
|
|
74
|
+
// Check for mismatched version (non-blocking)
|
|
71
75
|
const currentVersion = await getOwnVersion();
|
|
72
76
|
await checkForUpdates(currentVersion);
|
|
73
77
|
// Check authentication
|
|
@@ -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
|
package/dist/config/oauth.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { OAuthConfig } from '../auth/types.js';
|
|
2
2
|
export declare const DEFAULT_CALLBACK_PORT = 34462;
|
|
3
3
|
/**
|
|
4
|
-
* Get OAuth configuration for current environment
|
|
5
|
-
*
|
|
4
|
+
* Get OAuth configuration for current environment.
|
|
5
|
+
*
|
|
6
|
+
* Honors env overrides:
|
|
7
|
+
* FIGMA_API_BASE_URL → tokenUrl, refreshUrl
|
|
8
|
+
* FIGMA_WEB_BASE_URL → authorizationUrl (appends /oauth)
|
|
9
|
+
* FIGMA_CLIENT_ID → clientId
|
|
10
|
+
* FIGMA_REDIRECT_URI → redirectUri
|
|
11
|
+
*
|
|
12
|
+
* When FIGMA_INFRA_ENV=devbox, FIGMA_API_BASE_URL and FIGMA_WEB_BASE_URL are
|
|
13
|
+
* required (ENV_CONFIG.devbox has empty URL defaults to fail-closed).
|
|
6
14
|
*/
|
|
7
15
|
export declare function getOAuthConfig(): OAuthConfig;
|
|
8
16
|
export declare const TOKEN_EXPIRY_BUFFER_SECONDS = 300;
|
package/dist/config/oauth.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import { getEnvConfig } from '../constants.js';
|
|
1
|
+
import { getEnvConfig, getInfraEnvironment } from '../constants.js';
|
|
2
2
|
export const DEFAULT_CALLBACK_PORT = 34462;
|
|
3
3
|
/**
|
|
4
|
-
* Get OAuth configuration for current environment
|
|
5
|
-
*
|
|
4
|
+
* Get OAuth configuration for current environment.
|
|
5
|
+
*
|
|
6
|
+
* Honors env overrides:
|
|
7
|
+
* FIGMA_API_BASE_URL → tokenUrl, refreshUrl
|
|
8
|
+
* FIGMA_WEB_BASE_URL → authorizationUrl (appends /oauth)
|
|
9
|
+
* FIGMA_CLIENT_ID → clientId
|
|
10
|
+
* FIGMA_REDIRECT_URI → redirectUri
|
|
11
|
+
*
|
|
12
|
+
* When FIGMA_INFRA_ENV=devbox, FIGMA_API_BASE_URL and FIGMA_WEB_BASE_URL are
|
|
13
|
+
* required (ENV_CONFIG.devbox has empty URL defaults to fail-closed).
|
|
6
14
|
*/ export function getOAuthConfig() {
|
|
7
|
-
const { apiBaseUrl, authorizationUrl, clientId } = getEnvConfig();
|
|
15
|
+
const { apiBaseUrl: defaultApi, authorizationUrl: defaultAuth, clientId } = getEnvConfig();
|
|
16
|
+
const apiBaseUrl = process.env.FIGMA_API_BASE_URL || defaultApi;
|
|
17
|
+
const webBase = process.env.FIGMA_WEB_BASE_URL;
|
|
18
|
+
const authorizationUrl = webBase ? `${webBase}/oauth` : defaultAuth;
|
|
19
|
+
if (getInfraEnvironment() === 'devbox' && (!apiBaseUrl || !authorizationUrl)) {
|
|
20
|
+
throw new Error('FIGMA_API_BASE_URL and FIGMA_WEB_BASE_URL must be set when FIGMA_INFRA_ENV=devbox');
|
|
21
|
+
}
|
|
8
22
|
return {
|
|
9
23
|
authorizationUrl,
|
|
10
24
|
clientId: process.env.FIGMA_CLIENT_ID || clientId,
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Figma infrastructure environment (production or
|
|
2
|
+
* Figma infrastructure environment (production, staging, or devbox).
|
|
3
|
+
* devbox is a local-dev mode that requires FIGMA_API_BASE_URL and
|
|
4
|
+
* FIGMA_WEB_BASE_URL env overrides to point at a Coder devbox.
|
|
3
5
|
*/
|
|
4
|
-
export type Environment = 'production' | 'staging';
|
|
6
|
+
export type Environment = 'devbox' | 'production' | 'staging';
|
|
5
7
|
type EnvironmentConfig = {
|
|
6
8
|
/** Figma API base URL. Override: FIGMA_API_BASE_URL */
|
|
7
9
|
apiBaseUrl: string;
|
package/dist/constants.js
CHANGED
|
@@ -18,6 +18,17 @@ import { getEnvVarSync } from './utils/env-management.js';
|
|
|
18
18
|
contentApiUrl: 'https://us-east-1.cms-tenants-001-staging.figmacontentstaging.com',
|
|
19
19
|
identityMetadata: 'https://staging.figma.com/.well-known/openid-configuration',
|
|
20
20
|
jwksUri: 'https://static.figmacontentstaging.com/.well_known/jwks.json'
|
|
21
|
+
},
|
|
22
|
+
// Empty URLs are intentional: devbox mode requires FIGMA_API_BASE_URL and
|
|
23
|
+
// FIGMA_WEB_BASE_URL to be set, validated at getOAuthConfig() time.
|
|
24
|
+
// clientId defaults to the dev seed value in sinatra/db/seeds.rb.
|
|
25
|
+
devbox: {
|
|
26
|
+
apiBaseUrl: '',
|
|
27
|
+
authorizationUrl: '',
|
|
28
|
+
clientId: 'rNZBcf3xBDmI76mQ9603su',
|
|
29
|
+
contentApiUrl: '',
|
|
30
|
+
identityMetadata: '',
|
|
31
|
+
jwksUri: ''
|
|
21
32
|
}
|
|
22
33
|
};
|
|
23
34
|
/**
|
|
@@ -58,9 +69,13 @@ export function getInfraEnvironment() {
|
|
|
58
69
|
return envOverride;
|
|
59
70
|
}
|
|
60
71
|
// Check process.env first (case-insensitive)
|
|
61
|
-
|
|
72
|
+
const processEnv = process.env.FIGMA_INFRA_ENV?.toLowerCase();
|
|
73
|
+
if (processEnv === 'staging') {
|
|
62
74
|
return 'staging';
|
|
63
75
|
}
|
|
76
|
+
if (processEnv === 'devbox') {
|
|
77
|
+
return 'devbox';
|
|
78
|
+
}
|
|
64
79
|
// Fall back to .env file in cwd (check both new and old env var names)
|
|
65
80
|
const envFileValue = getEnvVarSync(process.cwd(), 'FIGMA_INFRA_ENV') ?? getEnvVarSync(process.cwd(), 'FIGMA_ENV');
|
|
66
81
|
if (envFileValue) {
|
|
@@ -68,6 +83,9 @@ export function getInfraEnvironment() {
|
|
|
68
83
|
if (env === 'staging') {
|
|
69
84
|
return 'staging';
|
|
70
85
|
}
|
|
86
|
+
if (env === 'devbox') {
|
|
87
|
+
return 'devbox';
|
|
88
|
+
}
|
|
71
89
|
if (env !== 'production') {
|
|
72
90
|
// eslint-disable-next-line no-console
|
|
73
91
|
console.warn(`Warning: Invalid FIGMA_INFRA_ENV value "${envFileValue}" in .env file. Using production.`);
|
|
@@ -8,6 +8,7 @@ import { addFallbackSort } from './temp-utilities/sorting.js';
|
|
|
8
8
|
import { unwrapDocument, unwrapFindResponse } from './temp-utilities/unwrapDocument.js';
|
|
9
9
|
import { createAuthMiddleware, createErrorMiddleware } from './utilities/auth.js';
|
|
10
10
|
import { dataToContentAPI, resolveVersionContent } from './utilities/data/index.js';
|
|
11
|
+
import { validateRelationshipIds } from './utilities/data/validateRelationships.js';
|
|
11
12
|
import { convertPayloadJoinsToContentAPI } from './utilities/joins.js';
|
|
12
13
|
import { addFallbackLocale } from './utilities/locale/index.js';
|
|
13
14
|
import { buildMeta } from './utilities/meta/buildMeta.js';
|
|
@@ -353,6 +354,11 @@ async function findDistinct(args) {
|
|
|
353
354
|
}
|
|
354
355
|
async function updateMany(args) {
|
|
355
356
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
357
|
+
validateRelationshipIds({
|
|
358
|
+
collectionSlug: args.collection,
|
|
359
|
+
data: args.data,
|
|
360
|
+
payload: this.payload
|
|
361
|
+
});
|
|
356
362
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
357
363
|
body: {
|
|
358
364
|
collection: args.collection,
|
|
@@ -394,6 +400,11 @@ async function updateOne(args) {
|
|
|
394
400
|
equals: args.id
|
|
395
401
|
}
|
|
396
402
|
};
|
|
403
|
+
validateRelationshipIds({
|
|
404
|
+
collectionSlug: args.collection,
|
|
405
|
+
data: args.data,
|
|
406
|
+
payload: this.payload
|
|
407
|
+
});
|
|
397
408
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
398
409
|
body: {
|
|
399
410
|
collection: args.collection,
|
|
@@ -498,6 +509,11 @@ async function create(args) {
|
|
|
498
509
|
id = uuid();
|
|
499
510
|
}
|
|
500
511
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
512
|
+
validateRelationshipIds({
|
|
513
|
+
collectionSlug: args.collection,
|
|
514
|
+
data: args.data,
|
|
515
|
+
payload: this.payload
|
|
516
|
+
});
|
|
501
517
|
const { data: response, error } = await this.client.POST('/api/v0/documents:create', {
|
|
502
518
|
body: {
|
|
503
519
|
collection: args.collection,
|
|
@@ -611,6 +627,11 @@ async function upsert(args) {
|
|
|
611
627
|
documentId = uuid();
|
|
612
628
|
}
|
|
613
629
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
630
|
+
validateRelationshipIds({
|
|
631
|
+
collectionSlug: args.collection,
|
|
632
|
+
data: args.data,
|
|
633
|
+
payload: this.payload
|
|
634
|
+
});
|
|
614
635
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
615
636
|
body: {
|
|
616
637
|
collection: args.collection,
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
export declare function validateRelationshipIds({ collectionSlug, data, payload, }: {
|
|
3
|
+
collectionSlug: string;
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
payload: Payload;
|
|
6
|
+
}): void;
|
|
7
|
+
//# sourceMappingURL=validateRelationships.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { traverseFields } from 'payload';
|
|
2
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
function collectRelationshipValues({ data, fields }) {
|
|
4
|
+
const relationships = [];
|
|
5
|
+
const callback = ({ field, ref })=>{
|
|
6
|
+
if (!('name' in field) || !field.name) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (field.type !== 'relationship' && field.type !== 'upload') {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!ref || typeof ref !== 'object') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const rawValue = ref[field.name];
|
|
16
|
+
if (rawValue == null || rawValue === '') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// For localized fields, the value is a locale map (e.g. { en: "uuid", fr: "uuid" }).
|
|
20
|
+
// Iterate locale values instead of treating the map as an ID.
|
|
21
|
+
if ('localized' in field && field.localized) {
|
|
22
|
+
if (typeof rawValue === 'object' && !Array.isArray(rawValue)) {
|
|
23
|
+
for (const localeValue of Object.values(rawValue)){
|
|
24
|
+
if (localeValue != null && localeValue !== '') {
|
|
25
|
+
parseRelationshipValues(localeValue, field.relationTo, relationships);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
parseRelationshipValues(rawValue, field.relationTo, relationships);
|
|
32
|
+
};
|
|
33
|
+
traverseFields({
|
|
34
|
+
callback,
|
|
35
|
+
fields,
|
|
36
|
+
fillEmpty: false,
|
|
37
|
+
ref: data
|
|
38
|
+
});
|
|
39
|
+
return relationships;
|
|
40
|
+
}
|
|
41
|
+
function parseRelationshipValues(value, relationTo, relationships) {
|
|
42
|
+
if (Array.isArray(relationTo)) {
|
|
43
|
+
// Polymorphic (single or hasMany): value is { relationTo, value } or array of them
|
|
44
|
+
const values = Array.isArray(value) ? value : [
|
|
45
|
+
value
|
|
46
|
+
];
|
|
47
|
+
for (const v of values){
|
|
48
|
+
if (v && typeof v === 'object' && 'relationTo' in v && 'value' in v) {
|
|
49
|
+
const obj = v;
|
|
50
|
+
if (obj.value != null && obj.value !== '') {
|
|
51
|
+
relationships.push({
|
|
52
|
+
collection: obj.relationTo,
|
|
53
|
+
value: obj.value
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Non-polymorphic (single or hasMany): value is a bare ID or array of IDs
|
|
60
|
+
const values = Array.isArray(value) ? value : [
|
|
61
|
+
value
|
|
62
|
+
];
|
|
63
|
+
for (const v of values){
|
|
64
|
+
if (v != null && v !== '') {
|
|
65
|
+
relationships.push({
|
|
66
|
+
collection: relationTo,
|
|
67
|
+
value: v
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isValidId(value, customIDType) {
|
|
74
|
+
if (customIDType === 'number') {
|
|
75
|
+
const num = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
|
|
76
|
+
return Number.isFinite(num);
|
|
77
|
+
}
|
|
78
|
+
if (customIDType === 'text') {
|
|
79
|
+
return typeof value === 'string' && value.length > 0;
|
|
80
|
+
}
|
|
81
|
+
return typeof value === 'string' && UUID_REGEX.test(value);
|
|
82
|
+
}
|
|
83
|
+
export function validateRelationshipIds({ collectionSlug, data, payload }) {
|
|
84
|
+
const isGlobal = collectionSlug.startsWith('_global-');
|
|
85
|
+
const actualSlug = isGlobal ? collectionSlug.substring(8) : collectionSlug;
|
|
86
|
+
const config = isGlobal ? payload.config.globals?.find((g)=>g.slug === actualSlug) : payload.config.collections.find((c)=>c.slug === actualSlug);
|
|
87
|
+
if (!config?.fields) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const relationships = collectRelationshipValues({
|
|
91
|
+
data,
|
|
92
|
+
fields: config.fields
|
|
93
|
+
});
|
|
94
|
+
for (const { collection, value } of relationships){
|
|
95
|
+
const customIDType = payload.collections?.[collection]?.customIDType;
|
|
96
|
+
if (!isValidId(value, customIDType)) {
|
|
97
|
+
throw new Error(`Invalid relationship ID "${String(value)}" for collection "${collection}". Expected ${customIDType === 'number' ? 'a number' : customIDType === 'text' ? 'a non-empty string' : 'a valid UUID'}.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=validateRelationships.js.map
|
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
|