@payloadcms/figma 0.0.1-alpha.42 → 0.0.1-alpha.6
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 +1 -22
- package/dist/api/control-plane.d.ts.map +1 -1
- package/dist/api/control-plane.js +22 -55
- package/dist/api/control-plane.js.map +1 -1
- package/dist/api/figma-api.d.ts.map +1 -1
- package/dist/api/figma-api.js +21 -45
- package/dist/api/figma-api.js.map +1 -1
- package/dist/auth/callback-server.d.ts.map +1 -1
- package/dist/auth/callback-server.js +1 -2
- package/dist/auth/callback-server.js.map +1 -1
- package/dist/auth/crypto-utils.d.ts.map +1 -1
- package/dist/auth/crypto-utils.js +5 -5
- package/dist/auth/crypto-utils.js.map +1 -1
- package/dist/auth/jwt-validator.d.ts +1 -2
- package/dist/auth/jwt-validator.d.ts.map +1 -1
- package/dist/auth/jwt-validator.js +2 -2
- package/dist/auth/jwt-validator.js.map +1 -1
- package/dist/auth/oauth-flow.d.ts +1 -1
- package/dist/auth/oauth-flow.d.ts.map +1 -1
- package/dist/auth/oauth-flow.js +11 -15
- package/dist/auth/oauth-flow.js.map +1 -1
- package/dist/auth/project-token.d.ts.map +1 -1
- package/dist/auth/project-token.js +4 -9
- package/dist/auth/project-token.js.map +1 -1
- package/dist/auth/refresh.d.ts +1 -2
- package/dist/auth/refresh.d.ts.map +1 -1
- package/dist/auth/refresh.js +12 -25
- package/dist/auth/refresh.js.map +1 -1
- package/dist/auth/token-store.d.ts +0 -12
- package/dist/auth/token-store.d.ts.map +1 -1
- package/dist/auth/token-store.js +5 -28
- package/dist/auth/token-store.js.map +1 -1
- package/dist/auth/types.d.ts +4 -7
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -39
- package/dist/cli.js.map +1 -1
- package/dist/commands/deploy.d.ts +0 -4
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +129 -72
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +85 -105
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list-tokens.d.ts +3 -3
- package/dist/commands/list-tokens.d.ts.map +1 -1
- package/dist/commands/list-tokens.js +7 -22
- package/dist/commands/list-tokens.js.map +1 -1
- package/dist/commands/login.d.ts +1 -8
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +12 -30
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +4 -5
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logout.js +13 -56
- package/dist/commands/logout.js.map +1 -1
- package/dist/config/oauth.d.ts +14 -4
- package/dist/config/oauth.d.ts.map +1 -1
- package/dist/config/oauth.js +40 -19
- package/dist/config/oauth.js.map +1 -1
- package/dist/constants.d.ts +18 -26
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +20 -34
- package/dist/constants.js.map +1 -1
- package/dist/exports/client.d.ts +0 -1
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +0 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/oauth/components/LoginButton/index.d.ts.map +1 -1
- package/dist/oauth/components/LoginButton/index.js +6 -50
- package/dist/oauth/components/LoginButton/index.js.map +1 -1
- package/dist/oauth/components/LoginButton/index.scss +3 -50
- package/dist/oauth/components/LogoutButton/index.d.ts.map +1 -1
- package/dist/oauth/components/LogoutButton/index.js +4 -5
- package/dist/oauth/components/LogoutButton/index.js.map +1 -1
- package/dist/oauth/defaults.d.ts.map +1 -1
- package/dist/oauth/defaults.js +5 -29
- package/dist/oauth/defaults.js.map +1 -1
- package/dist/oauth/endpoints/getLoginEndpoint.d.ts.map +1 -1
- package/dist/oauth/endpoints/getLoginEndpoint.js +11 -21
- package/dist/oauth/endpoints/getLoginEndpoint.js.map +1 -1
- package/dist/oauth/endpoints/getLogoutEndpoint.d.ts.map +1 -1
- package/dist/oauth/endpoints/getLogoutEndpoint.js +4 -7
- package/dist/oauth/endpoints/getLogoutEndpoint.js.map +1 -1
- package/dist/oauth/index.d.ts.map +1 -1
- package/dist/oauth/index.js +13 -40
- package/dist/oauth/index.js.map +1 -1
- package/dist/oauth/strategy/index.d.ts +2 -0
- package/dist/oauth/strategy/index.d.ts.map +1 -1
- package/dist/oauth/strategy/index.js +15 -38
- package/dist/oauth/strategy/index.js.map +1 -1
- package/dist/oauth/types.d.ts +6 -6
- package/dist/oauth/types.d.ts.map +1 -1
- package/dist/oauth/types.js.map +1 -1
- package/dist/oauth/utilities/createCookieOptions.d.ts.map +1 -1
- package/dist/oauth/utilities/createCookieOptions.js.map +1 -1
- package/dist/oauth/utilities/getAdminPath.d.ts +8 -0
- package/dist/oauth/utilities/getAdminPath.d.ts.map +1 -0
- package/dist/oauth/utilities/getAdminPath.js +9 -0
- package/dist/oauth/utilities/getAdminPath.js.map +1 -0
- package/dist/oauth/utilities/refreshTokens.d.ts.map +1 -1
- package/dist/oauth/utilities/refreshTokens.js +1 -1
- package/dist/oauth/utilities/refreshTokens.js.map +1 -1
- package/dist/plugin/build-config.d.ts +4 -21
- package/dist/plugin/build-config.d.ts.map +1 -1
- package/dist/plugin/build-config.js +7 -113
- package/dist/plugin/build-config.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/asset-collection.d.ts +6 -3
- package/dist/utils/asset-collection.d.ts.map +1 -1
- package/dist/utils/asset-collection.js +34 -10
- package/dist/utils/asset-collection.js.map +1 -1
- package/dist/utils/config.js +1 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/env-management.d.ts +8 -9
- package/dist/utils/env-management.d.ts.map +1 -1
- package/dist/utils/env-management.js +74 -73
- package/dist/utils/env-management.js.map +1 -1
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +5 -3
- package/dist/utils/formatter.js.map +1 -1
- package/dist/utils/lambda-config.d.ts +1 -3
- package/dist/utils/lambda-config.d.ts.map +1 -1
- package/dist/utils/lambda-config.js +32 -38
- package/dist/utils/lambda-config.js.map +1 -1
- package/dist/utils/messages.js +8 -8
- package/dist/utils/messages.js.map +1 -1
- package/dist/utils/payload-config-ast.d.ts +3 -22
- package/dist/utils/payload-config-ast.d.ts.map +1 -1
- package/dist/utils/payload-config-ast.js +31 -89
- package/dist/utils/payload-config-ast.js.map +1 -1
- package/dist/utils/payload-config-finder.d.ts +2 -2
- package/dist/utils/payload-config-finder.d.ts.map +1 -1
- package/dist/utils/payload-config-finder.js +9 -48
- package/dist/utils/payload-config-finder.js.map +1 -1
- package/dist/utils/payload-config-modifier.d.ts.map +1 -1
- package/dist/utils/payload-config-modifier.js +6 -54
- package/dist/utils/payload-config-modifier.js.map +1 -1
- package/dist/utils/payload-package-check.d.ts +1 -12
- package/dist/utils/payload-package-check.d.ts.map +1 -1
- package/dist/utils/payload-package-check.js +5 -63
- package/dist/utils/payload-package-check.js.map +1 -1
- package/dist/utils/project.d.ts.map +1 -1
- package/dist/utils/project.js +5 -8
- package/dist/utils/project.js.map +1 -1
- package/dist/utils/s3-upload.d.ts +1 -0
- package/dist/utils/s3-upload.d.ts.map +1 -1
- package/dist/utils/s3-upload.js +3 -10
- package/dist/utils/s3-upload.js.map +1 -1
- package/dist/utils/token-display.d.ts.map +1 -1
- package/dist/utils/token-display.js +5 -11
- package/dist/utils/token-display.js.map +1 -1
- package/dist/utils/typescript-validator.d.ts.map +1 -1
- package/dist/utils/typescript-validator.js +6 -4
- package/dist/utils/typescript-validator.js.map +1 -1
- package/package.json +9 -27
- package/dist/commands/build-lambda-zip.d.ts +0 -5
- package/dist/commands/build-lambda-zip.d.ts.map +0 -1
- package/dist/commands/build-lambda-zip.js +0 -19
- package/dist/commands/build-lambda-zip.js.map +0 -1
- package/dist/db-content-api/README.md +0 -98
- package/dist/db-content-api/generated/content-api-types.d.ts +0 -3914
- package/dist/db-content-api/generated/content-api-types.d.ts.map +0 -1
- package/dist/db-content-api/generated/content-api-types.js +0 -7
- package/dist/db-content-api/generated/content-api-types.js.map +0 -1
- package/dist/db-content-api/index.d.ts +0 -18
- package/dist/db-content-api/index.d.ts.map +0 -1
- package/dist/db-content-api/index.js +0 -586
- package/dist/db-content-api/index.js.map +0 -1
- package/dist/db-content-api/temp-utilities/slug.d.ts +0 -2
- package/dist/db-content-api/temp-utilities/slug.d.ts.map +0 -1
- package/dist/db-content-api/temp-utilities/slug.js +0 -3
- package/dist/db-content-api/temp-utilities/slug.js.map +0 -1
- package/dist/db-content-api/temp-utilities/sorting.d.ts +0 -3
- package/dist/db-content-api/temp-utilities/sorting.d.ts.map +0 -1
- package/dist/db-content-api/temp-utilities/sorting.js +0 -32
- package/dist/db-content-api/temp-utilities/sorting.js.map +0 -1
- package/dist/db-content-api/temp-utilities/unwrapDocument.d.ts +0 -34
- package/dist/db-content-api/temp-utilities/unwrapDocument.d.ts.map +0 -1
- package/dist/db-content-api/temp-utilities/unwrapDocument.js +0 -53
- package/dist/db-content-api/temp-utilities/unwrapDocument.js.map +0 -1
- package/dist/db-content-api/utilities/auth.d.ts +0 -30
- package/dist/db-content-api/utilities/auth.d.ts.map +0 -1
- package/dist/db-content-api/utilities/auth.js +0 -81
- package/dist/db-content-api/utilities/auth.js.map +0 -1
- package/dist/db-content-api/utilities/buildPathTypes.d.ts +0 -12
- package/dist/db-content-api/utilities/buildPathTypes.d.ts.map +0 -1
- package/dist/db-content-api/utilities/buildPathTypes.js +0 -157
- package/dist/db-content-api/utilities/buildPathTypes.js.map +0 -1
- package/dist/db-content-api/utilities/data/applyDefaults.d.ts +0 -7
- package/dist/db-content-api/utilities/data/applyDefaults.d.ts.map +0 -1
- package/dist/db-content-api/utilities/data/applyDefaults.js +0 -59
- package/dist/db-content-api/utilities/data/applyDefaults.js.map +0 -1
- package/dist/db-content-api/utilities/data/castFieldValue.d.ts +0 -11
- package/dist/db-content-api/utilities/data/castFieldValue.d.ts.map +0 -1
- package/dist/db-content-api/utilities/data/castFieldValue.js +0 -66
- package/dist/db-content-api/utilities/data/castFieldValue.js.map +0 -1
- package/dist/db-content-api/utilities/data/index.d.ts +0 -32
- package/dist/db-content-api/utilities/data/index.d.ts.map +0 -1
- package/dist/db-content-api/utilities/data/index.js +0 -190
- package/dist/db-content-api/utilities/data/index.js.map +0 -1
- package/dist/db-content-api/utilities/data/removeVirtualFields.d.ts +0 -12
- package/dist/db-content-api/utilities/data/removeVirtualFields.d.ts.map +0 -1
- package/dist/db-content-api/utilities/data/removeVirtualFields.js +0 -54
- package/dist/db-content-api/utilities/data/removeVirtualFields.js.map +0 -1
- package/dist/db-content-api/utilities/data/stripFields.d.ts +0 -48
- package/dist/db-content-api/utilities/data/stripFields.d.ts.map +0 -1
- package/dist/db-content-api/utilities/data/stripFields.js +0 -195
- package/dist/db-content-api/utilities/data/stripFields.js.map +0 -1
- package/dist/db-content-api/utilities/joins.d.ts +0 -55
- package/dist/db-content-api/utilities/joins.d.ts.map +0 -1
- package/dist/db-content-api/utilities/joins.js +0 -136
- package/dist/db-content-api/utilities/joins.js.map +0 -1
- package/dist/db-content-api/utilities/where.d.ts +0 -12
- package/dist/db-content-api/utilities/where.d.ts.map +0 -1
- package/dist/db-content-api/utilities/where.js +0 -121
- package/dist/db-content-api/utilities/where.js.map +0 -1
- package/dist/endpoints/health.d.ts +0 -3
- package/dist/endpoints/health.d.ts.map +0 -1
- package/dist/endpoints/health.js +0 -11
- package/dist/endpoints/health.js.map +0 -1
- package/dist/oauth/utilities/isDuplicateKeyError.d.ts +0 -7
- package/dist/oauth/utilities/isDuplicateKeyError.d.ts.map +0 -1
- package/dist/oauth/utilities/isDuplicateKeyError.js +0 -14
- package/dist/oauth/utilities/isDuplicateKeyError.js.map +0 -1
- package/dist/storage-content-api/api-types.d.ts +0 -168
- package/dist/storage-content-api/api-types.d.ts.map +0 -1
- package/dist/storage-content-api/api-types.js +0 -6
- package/dist/storage-content-api/api-types.js.map +0 -1
- package/dist/storage-content-api/client-uploads/ClientUploadHandler.d.ts +0 -9
- package/dist/storage-content-api/client-uploads/ClientUploadHandler.d.ts.map +0 -1
- package/dist/storage-content-api/client-uploads/ClientUploadHandler.js +0 -42
- package/dist/storage-content-api/client-uploads/ClientUploadHandler.js.map +0 -1
- package/dist/storage-content-api/client-uploads/generateSignedURL.d.ts +0 -13
- package/dist/storage-content-api/client-uploads/generateSignedURL.d.ts.map +0 -1
- package/dist/storage-content-api/client-uploads/generateSignedURL.js +0 -32
- package/dist/storage-content-api/client-uploads/generateSignedURL.js.map +0 -1
- package/dist/storage-content-api/client.d.ts +0 -6
- package/dist/storage-content-api/client.d.ts.map +0 -1
- package/dist/storage-content-api/client.js +0 -36
- package/dist/storage-content-api/client.js.map +0 -1
- package/dist/storage-content-api/generateURL.d.ts +0 -7
- package/dist/storage-content-api/generateURL.d.ts.map +0 -1
- package/dist/storage-content-api/generateURL.js +0 -8
- package/dist/storage-content-api/generateURL.js.map +0 -1
- package/dist/storage-content-api/handleDelete.d.ts +0 -8
- package/dist/storage-content-api/handleDelete.d.ts.map +0 -1
- package/dist/storage-content-api/handleDelete.js +0 -17
- package/dist/storage-content-api/handleDelete.js.map +0 -1
- package/dist/storage-content-api/handleUpload.d.ts +0 -11
- package/dist/storage-content-api/handleUpload.d.ts.map +0 -1
- package/dist/storage-content-api/handleUpload.js +0 -144
- package/dist/storage-content-api/handleUpload.js.map +0 -1
- package/dist/storage-content-api/index.d.ts +0 -5
- package/dist/storage-content-api/index.d.ts.map +0 -1
- package/dist/storage-content-api/index.js +0 -31
- package/dist/storage-content-api/index.js.map +0 -1
- package/dist/storage-content-api/staticHandler.d.ts +0 -10
- package/dist/storage-content-api/staticHandler.d.ts.map +0 -1
- package/dist/storage-content-api/staticHandler.js +0 -56
- package/dist/storage-content-api/staticHandler.js.map +0 -1
- package/dist/storage-content-api/types.d.ts +0 -10
- package/dist/storage-content-api/types.d.ts.map +0 -1
- package/dist/storage-content-api/types.js +0 -3
- package/dist/storage-content-api/types.js.map +0 -1
- package/dist/templates/README.md +0 -19
- package/dist/utils/build-lambda-zip.d.ts +0 -14
- package/dist/utils/build-lambda-zip.d.ts.map +0 -1
- package/dist/utils/build-lambda-zip.js +0 -81
- package/dist/utils/build-lambda-zip.js.map +0 -1
- package/dist/utils/format-env-suffix.d.ts +0 -12
- package/dist/utils/format-env-suffix.d.ts.map +0 -1
- package/dist/utils/format-env-suffix.js +0 -13
- package/dist/utils/format-env-suffix.js.map +0 -1
- package/dist/utils/version-check.d.ts +0 -3
- package/dist/utils/version-check.d.ts.map +0 -1
- package/dist/utils/version-check.js +0 -38
- package/dist/utils/version-check.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/crypto-utils.ts"],"sourcesContent":["import Conf from 'conf'\nimport crypto from 'crypto'\nimport os from 'os'\n\n/**\n * Get or generate a random salt for key derivation\n *\n * Generates a cryptographically secure random salt on first run and persists it\n * for future use. This ensures the encryption key is unique per machine and not\n * reproducible even if machine identifiers are known.\n *\n * @returns 32-byte hex string (64 characters)\n */\nfunction getOrCreateSalt(): string {\n
|
|
1
|
+
{"version":3,"sources":["../../src/auth/crypto-utils.ts"],"sourcesContent":["import Conf from 'conf'\nimport crypto from 'crypto'\nimport os from 'os'\n\n// Configuration store for cryptographic salt\nconst cryptoConf = new Conf<{ salt: string }>({\n configName: 'payloadcms-figma-crypto',\n projectName: 'payloadcms-figma',\n})\n\n/**\n * Get or generate a random salt for key derivation\n *\n * Generates a cryptographically secure random salt on first run and persists it\n * for future use. This ensures the encryption key is unique per machine and not\n * reproducible even if machine identifiers are known.\n *\n * @returns 32-byte hex string (64 characters)\n */\nfunction getOrCreateSalt(): string {\n let salt = cryptoConf.get('salt')\n if (!salt) {\n // Generate cryptographically secure random salt (32 bytes = 256 bits)\n salt = crypto.randomBytes(32).toString('hex')\n cryptoConf.set('salt', salt)\n }\n return salt\n}\n\n/**\n * Gather machine-specific entropy sources\n *\n * Collects multiple machine and user identifiers to create a unique input\n * for key derivation. More entropy sources make the key harder to guess.\n *\n * @returns Combined machine identifier string\n * @throws Error if unable to gather sufficient entropy\n */\nfunction gatherMachineEntropy(): string {\n try {\n const userInfo = os.userInfo()\n const networkInterfaces = os.networkInterfaces()\n\n // Get MAC address from first available network interface (if available)\n const macAddress = Object.values(networkInterfaces)\n .flat()\n .find((iface) => iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')?.mac\n\n const entropy = [\n os.hostname(), // Machine hostname\n os.homedir(), // User home directory path\n userInfo.username, // OS username\n os.platform(), // Operating system platform\n os.arch(), // CPU architecture\n macAddress, // MAC address (if available)\n ].filter(Boolean) // Remove any undefined values\n\n if (entropy.length < 4) {\n throw new Error('Insufficient machine entropy available')\n }\n\n return entropy.join('::')\n } catch (error) {\n throw new Error(\n `Failed to gather machine entropy: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Derive a machine-specific encryption key for token storage\n *\n * Creates a deterministic encryption key using PBKDF2 with machine-specific\n * identifiers and a random salt. This provides stronger security than simple\n * hashing by making brute force attacks computationally expensive.\n *\n * Security model - What this protects against:\n * ✅ Accidental file exposure (cloud backups, shared drives)\n * ✅ Non-technical users reading token files\n * ✅ Brute force attacks (100k PBKDF2 iterations)\n * ✅ Key guessing even with known machine identifiers (random salt)\n *\n * Security model - What this DOES NOT protect against:\n * ❌ Local user compromise (same user can run this function)\n * ❌ Root/admin access attacks\n * ❌ Sophisticated attackers with system access\n *\n * For stronger security, use OS keychain (@napi-rs/keyring) instead.\n * This is a fallback for environments without keychain access.\n *\n * Implementation details:\n * - Uses PBKDF2 with SHA-256 and 100,000 iterations (~500ms-1s)\n * - Combines multiple machine identifiers for entropy\n * - Uses persistent random salt unique to each machine\n * - Produces consistent key for same machine (deterministic)\n * - Tokens are not portable between machines (by design)\n *\n * @returns 64-character hex string (32-byte key) suitable for AES-256 encryption\n * @throws Error if unable to derive key due to missing machine information\n */\nexport function deriveEncryptionKey(): string {\n const machineId = gatherMachineEntropy()\n const salt = getOrCreateSalt()\n\n // PBKDF2 with 100k iterations provides strong security against brute force\n // while remaining performant for CLI use (~500ms-1s key derivation time)\n const key = crypto.pbkdf2Sync(\n machineId, // Password (machine identifiers)\n salt, // Salt (random, persisted)\n 100000, // Iterations (100k for strong security)\n 32, // Key length (32 bytes = 256 bits for AES-256)\n 'sha256', // Hash algorithm\n )\n\n return key.toString('hex')\n}\n"],"names":["Conf","crypto","os","cryptoConf","configName","projectName","getOrCreateSalt","salt","get","randomBytes","toString","set","gatherMachineEntropy","userInfo","networkInterfaces","macAddress","Object","values","flat","find","iface","internal","mac","entropy","hostname","homedir","username","platform","arch","filter","Boolean","length","Error","join","error","message","deriveEncryptionKey","machineId","key","pbkdf2Sync"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AACvB,OAAOC,YAAY,SAAQ;AAC3B,OAAOC,QAAQ,KAAI;AAEnB,6CAA6C;AAC7C,MAAMC,aAAa,IAAIH,KAAuB;IAC5CI,YAAY;IACZC,aAAa;AACf;AAEA;;;;;;;;CAQC,GACD,SAASC;IACP,IAAIC,OAAOJ,WAAWK,GAAG,CAAC;IAC1B,IAAI,CAACD,MAAM;QACT,sEAAsE;QACtEA,OAAON,OAAOQ,WAAW,CAAC,IAAIC,QAAQ,CAAC;QACvCP,WAAWQ,GAAG,CAAC,QAAQJ;IACzB;IACA,OAAOA;AACT;AAEA;;;;;;;;CAQC,GACD,SAASK;IACP,IAAI;QACF,MAAMC,WAAWX,GAAGW,QAAQ;QAC5B,MAAMC,oBAAoBZ,GAAGY,iBAAiB;QAE9C,wEAAwE;QACxE,MAAMC,aAAaC,OAAOC,MAAM,CAACH,mBAC9BI,IAAI,GACJC,IAAI,CAAC,CAACC,QAAUA,SAAS,CAACA,MAAMC,QAAQ,IAAID,MAAME,GAAG,KAAK,sBAAsBA;QAEnF,MAAMC,UAAU;YACdrB,GAAGsB,QAAQ;YACXtB,GAAGuB,OAAO;YACVZ,SAASa,QAAQ;YACjBxB,GAAGyB,QAAQ;YACXzB,GAAG0B,IAAI;YACPb;SACD,CAACc,MAAM,CAACC,SAAS,8BAA8B;;QAEhD,IAAIP,QAAQQ,MAAM,GAAG,GAAG;YACtB,MAAM,IAAIC,MAAM;QAClB;QAEA,OAAOT,QAAQU,IAAI,CAAC;IACtB,EAAE,OAAOC,OAAO;QACd,MAAM,IAAIF,MACR,CAAC,kCAAkC,EAAEE,iBAAiBF,QAAQE,MAAMC,OAAO,GAAG,iBAAiB;IAEnG;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC;IACd,MAAMC,YAAYzB;IAClB,MAAML,OAAOD;IAEb,2EAA2E;IAC3E,yEAAyE;IACzE,MAAMgC,MAAMrC,OAAOsC,UAAU,CAC3BF,WACA9B,MACA,QACA,IACA;IAGF,OAAO+B,IAAI5B,QAAQ,CAAC;AACtB"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Environment } from '../constants.js';
|
|
2
1
|
import type { JWTPayload } from './types.js';
|
|
3
2
|
/**
|
|
4
3
|
* JWT validation error with specific error codes
|
|
@@ -28,7 +27,7 @@ export declare class JWTValidationError extends Error {
|
|
|
28
27
|
* @returns Promise resolving to validated JWT payload
|
|
29
28
|
* @throws {JWTValidationError} If validation fails
|
|
30
29
|
*/
|
|
31
|
-
export declare function validateProjectToken(token: string, environment:
|
|
30
|
+
export declare function validateProjectToken(token: string, environment: 'production' | 'staging'): Promise<JWTPayload>;
|
|
32
31
|
/**
|
|
33
32
|
* Clear JWKS cache (useful for testing or forcing refresh)
|
|
34
33
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-validator.d.ts","sourceRoot":"","sources":["../../src/auth/jwt-validator.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"jwt-validator.d.ts","sourceRoot":"","sources":["../../src/auth/jwt-validator.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAI5C;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGlC,IAAI,EAAE,SAAS,GAAG,mBAAmB,GAAG,aAAa,GAAG,mBAAmB;IAC3E,KAAK,CAAC,EAAE,KAAK;gBAFpB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,SAAS,GAAG,mBAAmB,GAAG,aAAa,GAAG,mBAAmB,EAC3E,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAqCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,YAAY,GAAG,SAAS,GACpC,OAAO,CAAC,UAAU,CAAC,CA8ErB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
2
|
import { JWKSMultipleMatchingKeys, JWKSNoMatchingKey, JWSInvalid, JWSSignatureVerificationFailed, JWTExpired } from 'jose/errors';
|
|
3
|
-
import {
|
|
3
|
+
import { getJWKSUri } from '../constants.js';
|
|
4
4
|
/**
|
|
5
5
|
* JWT validation error with specific error codes
|
|
6
6
|
*/ export class JWTValidationError extends Error {
|
|
@@ -28,7 +28,7 @@ import { ENV_CONFIG } from '../constants.js';
|
|
|
28
28
|
* @param environment - Figma environment ('production' or 'staging')
|
|
29
29
|
* @returns JWKS resolver function
|
|
30
30
|
*/ function getJWKSResolver(environment) {
|
|
31
|
-
const jwksUri =
|
|
31
|
+
const jwksUri = getJWKSUri(environment);
|
|
32
32
|
if (!jwksResolvers.has(jwksUri)) {
|
|
33
33
|
jwksResolvers.set(jwksUri, createRemoteJWKSet(new URL(jwksUri), {
|
|
34
34
|
cacheMaxAge: 24 * 60 * 60 * 1000,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/jwt-validator.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose'\nimport {\n JWKSMultipleMatchingKeys,\n JWKSNoMatchingKey,\n JWSInvalid,\n JWSSignatureVerificationFailed,\n JWTExpired,\n} from 'jose/errors'\n\nimport type {
|
|
1
|
+
{"version":3,"sources":["../../src/auth/jwt-validator.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose'\nimport {\n JWKSMultipleMatchingKeys,\n JWKSNoMatchingKey,\n JWSInvalid,\n JWSSignatureVerificationFailed,\n JWTExpired,\n} from 'jose/errors'\n\nimport type { JWTPayload } from './types.js'\n\nimport { getJWKSUri } from '../constants.js'\n\n/**\n * JWT validation error with specific error codes\n */\nexport class JWTValidationError extends Error {\n constructor(\n message: string,\n public code: 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID',\n public cause?: Error,\n ) {\n super(message)\n this.name = 'JWTValidationError'\n }\n}\n\n/**\n * JWKS resolvers cached per environment\n * Uses jose's createRemoteJWKSet which handles:\n * - Automatic JWKS fetching from URI\n * - Built-in caching with configurable max age\n * - Automatic key rotation handling\n * - Cooldown protection between refetches\n */\nconst jwksResolvers = new Map<string, ReturnType<typeof createRemoteJWKSet>>()\n\n/**\n * Get or create a JWKS resolver for a given environment\n *\n * The resolver is cached in module scope and reused across validations.\n * Jose handles all the complexity of fetching, caching, and rotating keys.\n *\n * @param environment - Figma environment ('production' or 'staging')\n * @returns JWKS resolver function\n */\nfunction getJWKSResolver(environment: 'production' | 'staging') {\n const jwksUri = getJWKSUri(environment)\n\n if (!jwksResolvers.has(jwksUri)) {\n jwksResolvers.set(\n jwksUri,\n createRemoteJWKSet(new URL(jwksUri), {\n cacheMaxAge: 24 * 60 * 60 * 1000, // 24 hours\n cooldownDuration: 30000, // 30 seconds between refetches\n }),\n )\n }\n\n return jwksResolvers.get(jwksUri)!\n}\n\n/**\n * Validate a project token JWT\n *\n * This function:\n * 1. Gets the JWKS resolver for the environment (cached)\n * 2. Uses jose's jwtVerify to validate signature and claims in one call\n * 3. Maps jose errors to our custom error types\n *\n * Jose automatically:\n * - Fetches JWKS from the URI (with caching)\n * - Matches the key by kid from the JWT header\n * - Verifies the signature using the correct algorithm\n * - Validates standard claims (exp, iat, nbf)\n * - Handles clock skew tolerance\n *\n * @param token - JWT token string\n * @param environment - Figma environment ('production' or 'staging')\n * @returns Promise resolving to validated JWT payload\n * @throws {JWTValidationError} If validation fails\n */\nexport async function validateProjectToken(\n token: string,\n environment: 'production' | 'staging',\n): Promise<JWTPayload> {\n try {\n const JWKS = getJWKSResolver(environment)\n\n const { payload } = await jwtVerify(token, JWKS, {\n clockTolerance: 300, // 5 minutes clock skew tolerance\n // Optional additional validation (uncomment if Figma provides these):\n // issuer: 'https://api.figma.com',\n // audience: 'payloadcms-figma',\n })\n\n return payload\n } catch (error: unknown) {\n // Map jose errors to our custom error types using instanceof checks\n // This is more reliable than checking error.code strings\n\n // Token expired\n if (error instanceof JWTExpired) {\n throw new JWTValidationError('Project token has expired', 'EXPIRED', error)\n }\n\n // Key not found in JWKS (possible key rotation or invalid kid)\n if (error instanceof JWKSNoMatchingKey) {\n throw new JWTValidationError(\n 'Key not found in JWKS - this may indicate key rotation or an invalid token',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Multiple matching keys (JWKS configuration issue)\n if (error instanceof JWKSMultipleMatchingKeys) {\n throw new JWTValidationError(\n 'Multiple matching keys found in JWKS',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Invalid signature or other verification failure\n if (error instanceof JWSSignatureVerificationFailed) {\n throw new JWTValidationError(\n 'JWT signature verification failed - token may be tampered with',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Missing or invalid kid/alg in JWT header\n if (error instanceof JWSInvalid) {\n throw new JWTValidationError(\n 'Invalid JWT structure - missing required header fields (kid or alg)',\n 'MISSING_KID',\n error,\n )\n }\n\n // Network or fetch errors (not jose errors, generic Error instances)\n if (error instanceof Error) {\n const message = error.message\n if (\n message.includes('fetch') ||\n message.includes('network') ||\n message.includes('ENOTFOUND') ||\n message.includes('ECONNREFUSED')\n ) {\n throw new JWTValidationError(`Failed to fetch JWKS: ${message}`, 'JWKS_FETCH_FAILED', error)\n }\n }\n\n // Generic error fallback\n const message = error instanceof Error ? error.message : 'Unknown error'\n throw new JWTValidationError(\n `JWT validation failed: ${message}`,\n 'SIGNATURE_INVALID',\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Clear JWKS cache (useful for testing or forcing refresh)\n *\n * This clears the cached JWKS resolvers, forcing fresh fetches\n * on the next validation. Useful for:\n * - Testing with different JWKS\n * - Forcing key rotation updates\n * - Clearing state between test runs\n */\nexport function clearJWKSCache(): void {\n jwksResolvers.clear()\n}\n"],"names":["createRemoteJWKSet","jwtVerify","JWKSMultipleMatchingKeys","JWKSNoMatchingKey","JWSInvalid","JWSSignatureVerificationFailed","JWTExpired","getJWKSUri","JWTValidationError","Error","message","code","cause","name","jwksResolvers","Map","getJWKSResolver","environment","jwksUri","has","set","URL","cacheMaxAge","cooldownDuration","get","validateProjectToken","token","JWKS","payload","clockTolerance","error","includes","undefined","clearJWKSCache","clear"],"mappings":"AAAA,SAASA,kBAAkB,EAAEC,SAAS,QAAQ,OAAM;AACpD,SACEC,wBAAwB,EACxBC,iBAAiB,EACjBC,UAAU,EACVC,8BAA8B,EAC9BC,UAAU,QACL,cAAa;AAIpB,SAASC,UAAU,QAAQ,kBAAiB;AAE5C;;CAEC,GACD,OAAO,MAAMC,2BAA2BC;;;IACtC,YACEC,OAAe,EACf,AAAOC,IAA2E,EAClF,AAAOC,KAAa,CACpB;QACA,KAAK,CAACF,eAHCC,OAAAA,WACAC,QAAAA;QAGP,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;;;;CAOC,GACD,MAAMC,gBAAgB,IAAIC;AAE1B;;;;;;;;CAQC,GACD,SAASC,gBAAgBC,WAAqC;IAC5D,MAAMC,UAAUX,WAAWU;IAE3B,IAAI,CAACH,cAAcK,GAAG,CAACD,UAAU;QAC/BJ,cAAcM,GAAG,CACfF,SACAlB,mBAAmB,IAAIqB,IAAIH,UAAU;YACnCI,aAAa,KAAK,KAAK,KAAK;YAC5BC,kBAAkB;QACpB;IAEJ;IAEA,OAAOT,cAAcU,GAAG,CAACN;AAC3B;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,eAAeO,qBACpBC,KAAa,EACbT,WAAqC;IAErC,IAAI;QACF,MAAMU,OAAOX,gBAAgBC;QAE7B,MAAM,EAAEW,OAAO,EAAE,GAAG,MAAM3B,UAAUyB,OAAOC,MAAM;YAC/CE,gBAAgB;QAIlB;QAEA,OAAOD;IACT,EAAE,OAAOE,OAAgB;QACvB,oEAAoE;QACpE,yDAAyD;QAEzD,gBAAgB;QAChB,IAAIA,iBAAiBxB,YAAY;YAC/B,MAAM,IAAIE,mBAAmB,6BAA6B,WAAWsB;QACvE;QAEA,+DAA+D;QAC/D,IAAIA,iBAAiB3B,mBAAmB;YACtC,MAAM,IAAIK,mBACR,8EACA,qBACAsB;QAEJ;QAEA,oDAAoD;QACpD,IAAIA,iBAAiB5B,0BAA0B;YAC7C,MAAM,IAAIM,mBACR,wCACA,qBACAsB;QAEJ;QAEA,kDAAkD;QAClD,IAAIA,iBAAiBzB,gCAAgC;YACnD,MAAM,IAAIG,mBACR,kEACA,qBACAsB;QAEJ;QAEA,2CAA2C;QAC3C,IAAIA,iBAAiB1B,YAAY;YAC/B,MAAM,IAAII,mBACR,uEACA,eACAsB;QAEJ;QAEA,qEAAqE;QACrE,IAAIA,iBAAiBrB,OAAO;YAC1B,MAAMC,UAAUoB,MAAMpB,OAAO;YAC7B,IACEA,QAAQqB,QAAQ,CAAC,YACjBrB,QAAQqB,QAAQ,CAAC,cACjBrB,QAAQqB,QAAQ,CAAC,gBACjBrB,QAAQqB,QAAQ,CAAC,iBACjB;gBACA,MAAM,IAAIvB,mBAAmB,CAAC,sBAAsB,EAAEE,SAAS,EAAE,qBAAqBoB;YACxF;QACF;QAEA,yBAAyB;QACzB,MAAMpB,UAAUoB,iBAAiBrB,QAAQqB,MAAMpB,OAAO,GAAG;QACzD,MAAM,IAAIF,mBACR,CAAC,uBAAuB,EAAEE,SAAS,EACnC,qBACAoB,iBAAiBrB,QAAQqB,QAAQE;IAErC;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACdnB,cAAcoB,KAAK;AACrB"}
|
|
@@ -13,7 +13,7 @@ export declare class OAuthFlowError extends Error {
|
|
|
13
13
|
export interface OAuthFlowOptions {
|
|
14
14
|
/** OAuth2 client ID (defaults to config) */
|
|
15
15
|
clientId?: string;
|
|
16
|
-
/** Callback server port (defaults to
|
|
16
|
+
/** Callback server port (defaults to 3000) */
|
|
17
17
|
port?: number;
|
|
18
18
|
/** Redirect URI (defaults to config) */
|
|
19
19
|
redirectUri?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAS7C;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAG9B,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB
|
|
1
|
+
{"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAS7C;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAG9B,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,MAAM,EAAE,WAAW,CAAA;IACnB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAsI1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAyBxF"}
|
package/dist/auth/oauth-flow.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { OAUTH_CONFIG } from '../config/oauth.js';
|
|
4
4
|
import * as log from '../utils/log.js';
|
|
5
5
|
import { buildAuthorizationUrl, openBrowser } from './browser.js';
|
|
6
6
|
import { CallbackServer } from './callback-server.js';
|
|
7
7
|
import { generatePKCEPair } from './pkce.js';
|
|
8
|
-
import { exchangeCodeForTokens, refreshAccessToken
|
|
8
|
+
import { exchangeCodeForTokens, refreshAccessToken } from './refresh.js';
|
|
9
9
|
/**
|
|
10
10
|
* Error during OAuth flow
|
|
11
11
|
*/ export class OAuthFlowError extends Error {
|
|
@@ -32,17 +32,16 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
|
|
|
32
32
|
* @throws OAuthFlowError if any step fails
|
|
33
33
|
*/ export async function executeOAuthFlow(tokenStore, options = {}) {
|
|
34
34
|
// Check if OAuth flow should be mocked
|
|
35
|
-
// Default to
|
|
36
|
-
const shouldMock = process.env.FIGMA_MOCK_OAUTH
|
|
37
|
-
const oauthConfig = getOAuthConfig();
|
|
35
|
+
// Default to true for OAuth (allow testing without real authentication by default)
|
|
36
|
+
const shouldMock = process.env.FIGMA_MOCK_OAUTH !== 'false';
|
|
38
37
|
if (shouldMock) {
|
|
39
38
|
// MOCK IMPLEMENTATION: Return mock tokens for testing
|
|
40
|
-
log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH
|
|
39
|
+
log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH != "false")');
|
|
41
40
|
const mockTokens = {
|
|
42
41
|
accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),
|
|
43
42
|
expiresAt: Date.now() + 2 * 60 * 60 * 1000,
|
|
44
43
|
refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),
|
|
45
|
-
scopes: options.scopes ||
|
|
44
|
+
scopes: options.scopes || OAUTH_CONFIG.scopes,
|
|
46
45
|
tokenType: 'Bearer'
|
|
47
46
|
};
|
|
48
47
|
// Store mock tokens
|
|
@@ -53,7 +52,7 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
|
|
|
53
52
|
};
|
|
54
53
|
}
|
|
55
54
|
// REAL OAUTH IMPLEMENTATION
|
|
56
|
-
const { clientId =
|
|
55
|
+
const { clientId = OAUTH_CONFIG.clientId, port = 3000, redirectUri = OAUTH_CONFIG.redirectUri, scopes = OAUTH_CONFIG.scopes, timeoutMs = 120000 } = options;
|
|
57
56
|
// Generate PKCE pair for enhanced security
|
|
58
57
|
const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair();
|
|
59
58
|
// Generate random state for CSRF protection
|
|
@@ -74,7 +73,7 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
|
|
|
74
73
|
const actualRedirectUri = redirectUri.replace(/:\d+/, `:${actualPort}`);
|
|
75
74
|
// Build authorization URL
|
|
76
75
|
const authUrl = buildAuthorizationUrl({
|
|
77
|
-
authorizationUrl:
|
|
76
|
+
authorizationUrl: OAUTH_CONFIG.authorizationUrl,
|
|
78
77
|
clientId,
|
|
79
78
|
codeChallenge,
|
|
80
79
|
codeChallengeMethod,
|
|
@@ -85,7 +84,7 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
|
|
|
85
84
|
// Prompt user to open browser
|
|
86
85
|
const shouldOpenBrowser = await p.confirm({
|
|
87
86
|
initialValue: true,
|
|
88
|
-
message: '
|
|
87
|
+
message: 'Log in or create Figma account in your browser?'
|
|
89
88
|
});
|
|
90
89
|
if (p.isCancel(shouldOpenBrowser)) {
|
|
91
90
|
throw new OAuthFlowError('Authentication cancelled by user');
|
|
@@ -161,11 +160,8 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
|
|
|
161
160
|
tokenStore.setTokens(tokens);
|
|
162
161
|
return tokens.accessToken;
|
|
163
162
|
} catch (error) {
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
if (isAuthFailure) {
|
|
167
|
-
tokenStore.clearTokens();
|
|
168
|
-
}
|
|
163
|
+
// Refresh failed - clear invalid tokens
|
|
164
|
+
tokenStore.clearTokens();
|
|
169
165
|
throw new OAuthFlowError('Failed to refresh access token', error instanceof Error ? error : undefined);
|
|
170
166
|
}
|
|
171
167
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/oauth-flow.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport crypto from 'crypto'\n\nimport type { TokenStore } from './token-store.js'\nimport type { FigmaTokens } from './types.js'\n\nimport { DEFAULT_CALLBACK_PORT, getOAuthConfig } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\nimport { buildAuthorizationUrl, openBrowser } from './browser.js'\nimport { CallbackServer } from './callback-server.js'\nimport { generatePKCEPair } from './pkce.js'\nimport { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './refresh.js'\n\n/**\n * Error during OAuth flow\n */\nexport class OAuthFlowError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'OAuthFlowError'\n }\n}\n\n/**\n * Options for OAuth flow\n */\nexport interface OAuthFlowOptions {\n /** OAuth2 client ID (defaults to config) */\n clientId?: string\n /** Callback server port (defaults to DEFAULT_CALLBACK_PORT) */\n port?: number\n /** Redirect URI (defaults to config) */\n redirectUri?: string\n /** OAuth scopes (defaults to config) */\n scopes?: string[]\n /** Callback timeout in milliseconds (defaults to 120000) */\n timeoutMs?: number\n}\n\n/**\n * Result of OAuth flow\n */\nexport interface OAuthFlowResult {\n /** OAuth2 tokens */\n tokens: FigmaTokens\n /** Figma user ID */\n userId?: string\n}\n\n/**\n * Execute complete OAuth2 authorization flow\n *\n * This orchestrates the full OAuth flow:\n * 1. Generate PKCE pair for security\n * 2. Start local callback server\n * 3. Open browser to authorization URL\n * 4. Wait for user to authorize\n * 5. Exchange authorization code for tokens\n * 6. Store tokens securely\n *\n * @param tokenStore - Token store for persisting tokens\n * @param options - Flow configuration options\n * @returns Promise resolving to OAuth flow result\n * @throws OAuthFlowError if any step fails\n */\nexport async function executeOAuthFlow(\n tokenStore: TokenStore,\n options: OAuthFlowOptions = {},\n): Promise<OAuthFlowResult> {\n // Check if OAuth flow should be mocked\n // Default to false for OAuth\n const shouldMock = process.env.FIGMA_MOCK_OAUTH === 'true'\n\n const oauthConfig = getOAuthConfig()\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock tokens for testing\n log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH = true)')\n const mockTokens: FigmaTokens = {\n accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),\n expiresAt: Date.now() + 2 * 60 * 60 * 1000, // 2 hours\n refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),\n scopes: (options.scopes || oauthConfig.scopes) as FigmaTokens['scopes'],\n tokenType: 'Bearer',\n }\n\n // Store mock tokens\n tokenStore.setTokens(mockTokens)\n\n return {\n tokens: mockTokens,\n userId: 'mock_user_123',\n }\n }\n\n // REAL OAUTH IMPLEMENTATION\n const {\n clientId = oauthConfig.clientId,\n port = DEFAULT_CALLBACK_PORT,\n redirectUri = oauthConfig.redirectUri,\n scopes = oauthConfig.scopes,\n timeoutMs = 120000,\n } = options\n\n // Generate PKCE pair for enhanced security\n const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair()\n\n // Generate random state for CSRF protection\n const state = crypto.randomBytes(32).toString('base64url')\n\n // Create callback server\n const callbackServer = new CallbackServer({ port, timeoutMs })\n\n try {\n // Start waiting for callback (this starts the server and captures actual port)\n const callbackPromise = callbackServer.waitForCallback(state)\n\n // Wait a bit for server to start and determine actual port\n await new Promise((resolve) => setTimeout(resolve, 100))\n\n // Get the actual port (may differ from preferred if that was in use)\n const actualPort = callbackServer.getActualPort()\n\n // Build redirect URI with actual port\n const actualRedirectUri = redirectUri.replace(/:\\d+/, `:${actualPort}`)\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n authorizationUrl: oauthConfig.authorizationUrl,\n clientId,\n codeChallenge,\n codeChallengeMethod,\n redirectUri: actualRedirectUri,\n scopes,\n state,\n })\n\n // Prompt user to open browser\n const shouldOpenBrowser = await p.confirm({\n initialValue: true,\n message: 'Authenticate with Figma in your browser?',\n })\n\n if (p.isCancel(shouldOpenBrowser)) {\n throw new OAuthFlowError('Authentication cancelled by user')\n }\n\n if (shouldOpenBrowser) {\n // Try to open browser, but don't fail if it doesn't work\n try {\n await openBrowser(authUrl)\n } catch (_error) {\n // Browser opening failed, show URL for manual opening\n log.warning('Could not open browser automatically.')\n log.info(`Please open this URL in your browser:`)\n log.info(` ${authUrl}`)\n }\n } else {\n // User declined automatic opening, show URL\n log.info(`Please open this URL manually in your browser:`)\n log.info(` ${authUrl}`)\n }\n\n // Wait for callback\n const { code } = await callbackPromise\n\n // Exchange authorization code for tokens\n let tokens: FigmaTokens\n try {\n tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId)\n } catch (error) {\n throw new OAuthFlowError(\n 'Failed to exchange authorization code for tokens',\n error instanceof Error ? error : undefined,\n )\n }\n\n // Store tokens\n try {\n tokenStore.setTokens(tokens)\n } catch (error) {\n throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined)\n }\n\n return {\n tokens,\n userId: tokens.userId,\n }\n } catch (error) {\n // Clean up server on error\n callbackServer.stop()\n\n if (error instanceof OAuthFlowError) {\n throw error\n }\n\n throw new OAuthFlowError(\n `OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n } finally {\n // Ensure server is always stopped\n callbackServer.stop()\n }\n}\n\n/**\n * Get valid access token, refreshing if necessary\n *\n * Checks if stored tokens exist and are valid. If expired, automatically\n * refreshes using the refresh token. If no tokens exist, returns null.\n *\n * @param tokenStore - Token store to use\n * @returns Promise resolving to access token or null if no valid tokens\n * @throws OAuthFlowError if refresh fails\n */\nexport async function getValidAccessToken(tokenStore: TokenStore): Promise<null | string> {\n // Check if we have valid tokens\n if (tokenStore.hasValidTokens()) {\n return tokenStore.getAccessToken()\n }\n\n // Check if we have a refresh token\n const refreshToken = tokenStore.getRefreshToken()\n if (!refreshToken) {\n return null\n }\n\n // Try to refresh\n try {\n const tokens = await refreshAccessToken(refreshToken)\n tokenStore.setTokens(tokens)\n return tokens.accessToken\n } catch (error) {\n // Only clear tokens for actual auth failures, not transient errors\n const isAuthFailure =\n error instanceof TokenRefreshError &&\n (error.statusCode === 401 ||\n error.statusCode === 403 ||\n (error.statusCode === 400 && error.errorCode === 'invalid_grant'))\n\n if (isAuthFailure) {\n tokenStore.clearTokens()\n }\n\n throw new OAuthFlowError(\n 'Failed to refresh access token',\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["p","crypto","DEFAULT_CALLBACK_PORT","getOAuthConfig","log","buildAuthorizationUrl","openBrowser","CallbackServer","generatePKCEPair","exchangeCodeForTokens","refreshAccessToken","TokenRefreshError","OAuthFlowError","Error","message","cause","name","executeOAuthFlow","tokenStore","options","shouldMock","process","env","FIGMA_MOCK_OAUTH","oauthConfig","debug","mockTokens","accessToken","randomBytes","toString","expiresAt","Date","now","refreshToken","scopes","tokenType","setTokens","tokens","userId","clientId","port","redirectUri","timeoutMs","codeChallenge","codeChallengeMethod","codeVerifier","state","callbackServer","callbackPromise","waitForCallback","Promise","resolve","setTimeout","actualPort","getActualPort","actualRedirectUri","replace","authUrl","authorizationUrl","shouldOpenBrowser","confirm","initialValue","isCancel","_error","warning","info","code","error","undefined","stop","getValidAccessToken","hasValidTokens","getAccessToken","getRefreshToken","isAuthFailure","statusCode","errorCode","clearTokens"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/oauth-flow.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport crypto from 'crypto'\n\nimport type { TokenStore } from './token-store.js'\nimport type { FigmaTokens } from './types.js'\n\nimport { OAUTH_CONFIG } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\nimport { buildAuthorizationUrl, openBrowser } from './browser.js'\nimport { CallbackServer } from './callback-server.js'\nimport { generatePKCEPair } from './pkce.js'\nimport { exchangeCodeForTokens, refreshAccessToken } from './refresh.js'\n\n/**\n * Error during OAuth flow\n */\nexport class OAuthFlowError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'OAuthFlowError'\n }\n}\n\n/**\n * Options for OAuth flow\n */\nexport interface OAuthFlowOptions {\n /** OAuth2 client ID (defaults to config) */\n clientId?: string\n /** Callback server port (defaults to 3000) */\n port?: number\n /** Redirect URI (defaults to config) */\n redirectUri?: string\n /** OAuth scopes (defaults to config) */\n scopes?: string[]\n /** Callback timeout in milliseconds (defaults to 120000) */\n timeoutMs?: number\n}\n\n/**\n * Result of OAuth flow\n */\nexport interface OAuthFlowResult {\n /** OAuth2 tokens */\n tokens: FigmaTokens\n /** Figma user ID */\n userId?: string\n}\n\n/**\n * Execute complete OAuth2 authorization flow\n *\n * This orchestrates the full OAuth flow:\n * 1. Generate PKCE pair for security\n * 2. Start local callback server\n * 3. Open browser to authorization URL\n * 4. Wait for user to authorize\n * 5. Exchange authorization code for tokens\n * 6. Store tokens securely\n *\n * @param tokenStore - Token store for persisting tokens\n * @param options - Flow configuration options\n * @returns Promise resolving to OAuth flow result\n * @throws OAuthFlowError if any step fails\n */\nexport async function executeOAuthFlow(\n tokenStore: TokenStore,\n options: OAuthFlowOptions = {},\n): Promise<OAuthFlowResult> {\n // Check if OAuth flow should be mocked\n // Default to true for OAuth (allow testing without real authentication by default)\n const shouldMock = process.env.FIGMA_MOCK_OAUTH !== 'false'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock tokens for testing\n log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH != \"false\")')\n const mockTokens: FigmaTokens = {\n accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),\n expiresAt: Date.now() + 2 * 60 * 60 * 1000, // 2 hours\n refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),\n scopes: (options.scopes || OAUTH_CONFIG.scopes) as FigmaTokens['scopes'],\n tokenType: 'Bearer',\n }\n\n // Store mock tokens\n tokenStore.setTokens(mockTokens)\n\n return {\n tokens: mockTokens,\n userId: 'mock_user_123',\n }\n }\n\n // REAL OAUTH IMPLEMENTATION\n const {\n clientId = OAUTH_CONFIG.clientId,\n port = 3000,\n redirectUri = OAUTH_CONFIG.redirectUri,\n scopes = OAUTH_CONFIG.scopes,\n timeoutMs = 120000,\n } = options\n\n // Generate PKCE pair for enhanced security\n const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair()\n\n // Generate random state for CSRF protection\n const state = crypto.randomBytes(32).toString('base64url')\n\n // Create callback server\n const callbackServer = new CallbackServer({ port, timeoutMs })\n\n try {\n // Start waiting for callback (this starts the server and captures actual port)\n const callbackPromise = callbackServer.waitForCallback(state)\n\n // Wait a bit for server to start and determine actual port\n await new Promise((resolve) => setTimeout(resolve, 100))\n\n // Get the actual port (may differ from preferred if that was in use)\n const actualPort = callbackServer.getActualPort()\n\n // Build redirect URI with actual port\n const actualRedirectUri = redirectUri.replace(/:\\d+/, `:${actualPort}`)\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n authorizationUrl: OAUTH_CONFIG.authorizationUrl,\n clientId,\n codeChallenge,\n codeChallengeMethod,\n redirectUri: actualRedirectUri,\n scopes,\n state,\n })\n\n // Prompt user to open browser\n const shouldOpenBrowser = await p.confirm({\n initialValue: true,\n message: 'Log in or create Figma account in your browser?',\n })\n\n if (p.isCancel(shouldOpenBrowser)) {\n throw new OAuthFlowError('Authentication cancelled by user')\n }\n\n if (shouldOpenBrowser) {\n // Try to open browser, but don't fail if it doesn't work\n try {\n await openBrowser(authUrl)\n } catch (_error) {\n // Browser opening failed, show URL for manual opening\n log.warning('Could not open browser automatically.')\n log.info(`Please open this URL in your browser:`)\n log.info(` ${authUrl}`)\n }\n } else {\n // User declined automatic opening, show URL\n log.info(`Please open this URL manually in your browser:`)\n log.info(` ${authUrl}`)\n }\n\n // Wait for callback\n const { code } = await callbackPromise\n\n // Exchange authorization code for tokens\n let tokens: FigmaTokens\n try {\n tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId)\n } catch (error) {\n throw new OAuthFlowError(\n 'Failed to exchange authorization code for tokens',\n error instanceof Error ? error : undefined,\n )\n }\n\n // Store tokens\n try {\n tokenStore.setTokens(tokens)\n } catch (error) {\n throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined)\n }\n\n return {\n tokens,\n userId: tokens.userId,\n }\n } catch (error) {\n // Clean up server on error\n callbackServer.stop()\n\n if (error instanceof OAuthFlowError) {\n throw error\n }\n\n throw new OAuthFlowError(\n `OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n } finally {\n // Ensure server is always stopped\n callbackServer.stop()\n }\n}\n\n/**\n * Get valid access token, refreshing if necessary\n *\n * Checks if stored tokens exist and are valid. If expired, automatically\n * refreshes using the refresh token. If no tokens exist, returns null.\n *\n * @param tokenStore - Token store to use\n * @returns Promise resolving to access token or null if no valid tokens\n * @throws OAuthFlowError if refresh fails\n */\nexport async function getValidAccessToken(tokenStore: TokenStore): Promise<null | string> {\n // Check if we have valid tokens\n if (tokenStore.hasValidTokens()) {\n return tokenStore.getAccessToken()\n }\n\n // Check if we have a refresh token\n const refreshToken = tokenStore.getRefreshToken()\n if (!refreshToken) {\n return null\n }\n\n // Try to refresh\n try {\n const tokens = await refreshAccessToken(refreshToken)\n tokenStore.setTokens(tokens)\n return tokens.accessToken\n } catch (error) {\n // Refresh failed - clear invalid tokens\n tokenStore.clearTokens()\n throw new OAuthFlowError(\n 'Failed to refresh access token',\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["p","crypto","OAUTH_CONFIG","log","buildAuthorizationUrl","openBrowser","CallbackServer","generatePKCEPair","exchangeCodeForTokens","refreshAccessToken","OAuthFlowError","Error","message","cause","name","executeOAuthFlow","tokenStore","options","shouldMock","process","env","FIGMA_MOCK_OAUTH","debug","mockTokens","accessToken","randomBytes","toString","expiresAt","Date","now","refreshToken","scopes","tokenType","setTokens","tokens","userId","clientId","port","redirectUri","timeoutMs","codeChallenge","codeChallengeMethod","codeVerifier","state","callbackServer","callbackPromise","waitForCallback","Promise","resolve","setTimeout","actualPort","getActualPort","actualRedirectUri","replace","authUrl","authorizationUrl","shouldOpenBrowser","confirm","initialValue","isCancel","_error","warning","info","code","error","undefined","stop","getValidAccessToken","hasValidTokens","getAccessToken","getRefreshToken","clearTokens"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-token.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AASlD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAkCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"project-token.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AASlD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAkCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAiExB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAMxB"}
|
|
@@ -60,17 +60,13 @@ import { getValidAccessToken } from './oauth-flow.js';
|
|
|
60
60
|
* @returns Promise resolving to JWT token string or null if failed
|
|
61
61
|
* @throws {ProjectTokenError} If token generation fails
|
|
62
62
|
*/ export async function getValidProjectToken(tokenStore, tenantId) {
|
|
63
|
-
if
|
|
64
|
-
log.debug('FIGMA_CONTENT_API_ACCESS_KEY is set; skipping project token retrieval from API');
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
// Check for existing valid project token
|
|
63
|
+
// Check if we have a valid project token already
|
|
68
64
|
if (tokenStore.hasValidProjectToken(tenantId)) {
|
|
69
65
|
const projectToken = tokenStore.getProjectToken(tenantId);
|
|
70
66
|
return projectToken?.token || null;
|
|
71
67
|
}
|
|
72
68
|
// We need to refresh the project token
|
|
73
|
-
//
|
|
69
|
+
// First, get a valid user access token (will auto-refresh if needed)
|
|
74
70
|
let accessToken;
|
|
75
71
|
try {
|
|
76
72
|
accessToken = await getValidAccessToken(tokenStore);
|
|
@@ -84,12 +80,11 @@ import { getValidAccessToken } from './oauth-flow.js';
|
|
|
84
80
|
// Fetch a new project token from the Figma API
|
|
85
81
|
const projectToken = await fetchProjectToken(accessToken, tenantId);
|
|
86
82
|
// Validate the JWT signature and claims (skip for mocks)
|
|
87
|
-
const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
|
|
88
83
|
const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
|
|
89
84
|
let validatedClaims;
|
|
90
|
-
if (
|
|
85
|
+
if (shouldMock) {
|
|
91
86
|
// MOCK: Skip JWT validation and parse claims directly
|
|
92
|
-
log.debug('Skipping JWT validation for mock
|
|
87
|
+
log.debug('Skipping JWT validation for mock project token');
|
|
93
88
|
validatedClaims = parseJWTClaims(projectToken.token);
|
|
94
89
|
} else {
|
|
95
90
|
// REAL: Validate JWT signature against JWKS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/project-token.ts"],"sourcesContent":["/**\n * Project Token Management\n *\n * Handles project token (JWT) generation and refresh for authenticating\n * to the Content API. Project tokens are scoped to a specific tenant/CMS\n * and have a shorter lifespan (15-30 minutes) than OAuth tokens.\n */\n\nimport type { TokenStore } from './token-store.js'\nimport type { JWTPayload } from './types.js'\n\nimport { getProjectToken as fetchProjectToken } from '../api/figma-api.js'\nimport { getEnvironment } from '../constants.js'\nimport * as log from '../utils/log.js'\nimport { JWTValidationError, validateProjectToken } from './jwt-validator.js'\nimport { getValidAccessToken } from './oauth-flow.js'\n\n/**\n * Error during project token operations\n */\nexport class ProjectTokenError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'ProjectTokenError'\n }\n}\n\n/**\n * Parse JWT claims from token without validation\n * Used for mock tokens where signature validation is not needed\n *\n * @param token - JWT token string\n * @returns Parsed JWT payload\n * @throws {ProjectTokenError} If JWT cannot be parsed\n */\nfunction parseJWTClaims(token: string): JWTPayload {\n try {\n // JWT format: header.payload.signature\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format')\n }\n\n // Decode the payload (second part)\n const payload = parts[1]\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'))\n\n return decodedPayload as JWTPayload\n } catch (error) {\n throw new ProjectTokenError(\n `Failed to parse JWT claims: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Get a valid project token for a tenant, refreshing if necessary\n *\n * This function:\n * 1. Checks if a stored project token exists and is valid (not expired)\n * 2. If expired or missing, gets a valid user access token\n * 3. Calls the Figma API to generate a new project token\n * 4. Stores the new project token in encrypted storage\n * 5. Returns the JWT string\n *\n * The project token is used by local Payload instances to authenticate\n * requests to the Content API without requiring a round-trip to Sinatra.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to get a token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token generation fails\n */\nexport async function getValidProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n
|
|
1
|
+
{"version":3,"sources":["../../src/auth/project-token.ts"],"sourcesContent":["/**\n * Project Token Management\n *\n * Handles project token (JWT) generation and refresh for authenticating\n * to the Content API. Project tokens are scoped to a specific tenant/CMS\n * and have a shorter lifespan (15-30 minutes) than OAuth tokens.\n */\n\nimport type { TokenStore } from './token-store.js'\nimport type { JWTPayload } from './types.js'\n\nimport { getProjectToken as fetchProjectToken } from '../api/figma-api.js'\nimport { getEnvironment } from '../constants.js'\nimport * as log from '../utils/log.js'\nimport { JWTValidationError, validateProjectToken } from './jwt-validator.js'\nimport { getValidAccessToken } from './oauth-flow.js'\n\n/**\n * Error during project token operations\n */\nexport class ProjectTokenError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'ProjectTokenError'\n }\n}\n\n/**\n * Parse JWT claims from token without validation\n * Used for mock tokens where signature validation is not needed\n *\n * @param token - JWT token string\n * @returns Parsed JWT payload\n * @throws {ProjectTokenError} If JWT cannot be parsed\n */\nfunction parseJWTClaims(token: string): JWTPayload {\n try {\n // JWT format: header.payload.signature\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format')\n }\n\n // Decode the payload (second part)\n const payload = parts[1]\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'))\n\n return decodedPayload as JWTPayload\n } catch (error) {\n throw new ProjectTokenError(\n `Failed to parse JWT claims: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Get a valid project token for a tenant, refreshing if necessary\n *\n * This function:\n * 1. Checks if a stored project token exists and is valid (not expired)\n * 2. If expired or missing, gets a valid user access token\n * 3. Calls the Figma API to generate a new project token\n * 4. Stores the new project token in encrypted storage\n * 5. Returns the JWT string\n *\n * The project token is used by local Payload instances to authenticate\n * requests to the Content API without requiring a round-trip to Sinatra.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to get a token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token generation fails\n */\nexport async function getValidProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n // Check if we have a valid project token already\n if (tokenStore.hasValidProjectToken(tenantId)) {\n const projectToken = tokenStore.getProjectToken(tenantId)\n return projectToken?.token || null\n }\n\n // We need to refresh the project token\n // First, get a valid user access token (will auto-refresh if needed)\n let accessToken: null | string\n try {\n accessToken = await getValidAccessToken(tokenStore)\n } catch (error) {\n throw new ProjectTokenError(\n 'Failed to get valid access token for project token refresh',\n error instanceof Error ? error : undefined,\n )\n }\n\n if (!accessToken) {\n // No valid user tokens available - user needs to authenticate\n return null\n }\n\n // Fetch a new project token from the Figma API\n const projectToken = await fetchProjectToken(accessToken, tenantId)\n\n // Validate the JWT signature and claims (skip for mocks)\n const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false'\n let validatedClaims: JWTPayload\n\n if (shouldMock) {\n // MOCK: Skip JWT validation and parse claims directly\n log.debug('Skipping JWT validation for mock project token')\n validatedClaims = parseJWTClaims(projectToken.token)\n } else {\n // REAL: Validate JWT signature against JWKS\n try {\n validatedClaims = await validateProjectToken(projectToken.token, getEnvironment())\n } catch (validationError) {\n // Handle validation errors\n if (validationError instanceof JWTValidationError) {\n throw new ProjectTokenError(\n `Project token validation failed: ${validationError.message}`,\n validationError,\n )\n }\n throw validationError\n }\n }\n\n // Ensure exp claim is present\n if (!validatedClaims.exp) {\n throw new ProjectTokenError('Project token missing expiration (exp) claim')\n }\n\n // Store the project token with validated expiration and claims\n tokenStore.setProjectToken(tenantId, {\n claims: validatedClaims,\n expiresAt: validatedClaims.exp * 1000, // Convert from seconds to milliseconds\n tenantId,\n token: projectToken.token,\n })\n\n return projectToken.token\n}\n\n/**\n * Refresh a project token for a tenant\n *\n * Forces a refresh of the project token even if the current one is still valid.\n * Useful for testing or when you want to ensure you have the freshest token.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to refresh token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token refresh fails\n */\nexport async function refreshProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n // Clear the existing project token to force a refresh\n tokenStore.clearProjectToken(tenantId)\n\n // Get a new token (which will fetch from API since we just cleared it)\n return getValidProjectToken(tokenStore, tenantId)\n}\n"],"names":["getProjectToken","fetchProjectToken","getEnvironment","log","JWTValidationError","validateProjectToken","getValidAccessToken","ProjectTokenError","Error","message","cause","name","parseJWTClaims","token","parts","split","length","payload","base64","replace","paddedBase64","padEnd","decodedPayload","JSON","parse","Buffer","from","toString","error","undefined","getValidProjectToken","tokenStore","tenantId","hasValidProjectToken","projectToken","accessToken","shouldMock","process","env","FIGMA_MOCK_PROJECT_TOKEN","validatedClaims","debug","validationError","exp","setProjectToken","claims","expiresAt","refreshProjectToken","clearProjectToken"],"mappings":"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"}
|
package/dist/auth/refresh.d.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type { FigmaTokens } from './types.js';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class TokenRefreshError extends Error {
|
|
6
6
|
statusCode?: number | undefined;
|
|
7
|
-
|
|
8
|
-
constructor(message: string, statusCode?: number | undefined, errorCode?: string | undefined);
|
|
7
|
+
constructor(message: string, statusCode?: number | undefined);
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
10
|
* Refresh an expired OAuth2 access token using a refresh token
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,YAAY,CAAA;AAKjE;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,UAAU,CAAC,EAAE,MAAM;
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,YAAY,CAAA;AAKjE;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,UAAU,CAAC,EAAE,MAAM;gBAD1B,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA;CAK7B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,MAA8B,GACvC,OAAO,CAAC,WAAW,CAAC,CAmDtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,WAAW,GAAE,MAAiC,EAC9C,QAAQ,GAAE,MAA8B,GACvC,OAAO,CAAC,WAAW,CAAC,CAuFtB"}
|
package/dist/auth/refresh.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
/* eslint-disable perfectionist/sort-objects */ import {
|
|
1
|
+
/* eslint-disable perfectionist/sort-objects */ import { OAUTH_CONFIG } from '../config/oauth.js';
|
|
2
2
|
import * as log from '../utils/log.js';
|
|
3
3
|
/**
|
|
4
4
|
* Error thrown when token refresh fails
|
|
5
5
|
*/ export class TokenRefreshError extends Error {
|
|
6
6
|
statusCode;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
super(message), this.statusCode = statusCode, this.errorCode = errorCode;
|
|
7
|
+
constructor(message, statusCode){
|
|
8
|
+
super(message), this.statusCode = statusCode;
|
|
10
9
|
this.name = 'TokenRefreshError';
|
|
11
10
|
}
|
|
12
11
|
}
|
|
@@ -20,17 +19,15 @@ import * as log from '../utils/log.js';
|
|
|
20
19
|
* @param clientId - OAuth2 client ID (defaults to config)
|
|
21
20
|
* @returns Promise resolving to new tokens
|
|
22
21
|
* @throws TokenRefreshError if refresh fails
|
|
23
|
-
*/ export async function refreshAccessToken(refreshToken, clientId) {
|
|
24
|
-
const oauthConfig = getOAuthConfig();
|
|
25
|
-
const resolvedClientId = clientId ?? oauthConfig.clientId;
|
|
22
|
+
*/ export async function refreshAccessToken(refreshToken, clientId = OAUTH_CONFIG.clientId) {
|
|
26
23
|
// Prepare form data for token refresh request (public client)
|
|
27
24
|
const params = new URLSearchParams({
|
|
28
|
-
client_id:
|
|
25
|
+
client_id: clientId,
|
|
29
26
|
grant_type: 'refresh_token',
|
|
30
27
|
refresh_token: refreshToken
|
|
31
28
|
});
|
|
32
29
|
try {
|
|
33
|
-
const response = await fetch(
|
|
30
|
+
const response = await fetch(OAUTH_CONFIG.refreshUrl, {
|
|
34
31
|
method: 'POST',
|
|
35
32
|
headers: {
|
|
36
33
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
@@ -39,14 +36,7 @@ import * as log from '../utils/log.js';
|
|
|
39
36
|
});
|
|
40
37
|
if (!response.ok) {
|
|
41
38
|
const errorText = await response.text();
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const parsed = JSON.parse(errorText);
|
|
45
|
-
errorCode = parsed.error;
|
|
46
|
-
} catch {
|
|
47
|
-
// Not JSON, leave errorCode undefined
|
|
48
|
-
}
|
|
49
|
-
throw new TokenRefreshError(`Token refresh failed: ${response.statusText}. ${errorText}`, response.status, errorCode);
|
|
39
|
+
throw new TokenRefreshError(`Token refresh failed: ${response.statusText}. ${errorText}`, response.status);
|
|
50
40
|
}
|
|
51
41
|
const data = await response.json();
|
|
52
42
|
// Validate response contains required fields
|
|
@@ -81,17 +71,14 @@ import * as log from '../utils/log.js';
|
|
|
81
71
|
* @param clientId - OAuth2 client ID (defaults to config)
|
|
82
72
|
* @returns Promise resolving to tokens
|
|
83
73
|
* @throws TokenRefreshError if exchange fails
|
|
84
|
-
*/ export async function exchangeCodeForTokens(code, codeVerifier, redirectUri, clientId) {
|
|
85
|
-
const oauthConfig = getOAuthConfig();
|
|
86
|
-
const resolvedRedirectUri = redirectUri ?? oauthConfig.redirectUri;
|
|
87
|
-
const resolvedClientId = clientId ?? oauthConfig.clientId;
|
|
74
|
+
*/ export async function exchangeCodeForTokens(code, codeVerifier, redirectUri = OAUTH_CONFIG.redirectUri, clientId = OAUTH_CONFIG.clientId) {
|
|
88
75
|
// Prepare form data for token exchange (public client with PKCE)
|
|
89
76
|
const params = new URLSearchParams({
|
|
90
|
-
client_id:
|
|
77
|
+
client_id: clientId,
|
|
91
78
|
code,
|
|
92
79
|
code_verifier: codeVerifier,
|
|
93
80
|
grant_type: 'authorization_code',
|
|
94
|
-
redirect_uri:
|
|
81
|
+
redirect_uri: redirectUri
|
|
95
82
|
});
|
|
96
83
|
try {
|
|
97
84
|
// Public client - no client secret or Basic Auth header needed
|
|
@@ -101,11 +88,11 @@ import * as log from '../utils/log.js';
|
|
|
101
88
|
};
|
|
102
89
|
// Debug logging
|
|
103
90
|
log.debug(`Token exchange request: ${JSON.stringify({
|
|
104
|
-
url:
|
|
91
|
+
url: OAUTH_CONFIG.tokenUrl,
|
|
105
92
|
headers,
|
|
106
93
|
body: params.toString()
|
|
107
94
|
})}`);
|
|
108
|
-
const response = await fetch(
|
|
95
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
109
96
|
method: 'POST',
|
|
110
97
|
headers,
|
|
111
98
|
body: params.toString()
|
package/dist/auth/refresh.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/refresh.ts"],"sourcesContent":["/* eslint-disable perfectionist/sort-objects */\nimport type { FigmaTokenResponse, FigmaTokens } from './types.js'\n\nimport { getOAuthConfig } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\n\n/**\n * Error thrown when token refresh fails\n */\nexport class TokenRefreshError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public errorCode?: string,\n ) {\n super(message)\n this.name = 'TokenRefreshError'\n }\n}\n\n/**\n * Refresh an expired OAuth2 access token using a refresh token\n *\n * Makes a POST request to Figma's token endpoint with the refresh token\n * to obtain a new access token. Uses public client pattern (no client secret).\n *\n * @param refreshToken - The refresh token to use\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to new tokens\n * @throws TokenRefreshError if refresh fails\n */\nexport async function refreshAccessToken(\n refreshToken: string,\n clientId?: string,\n): Promise<FigmaTokens> {\n const oauthConfig = getOAuthConfig()\n const resolvedClientId = clientId ?? oauthConfig.clientId\n\n // Prepare form data for token refresh request (public client)\n const params = new URLSearchParams({\n client_id: resolvedClientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n })\n\n try {\n const response = await fetch(oauthConfig.refreshUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: params.toString(),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n let errorCode: string | undefined\n try {\n const parsed = JSON.parse(errorText)\n errorCode = parsed.error\n } catch {\n // Not JSON, leave errorCode undefined\n }\n throw new TokenRefreshError(\n `Token refresh failed: ${response.statusText}. ${errorText}`,\n response.status,\n errorCode,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Exchange an authorization code for OAuth2 tokens\n *\n * This is used during the initial OAuth flow after the user authorizes the app.\n * Uses public client pattern with PKCE (code verifier is required).\n *\n * @param code - Authorization code from OAuth callback\n * @param codeVerifier - PKCE code verifier (required for public clients)\n * @param redirectUri - Redirect URI used in the authorization request\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to tokens\n * @throws TokenRefreshError if exchange fails\n */\nexport async function exchangeCodeForTokens(\n code: string,\n codeVerifier: string,\n redirectUri?: string,\n clientId?: string,\n): Promise<FigmaTokens> {\n const oauthConfig = getOAuthConfig()\n const resolvedRedirectUri = redirectUri ?? oauthConfig.redirectUri\n const resolvedClientId = clientId ?? oauthConfig.clientId\n\n // Prepare form data for token exchange (public client with PKCE)\n const params = new URLSearchParams({\n client_id: resolvedClientId,\n code,\n code_verifier: codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: resolvedRedirectUri,\n })\n\n try {\n // Public client - no client secret or Basic Auth header needed\n // Security is provided by PKCE (code_verifier in the request body)\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }\n\n // Debug logging\n log.debug(\n `Token exchange request: ${JSON.stringify({\n url: oauthConfig.tokenUrl,\n headers,\n body: params.toString(),\n })}`,\n )\n\n const response = await fetch(oauthConfig.tokenUrl, {\n method: 'POST',\n headers,\n body: params.toString(),\n })\n\n log.debug(\n `Token exchange response: ${JSON.stringify({\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n })}`,\n )\n\n if (!response.ok) {\n const errorText = await response.text()\n log.debug(`Error response body: ${errorText}`)\n\n throw new TokenRefreshError(\n `Token exchange failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n log.debug(\n `Token response: ${JSON.stringify({\n access_token: data.access_token,\n token_type: data.token_type,\n expires_in: data.expires_in,\n scope: data.scope,\n user_id_string: data.user_id_string,\n refresh_token: data.refresh_token?.substring(0, 10) + '...',\n })}`,\n )\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to exchange code for tokens: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n"],"names":["getOAuthConfig","log","TokenRefreshError","Error","message","statusCode","errorCode","name","refreshAccessToken","refreshToken","clientId","oauthConfig","resolvedClientId","params","URLSearchParams","client_id","grant_type","refresh_token","response","fetch","refreshUrl","method","headers","body","toString","ok","errorText","text","parsed","JSON","parse","error","statusText","status","data","json","access_token","expires_in","expiresAt","Date","now","accessToken","userId","user_id_string","exchangeCodeForTokens","code","codeVerifier","redirectUri","resolvedRedirectUri","code_verifier","redirect_uri","debug","stringify","url","tokenUrl","Object","fromEntries","entries","token_type","scope","substring"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/refresh.ts"],"sourcesContent":["/* eslint-disable perfectionist/sort-objects */\nimport type { FigmaTokenResponse, FigmaTokens } from './types.js'\n\nimport { OAUTH_CONFIG } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\n\n/**\n * Error thrown when token refresh fails\n */\nexport class TokenRefreshError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n ) {\n super(message)\n this.name = 'TokenRefreshError'\n }\n}\n\n/**\n * Refresh an expired OAuth2 access token using a refresh token\n *\n * Makes a POST request to Figma's token endpoint with the refresh token\n * to obtain a new access token. Uses public client pattern (no client secret).\n *\n * @param refreshToken - The refresh token to use\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to new tokens\n * @throws TokenRefreshError if refresh fails\n */\nexport async function refreshAccessToken(\n refreshToken: string,\n clientId: string = OAUTH_CONFIG.clientId,\n): Promise<FigmaTokens> {\n // Prepare form data for token refresh request (public client)\n const params = new URLSearchParams({\n client_id: clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n })\n\n try {\n const response = await fetch(OAUTH_CONFIG.refreshUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: params.toString(),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new TokenRefreshError(\n `Token refresh failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Exchange an authorization code for OAuth2 tokens\n *\n * This is used during the initial OAuth flow after the user authorizes the app.\n * Uses public client pattern with PKCE (code verifier is required).\n *\n * @param code - Authorization code from OAuth callback\n * @param codeVerifier - PKCE code verifier (required for public clients)\n * @param redirectUri - Redirect URI used in the authorization request\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to tokens\n * @throws TokenRefreshError if exchange fails\n */\nexport async function exchangeCodeForTokens(\n code: string,\n codeVerifier: string,\n redirectUri: string = OAUTH_CONFIG.redirectUri,\n clientId: string = OAUTH_CONFIG.clientId,\n): Promise<FigmaTokens> {\n // Prepare form data for token exchange (public client with PKCE)\n const params = new URLSearchParams({\n client_id: clientId,\n code,\n code_verifier: codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n })\n\n try {\n // Public client - no client secret or Basic Auth header needed\n // Security is provided by PKCE (code_verifier in the request body)\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }\n\n // Debug logging\n log.debug(\n `Token exchange request: ${JSON.stringify({\n url: OAUTH_CONFIG.tokenUrl,\n headers,\n body: params.toString(),\n })}`,\n )\n\n const response = await fetch(OAUTH_CONFIG.tokenUrl, {\n method: 'POST',\n headers,\n body: params.toString(),\n })\n\n log.debug(\n `Token exchange response: ${JSON.stringify({\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n })}`,\n )\n\n if (!response.ok) {\n const errorText = await response.text()\n log.debug(`Error response body: ${errorText}`)\n\n throw new TokenRefreshError(\n `Token exchange failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n log.debug(\n `Token response: ${JSON.stringify({\n access_token: data.access_token,\n token_type: data.token_type,\n expires_in: data.expires_in,\n scope: data.scope,\n user_id_string: data.user_id_string,\n refresh_token: data.refresh_token?.substring(0, 10) + '...',\n })}`,\n )\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to exchange code for tokens: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n"],"names":["OAUTH_CONFIG","log","TokenRefreshError","Error","message","statusCode","name","refreshAccessToken","refreshToken","clientId","params","URLSearchParams","client_id","grant_type","refresh_token","response","fetch","refreshUrl","method","headers","body","toString","ok","errorText","text","statusText","status","data","json","access_token","expires_in","expiresAt","Date","now","accessToken","userId","user_id_string","error","exchangeCodeForTokens","code","codeVerifier","redirectUri","code_verifier","redirect_uri","debug","JSON","stringify","url","tokenUrl","Object","fromEntries","entries","token_type","scope","substring"],"mappings":"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"}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import type { Environment } from '../constants.js';
|
|
2
1
|
import type { FigmaTokens, JWTPayload, ProjectToken, TokenStoreConfig } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Get the singleton TokenStore instance for an environment
|
|
5
|
-
* Use this for all production code to ensure a single shared instance per environment
|
|
6
|
-
*
|
|
7
|
-
* @param environment - The environment to get the token store for
|
|
8
|
-
* @returns The singleton TokenStore instance for that environment
|
|
9
|
-
*/
|
|
10
|
-
export declare function getTokenStore(environment: Environment): TokenStore;
|
|
11
2
|
/**
|
|
12
3
|
* Secure storage manager for Figma OAuth2 tokens
|
|
13
4
|
*
|
|
@@ -18,9 +9,6 @@ export declare function getTokenStore(environment: Environment): TokenStore;
|
|
|
18
9
|
*
|
|
19
10
|
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
20
11
|
* are set to 0600 (owner read/write only)
|
|
21
|
-
*
|
|
22
|
-
* For production code, use getTokenStore() to get the singleton instance.
|
|
23
|
-
* The constructor is still exported for test isolation.
|
|
24
12
|
*/
|
|
25
13
|
export declare class TokenStore {
|
|
26
14
|
private config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAKzF;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAA4E;gBAE9E,OAAO,CAAC,EAAE,gBAAgB;IAWtC;;;OAGG;IACH,SAAS,IAAI,WAAW,GAAG,IAAI;IAK/B;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;;OAGG;IACH,cAAc,IAAI,OAAO;IAQzB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAcpB;;;OAGG;IACH,cAAc,IAAI,IAAI,GAAG,MAAM;IAQ/B;;;OAGG;IACH,eAAe,IAAI,IAAI,GAAG,MAAM;IAKhC;;;;;OAKG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAe/D;;;OAGG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;OAKG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY;IAiBtD;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI;IAMnE;;;OAGG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMzC;;OAEG;IACH,qBAAqB,IAAI,IAAI;IAI7B;;;;;OAKG;IACH,qBAAqB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO;IAQ1D;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQ/C;;;;OAIG;IACH,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAK1D;;;OAGG;IACH,2BAA2B,IAAI,MAAM,EAAE;CAIxC"}
|