@payloadcms/figma 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +11 -0
- package/bin/cli.js +4 -0
- package/dist/api/control-plane.d.ts +124 -0
- package/dist/api/control-plane.d.ts.map +1 -0
- package/dist/api/control-plane.js +245 -0
- package/dist/api/control-plane.js.map +1 -0
- package/dist/api/control-plane.spec.d.ts +2 -0
- package/dist/api/control-plane.spec.d.ts.map +1 -0
- package/dist/api/control-plane.spec.js +296 -0
- package/dist/api/control-plane.spec.js.map +1 -0
- package/dist/api/figma-api.d.ts +31 -0
- package/dist/api/figma-api.d.ts.map +1 -0
- package/dist/api/figma-api.js +169 -0
- package/dist/api/figma-api.js.map +1 -0
- package/dist/api/figma-api.spec.d.ts +14 -0
- package/dist/api/figma-api.spec.d.ts.map +1 -0
- package/dist/api/figma-api.spec.js +201 -0
- package/dist/api/figma-api.spec.js.map +1 -0
- package/dist/auth/__tests__/fixtures.d.ts +26 -0
- package/dist/auth/__tests__/fixtures.d.ts.map +1 -0
- package/dist/auth/__tests__/fixtures.js +67 -0
- package/dist/auth/__tests__/fixtures.js.map +1 -0
- package/dist/auth/__tests__/mocks.d.ts +33 -0
- package/dist/auth/__tests__/mocks.d.ts.map +1 -0
- package/dist/auth/__tests__/mocks.js +63 -0
- package/dist/auth/__tests__/mocks.js.map +1 -0
- package/dist/auth/browser.d.ts +31 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +41 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/callback-server.d.ts +61 -0
- package/dist/auth/callback-server.d.ts.map +1 -0
- package/dist/auth/callback-server.js +168 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/callback-server.spec.d.ts +2 -0
- package/dist/auth/callback-server.spec.d.ts.map +1 -0
- package/dist/auth/callback-server.spec.js +85 -0
- package/dist/auth/callback-server.spec.js.map +1 -0
- package/dist/auth/crypto-utils.d.ts +33 -0
- package/dist/auth/crypto-utils.d.ts.map +1 -0
- package/dist/auth/crypto-utils.js +96 -0
- package/dist/auth/crypto-utils.js.map +1 -0
- package/dist/auth/crypto-utils.spec.d.ts +2 -0
- package/dist/auth/crypto-utils.spec.d.ts.map +1 -0
- package/dist/auth/crypto-utils.spec.js +36 -0
- package/dist/auth/crypto-utils.spec.js.map +1 -0
- package/dist/auth/jwt-validator.d.ts +41 -0
- package/dist/auth/jwt-validator.d.ts.map +1 -0
- package/dist/auth/jwt-validator.js +113 -0
- package/dist/auth/jwt-validator.js.map +1 -0
- package/dist/auth/jwt-validator.spec.d.ts +10 -0
- package/dist/auth/jwt-validator.spec.d.ts.map +1 -0
- package/dist/auth/jwt-validator.spec.js +236 -0
- package/dist/auth/jwt-validator.spec.js.map +1 -0
- package/dist/auth/oauth-flow.d.ts +62 -0
- package/dist/auth/oauth-flow.d.ts.map +1 -0
- package/dist/auth/oauth-flow.js +172 -0
- package/dist/auth/oauth-flow.js.map +1 -0
- package/dist/auth/oauth-flow.spec.d.ts +2 -0
- package/dist/auth/oauth-flow.spec.d.ts.map +1 -0
- package/dist/auth/oauth-flow.spec.js +134 -0
- package/dist/auth/oauth-flow.spec.js.map +1 -0
- package/dist/auth/pages/error.html +85 -0
- package/dist/auth/pages/success.html +74 -0
- package/dist/auth/pkce.d.ts +51 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +71 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/pkce.spec.d.ts +2 -0
- package/dist/auth/pkce.spec.d.ts.map +1 -0
- package/dist/auth/pkce.spec.js +32 -0
- package/dist/auth/pkce.spec.js.map +1 -0
- package/dist/auth/project-token.d.ts +47 -0
- package/dist/auth/project-token.d.ts.map +1 -0
- package/dist/auth/project-token.js +134 -0
- package/dist/auth/project-token.js.map +1 -0
- package/dist/auth/project-token.spec.d.ts +2 -0
- package/dist/auth/project-token.spec.d.ts.map +1 -0
- package/dist/auth/project-token.spec.js +332 -0
- package/dist/auth/project-token.spec.js.map +1 -0
- package/dist/auth/refresh.d.ts +35 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +149 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/auth/refresh.spec.d.ts +2 -0
- package/dist/auth/refresh.spec.d.ts.map +1 -0
- package/dist/auth/refresh.spec.js +105 -0
- package/dist/auth/refresh.spec.js.map +1 -0
- package/dist/auth/token-store.d.ts +110 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +188 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/token-store.spec.d.ts +2 -0
- package/dist/auth/token-store.spec.d.ts.map +1 -0
- package/dist/auth/token-store.spec.js +276 -0
- package/dist/auth/token-store.spec.js.map +1 -0
- package/dist/auth/types.d.ts +197 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +6 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +108 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/__tests__/test-utils.d.ts +28 -0
- package/dist/commands/__tests__/test-utils.d.ts.map +1 -0
- package/dist/commands/__tests__/test-utils.js +34 -0
- package/dist/commands/__tests__/test-utils.js.map +1 -0
- package/dist/commands/deploy.d.ts +16 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +297 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/deploy.spec.d.ts +2 -0
- package/dist/commands/deploy.spec.d.ts.map +1 -0
- package/dist/commands/deploy.spec.js +18 -0
- package/dist/commands/deploy.spec.js.map +1 -0
- package/dist/commands/init.d.ts +30 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +410 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.spec.d.ts +2 -0
- package/dist/commands/init.spec.d.ts.map +1 -0
- package/dist/commands/init.spec.js +283 -0
- package/dist/commands/init.spec.js.map +1 -0
- package/dist/commands/list-tokens.d.ts +16 -0
- package/dist/commands/list-tokens.d.ts.map +1 -0
- package/dist/commands/list-tokens.js +44 -0
- package/dist/commands/list-tokens.js.map +1 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +69 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +16 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +43 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/config/oauth.d.ts +19 -0
- package/dist/config/oauth.d.ts.map +1 -0
- package/dist/config/oauth.js +43 -0
- package/dist/config/oauth.js.map +1 -0
- package/dist/constants.d.ts +27 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +27 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/adapter.d.ts +5 -0
- package/dist/plugin/adapter.d.ts.map +1 -0
- package/dist/plugin/adapter.js +139 -0
- package/dist/plugin/adapter.js.map +1 -0
- package/dist/plugin/build-config.d.ts +37 -0
- package/dist/plugin/build-config.d.ts.map +1 -0
- package/dist/plugin/build-config.js +25 -0
- package/dist/plugin/build-config.js.map +1 -0
- package/dist/plugin/build-config.spec.d.ts +2 -0
- package/dist/plugin/build-config.spec.d.ts.map +1 -0
- package/dist/plugin/build-config.spec.js +72 -0
- package/dist/plugin/build-config.spec.js.map +1 -0
- package/dist/types/config.d.ts +45 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +9 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/asset-collection.d.ts +30 -0
- package/dist/utils/asset-collection.d.ts.map +1 -0
- package/dist/utils/asset-collection.js +117 -0
- package/dist/utils/asset-collection.js.map +1 -0
- package/dist/utils/asset-collection.spec.d.ts +2 -0
- package/dist/utils/asset-collection.spec.d.ts.map +1 -0
- package/dist/utils/asset-collection.spec.js +155 -0
- package/dist/utils/asset-collection.spec.js.map +1 -0
- package/dist/utils/build-detection.d.ts +32 -0
- package/dist/utils/build-detection.d.ts.map +1 -0
- package/dist/utils/build-detection.js +55 -0
- package/dist/utils/build-detection.js.map +1 -0
- package/dist/utils/build-detection.spec.d.ts +2 -0
- package/dist/utils/build-detection.spec.d.ts.map +1 -0
- package/dist/utils/build-detection.spec.js +110 -0
- package/dist/utils/build-detection.spec.js.map +1 -0
- package/dist/utils/config.d.ts +33 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +78 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/config.spec.d.ts +2 -0
- package/dist/utils/config.spec.d.ts.map +1 -0
- package/dist/utils/config.spec.js +167 -0
- package/dist/utils/config.spec.js.map +1 -0
- package/dist/utils/download-template.d.ts +15 -0
- package/dist/utils/download-template.d.ts.map +1 -0
- package/dist/utils/download-template.js +74 -0
- package/dist/utils/download-template.js.map +1 -0
- package/dist/utils/download-template.spec.d.ts +2 -0
- package/dist/utils/download-template.spec.d.ts.map +1 -0
- package/dist/utils/download-template.spec.js +76 -0
- package/dist/utils/download-template.spec.js.map +1 -0
- package/dist/utils/env-management.d.ts +16 -0
- package/dist/utils/env-management.d.ts.map +1 -0
- package/dist/utils/env-management.js +98 -0
- package/dist/utils/env-management.js.map +1 -0
- package/dist/utils/env-management.spec.d.ts +2 -0
- package/dist/utils/env-management.spec.d.ts.map +1 -0
- package/dist/utils/env-management.spec.js +123 -0
- package/dist/utils/env-management.spec.js.map +1 -0
- package/dist/utils/format.d.ts +23 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +49 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/formatter.d.ts +11 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +47 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/git.d.ts +15 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +92 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/git.spec.d.ts +2 -0
- package/dist/utils/git.spec.d.ts.map +1 -0
- package/dist/utils/git.spec.js +99 -0
- package/dist/utils/git.spec.js.map +1 -0
- package/dist/utils/is-debug.d.ts +6 -0
- package/dist/utils/is-debug.d.ts.map +1 -0
- package/dist/utils/is-debug.js +8 -0
- package/dist/utils/is-debug.js.map +1 -0
- package/dist/utils/lambda-config.d.ts +7 -0
- package/dist/utils/lambda-config.d.ts.map +1 -0
- package/dist/utils/lambda-config.js +144 -0
- package/dist/utils/lambda-config.js.map +1 -0
- package/dist/utils/lambda-config.spec.d.ts +2 -0
- package/dist/utils/lambda-config.spec.d.ts.map +1 -0
- package/dist/utils/lambda-config.spec.js +141 -0
- package/dist/utils/lambda-config.spec.js.map +1 -0
- package/dist/utils/log.d.ts +6 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +22 -0
- package/dist/utils/log.js.map +1 -0
- package/dist/utils/messages.d.ts +10 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js +96 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/package-manager.d.ts +12 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +37 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/payload-config-ast.d.ts +30 -0
- package/dist/utils/payload-config-ast.d.ts.map +1 -0
- package/dist/utils/payload-config-ast.js +259 -0
- package/dist/utils/payload-config-ast.js.map +1 -0
- package/dist/utils/payload-config-ast.spec.d.ts +2 -0
- package/dist/utils/payload-config-ast.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-ast.spec.js +303 -0
- package/dist/utils/payload-config-ast.spec.js.map +1 -0
- package/dist/utils/payload-config-finder.d.ts +9 -0
- package/dist/utils/payload-config-finder.d.ts.map +1 -0
- package/dist/utils/payload-config-finder.js +26 -0
- package/dist/utils/payload-config-finder.js.map +1 -0
- package/dist/utils/payload-config-modifier.d.ts +18 -0
- package/dist/utils/payload-config-modifier.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.int.spec.d.ts +2 -0
- package/dist/utils/payload-config-modifier.int.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.int.spec.js +116 -0
- package/dist/utils/payload-config-modifier.int.spec.js.map +1 -0
- package/dist/utils/payload-config-modifier.js +232 -0
- package/dist/utils/payload-config-modifier.js.map +1 -0
- package/dist/utils/payload-config-modifier.spec.d.ts +2 -0
- package/dist/utils/payload-config-modifier.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.spec.js +162 -0
- package/dist/utils/payload-config-modifier.spec.js.map +1 -0
- package/dist/utils/payload-package-check.d.ts +15 -0
- package/dist/utils/payload-package-check.d.ts.map +1 -0
- package/dist/utils/payload-package-check.js +116 -0
- package/dist/utils/payload-package-check.js.map +1 -0
- package/dist/utils/payload-package-check.spec.d.ts +2 -0
- package/dist/utils/payload-package-check.spec.d.ts.map +1 -0
- package/dist/utils/payload-package-check.spec.js +148 -0
- package/dist/utils/payload-package-check.spec.js.map +1 -0
- package/dist/utils/project.d.ts +58 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/project.js +252 -0
- package/dist/utils/project.js.map +1 -0
- package/dist/utils/project.spec.d.ts +2 -0
- package/dist/utils/project.spec.d.ts.map +1 -0
- package/dist/utils/project.spec.js +222 -0
- package/dist/utils/project.spec.js.map +1 -0
- package/dist/utils/s3-upload.d.ts +43 -0
- package/dist/utils/s3-upload.d.ts.map +1 -0
- package/dist/utils/s3-upload.js +128 -0
- package/dist/utils/s3-upload.js.map +1 -0
- package/dist/utils/s3-upload.spec.d.ts +2 -0
- package/dist/utils/s3-upload.spec.d.ts.map +1 -0
- package/dist/utils/s3-upload.spec.js +140 -0
- package/dist/utils/s3-upload.spec.js.map +1 -0
- package/dist/utils/token-display.d.ts +14 -0
- package/dist/utils/token-display.d.ts.map +1 -0
- package/dist/utils/token-display.js +62 -0
- package/dist/utils/token-display.js.map +1 -0
- package/dist/utils/typescript-validator.d.ts +11 -0
- package/dist/utils/typescript-validator.d.ts.map +1 -0
- package/dist/utils/typescript-validator.js +58 -0
- package/dist/utils/typescript-validator.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const dirname = path.dirname(filename);
|
|
7
|
+
/**
|
|
8
|
+
* Error from OAuth callback
|
|
9
|
+
*/ export class CallbackError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(message, code){
|
|
12
|
+
super(message), this.code = code;
|
|
13
|
+
this.name = 'CallbackError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Local HTTP server for handling OAuth callbacks
|
|
18
|
+
*
|
|
19
|
+
* Starts a temporary server on localhost to receive the OAuth authorization code.
|
|
20
|
+
* The server automatically shuts down after receiving the callback or on timeout.
|
|
21
|
+
*
|
|
22
|
+
* Implements dynamic port allocation: tries preferred port first, falls back to
|
|
23
|
+
* OS-assigned port if unavailable.
|
|
24
|
+
*/ export class CallbackServer {
|
|
25
|
+
actualPort;
|
|
26
|
+
preferredPort;
|
|
27
|
+
server = null;
|
|
28
|
+
timeoutMs;
|
|
29
|
+
constructor(options = {}){
|
|
30
|
+
this.preferredPort = options.port || 3000;
|
|
31
|
+
this.timeoutMs = options.timeoutMs || 120000; // 2 minutes default
|
|
32
|
+
this.server = createServer();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Render error page shown in browser when auth fails
|
|
36
|
+
*/ renderErrorPage(error, description) {
|
|
37
|
+
const template = readFileSync(path.join(dirname, 'pages', 'error.html'), 'utf-8');
|
|
38
|
+
return template.replace('{{description}}', description || 'An error occurred during authentication.').replace('{{error}}', error ? `<p>Error: <span class="error-code">${error}</span></p>` : '');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Render success page shown in browser after successful auth
|
|
42
|
+
*/ renderSuccessPage() {
|
|
43
|
+
return readFileSync(path.join(dirname, 'pages', 'success.html'), 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the actual port the server is listening on
|
|
47
|
+
* @returns Port number (may differ from preferred if that was in use)
|
|
48
|
+
*/ getActualPort() {
|
|
49
|
+
return this.actualPort || this.preferredPort;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Stop the server if running
|
|
53
|
+
*/ stop() {
|
|
54
|
+
if (this.server) {
|
|
55
|
+
this.server.close();
|
|
56
|
+
this.server = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start server and wait for OAuth callback
|
|
61
|
+
*
|
|
62
|
+
* @param expectedState - State parameter to verify CSRF protection
|
|
63
|
+
* @returns Promise resolving to callback result with code and state
|
|
64
|
+
* @throws CallbackError if callback fails or times out
|
|
65
|
+
*/ async waitForCallback(expectedState) {
|
|
66
|
+
return new Promise((resolve, reject)=>{
|
|
67
|
+
const cleanup = (handle)=>{
|
|
68
|
+
clearTimeout(handle);
|
|
69
|
+
if (this.server) {
|
|
70
|
+
this.server.close();
|
|
71
|
+
this.server = null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// Set timeout for waiting
|
|
75
|
+
const timeoutHandle = setTimeout(()=>{
|
|
76
|
+
cleanup(timeoutHandle);
|
|
77
|
+
reject(new CallbackError('OAuth authorization timed out. Please try again.', 'timeout'));
|
|
78
|
+
}, this.timeoutMs);
|
|
79
|
+
// Create HTTP server
|
|
80
|
+
this.server = createServer((req, res)=>{
|
|
81
|
+
const url = new URL(req.url || '', `http://localhost:${this.getActualPort()}`);
|
|
82
|
+
// Only handle /callback path
|
|
83
|
+
if (url.pathname !== '/callback') {
|
|
84
|
+
res.writeHead(404, {
|
|
85
|
+
'Content-Type': 'text/html'
|
|
86
|
+
});
|
|
87
|
+
res.end('<html><body><h1>404 Not Found</h1></body></html>');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Extract code and state from query params
|
|
91
|
+
const code = url.searchParams.get('code');
|
|
92
|
+
const state = url.searchParams.get('state');
|
|
93
|
+
const error = url.searchParams.get('error');
|
|
94
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
95
|
+
// Handle OAuth errors
|
|
96
|
+
if (error) {
|
|
97
|
+
cleanup(timeoutHandle);
|
|
98
|
+
res.writeHead(400, {
|
|
99
|
+
'Content-Type': 'text/html'
|
|
100
|
+
});
|
|
101
|
+
res.end(this.renderErrorPage(error, errorDescription || undefined));
|
|
102
|
+
reject(new CallbackError(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ''}`, error));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Validate required parameters
|
|
106
|
+
if (!code || !state) {
|
|
107
|
+
cleanup(timeoutHandle);
|
|
108
|
+
res.writeHead(400, {
|
|
109
|
+
'Content-Type': 'text/html'
|
|
110
|
+
});
|
|
111
|
+
res.end(this.renderErrorPage('invalid_request', 'Missing code or state parameter'));
|
|
112
|
+
reject(new CallbackError('Invalid callback: missing code or state', 'invalid_request'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Verify state matches (CSRF protection)
|
|
116
|
+
if (state !== expectedState) {
|
|
117
|
+
cleanup(timeoutHandle);
|
|
118
|
+
res.writeHead(400, {
|
|
119
|
+
'Content-Type': 'text/html'
|
|
120
|
+
});
|
|
121
|
+
res.end(this.renderErrorPage('invalid_state', 'State parameter mismatch'));
|
|
122
|
+
reject(new CallbackError('State mismatch: possible CSRF attack', 'invalid_state'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Success! Send response to browser
|
|
126
|
+
res.writeHead(200, {
|
|
127
|
+
'Content-Type': 'text/html'
|
|
128
|
+
});
|
|
129
|
+
res.end(this.renderSuccessPage());
|
|
130
|
+
// Resolve with the callback result
|
|
131
|
+
cleanup(timeoutHandle);
|
|
132
|
+
resolve({
|
|
133
|
+
code,
|
|
134
|
+
state
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// Try to start server with dynamic port allocation
|
|
138
|
+
const tryListen = (port, isRetry = false)=>{
|
|
139
|
+
if (!this.server) {
|
|
140
|
+
reject(new CallbackError('Server instance is not available', 'server_error'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.server.listen(port, '127.0.0.1', ()=>{
|
|
144
|
+
// Capture the actual port that was assigned
|
|
145
|
+
const addr = this.server.address();
|
|
146
|
+
if (addr && typeof addr === 'object') {
|
|
147
|
+
this.actualPort = addr.port;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// Handle server errors (port in use, etc)
|
|
151
|
+
this.server.once('error', (err)=>{
|
|
152
|
+
if (err.code === 'EADDRINUSE' && !isRetry) {
|
|
153
|
+
// Port in use, try with OS-assigned port (0 = dynamic)
|
|
154
|
+
this.server.removeAllListeners();
|
|
155
|
+
tryListen(0, true);
|
|
156
|
+
} else {
|
|
157
|
+
cleanup(timeoutHandle);
|
|
158
|
+
reject(new CallbackError(`Failed to start callback server: ${err.message}${!isRetry ? ' (tried fallback port)' : ''}`));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
// Start with preferred port
|
|
163
|
+
tryListen(this.preferredPort);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=callback-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/callback-server.ts"],"sourcesContent":["import { readFileSync } from 'fs'\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'http'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst filename = fileURLToPath(import.meta.url)\nconst dirname = path.dirname(filename)\n\n/**\n * Result from OAuth callback server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth provider */\n code: string\n /** State parameter for CSRF verification */\n state: string\n}\n\n/**\n * Error from OAuth callback\n */\nexport class CallbackError extends Error {\n constructor(\n message: string,\n public code?: string,\n ) {\n super(message)\n this.name = 'CallbackError'\n }\n}\n\n/**\n * Local HTTP server for handling OAuth callbacks\n *\n * Starts a temporary server on localhost to receive the OAuth authorization code.\n * The server automatically shuts down after receiving the callback or on timeout.\n *\n * Implements dynamic port allocation: tries preferred port first, falls back to\n * OS-assigned port if unavailable.\n */\nexport class CallbackServer {\n private actualPort?: number\n private preferredPort: number\n private server: null | Server = null\n private timeoutMs: number\n\n constructor(options: { port?: number; timeoutMs?: number } = {}) {\n this.preferredPort = options.port || 3000\n this.timeoutMs = options.timeoutMs || 120000 // 2 minutes default\n this.server = createServer()\n }\n\n /**\n * Render error page shown in browser when auth fails\n */\n private renderErrorPage(error: string, description?: string): string {\n const template = readFileSync(path.join(dirname, 'pages', 'error.html'), 'utf-8')\n return template\n .replace('{{description}}', description || 'An error occurred during authentication.')\n .replace('{{error}}', error ? `<p>Error: <span class=\"error-code\">${error}</span></p>` : '')\n }\n\n /**\n * Render success page shown in browser after successful auth\n */\n private renderSuccessPage(): string {\n return readFileSync(path.join(dirname, 'pages', 'success.html'), 'utf-8')\n }\n\n /**\n * Get the actual port the server is listening on\n * @returns Port number (may differ from preferred if that was in use)\n */\n getActualPort(): number {\n return this.actualPort || this.preferredPort\n }\n\n /**\n * Stop the server if running\n */\n stop(): void {\n if (this.server) {\n this.server.close()\n this.server = null\n }\n }\n\n /**\n * Start server and wait for OAuth callback\n *\n * @param expectedState - State parameter to verify CSRF protection\n * @returns Promise resolving to callback result with code and state\n * @throws CallbackError if callback fails or times out\n */\n async waitForCallback(expectedState: string): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n const cleanup = (handle: NodeJS.Timeout) => {\n clearTimeout(handle)\n if (this.server) {\n this.server.close()\n this.server = null\n }\n }\n\n // Set timeout for waiting\n const timeoutHandle = setTimeout(() => {\n cleanup(timeoutHandle)\n reject(new CallbackError('OAuth authorization timed out. Please try again.', 'timeout'))\n }, this.timeoutMs)\n\n // Create HTTP server\n this.server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '', `http://localhost:${this.getActualPort()}`)\n\n // Only handle /callback path\n if (url.pathname !== '/callback') {\n res.writeHead(404, { 'Content-Type': 'text/html' })\n res.end('<html><body><h1>404 Not Found</h1></body></html>')\n return\n }\n\n // Extract code and state from query params\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n const error = url.searchParams.get('error')\n const errorDescription = url.searchParams.get('error_description')\n\n // Handle OAuth errors\n if (error) {\n cleanup(timeoutHandle)\n res.writeHead(400, { 'Content-Type': 'text/html' })\n res.end(this.renderErrorPage(error, errorDescription || undefined))\n reject(\n new CallbackError(\n `OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ''}`,\n error,\n ),\n )\n return\n }\n\n // Validate required parameters\n if (!code || !state) {\n cleanup(timeoutHandle)\n res.writeHead(400, { 'Content-Type': 'text/html' })\n res.end(this.renderErrorPage('invalid_request', 'Missing code or state parameter'))\n reject(new CallbackError('Invalid callback: missing code or state', 'invalid_request'))\n return\n }\n\n // Verify state matches (CSRF protection)\n if (state !== expectedState) {\n cleanup(timeoutHandle)\n res.writeHead(400, { 'Content-Type': 'text/html' })\n res.end(this.renderErrorPage('invalid_state', 'State parameter mismatch'))\n reject(new CallbackError('State mismatch: possible CSRF attack', 'invalid_state'))\n return\n }\n\n // Success! Send response to browser\n res.writeHead(200, { 'Content-Type': 'text/html' })\n res.end(this.renderSuccessPage())\n\n // Resolve with the callback result\n cleanup(timeoutHandle)\n resolve({ code, state })\n })\n\n // Try to start server with dynamic port allocation\n const tryListen = (port: number, isRetry: boolean = false) => {\n if (!this.server) {\n reject(new CallbackError('Server instance is not available', 'server_error'))\n return\n }\n\n this.server.listen(port, '127.0.0.1', () => {\n // Capture the actual port that was assigned\n const addr = this.server!.address()\n if (addr && typeof addr === 'object') {\n this.actualPort = addr.port\n }\n })\n\n // Handle server errors (port in use, etc)\n this.server.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE' && !isRetry) {\n // Port in use, try with OS-assigned port (0 = dynamic)\n this.server!.removeAllListeners()\n tryListen(0, true)\n } else {\n cleanup(timeoutHandle)\n reject(\n new CallbackError(\n `Failed to start callback server: ${err.message}${!isRetry ? ' (tried fallback port)' : ''}`,\n ),\n )\n }\n })\n }\n\n // Start with preferred port\n tryListen(this.preferredPort)\n })\n }\n}\n"],"names":["readFileSync","createServer","path","fileURLToPath","filename","url","dirname","CallbackError","Error","message","code","name","CallbackServer","actualPort","preferredPort","server","timeoutMs","options","port","renderErrorPage","error","description","template","join","replace","renderSuccessPage","getActualPort","stop","close","waitForCallback","expectedState","Promise","resolve","reject","cleanup","handle","clearTimeout","timeoutHandle","setTimeout","req","res","URL","pathname","writeHead","end","searchParams","get","state","errorDescription","undefined","tryListen","isRetry","listen","addr","address","once","err","removeAllListeners"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callback-server.spec.d.ts","sourceRoot":"","sources":["../../src/auth/callback-server.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CallbackError, CallbackServer } from './callback-server.js';
|
|
3
|
+
describe('CallbackServer', ()=>{
|
|
4
|
+
describe('waitForCallback', ()=>{
|
|
5
|
+
it('should resolve with code and state on successful callback', async ()=>{
|
|
6
|
+
const server = new CallbackServer({
|
|
7
|
+
port: 3001,
|
|
8
|
+
timeoutMs: 5000
|
|
9
|
+
});
|
|
10
|
+
const expectedState = 'test-state-123';
|
|
11
|
+
// Start waiting for callback
|
|
12
|
+
const callbackPromise = server.waitForCallback(expectedState);
|
|
13
|
+
// Simulate OAuth callback
|
|
14
|
+
await fetch(`http://localhost:3001/callback?code=auth-code-123&state=${expectedState}`);
|
|
15
|
+
const result = await callbackPromise;
|
|
16
|
+
expect(result.code).toBe('auth-code-123');
|
|
17
|
+
expect(result.state).toBe(expectedState);
|
|
18
|
+
server.stop();
|
|
19
|
+
});
|
|
20
|
+
it('should reject with CallbackError on state mismatch', async ()=>{
|
|
21
|
+
const server = new CallbackServer({
|
|
22
|
+
port: 3002,
|
|
23
|
+
timeoutMs: 5000
|
|
24
|
+
});
|
|
25
|
+
const expectedState = 'test-state-123';
|
|
26
|
+
const callbackPromise = server.waitForCallback(expectedState);
|
|
27
|
+
// Send wrong state
|
|
28
|
+
const fetchPromise = fetch(`http://localhost:3002/callback?code=auth-code-123&state=wrong-state`);
|
|
29
|
+
await expect(callbackPromise).rejects.toThrow(CallbackError);
|
|
30
|
+
await expect(callbackPromise).rejects.toThrow('State mismatch');
|
|
31
|
+
// Wait for fetch to complete
|
|
32
|
+
await fetchPromise;
|
|
33
|
+
server.stop();
|
|
34
|
+
});
|
|
35
|
+
it('should reject with CallbackError on OAuth error', async ()=>{
|
|
36
|
+
const server = new CallbackServer({
|
|
37
|
+
port: 3003,
|
|
38
|
+
timeoutMs: 5000
|
|
39
|
+
});
|
|
40
|
+
const expectedState = 'test-state-123';
|
|
41
|
+
const callbackPromise = server.waitForCallback(expectedState);
|
|
42
|
+
// Send OAuth error
|
|
43
|
+
const fetchPromise = fetch(`http://localhost:3003/callback?error=access_denied&error_description=User+denied`);
|
|
44
|
+
await expect(callbackPromise).rejects.toThrow(CallbackError);
|
|
45
|
+
await expect(callbackPromise).rejects.toThrow('access_denied');
|
|
46
|
+
// Wait for fetch to complete
|
|
47
|
+
await fetchPromise;
|
|
48
|
+
server.stop();
|
|
49
|
+
});
|
|
50
|
+
it('should reject with CallbackError on timeout', async ()=>{
|
|
51
|
+
const server = new CallbackServer({
|
|
52
|
+
port: 3005,
|
|
53
|
+
timeoutMs: 100
|
|
54
|
+
});
|
|
55
|
+
const expectedState = 'test-state-123';
|
|
56
|
+
const callbackPromise = server.waitForCallback(expectedState);
|
|
57
|
+
// Don't send callback - let it timeout
|
|
58
|
+
await expect(callbackPromise).rejects.toThrow(CallbackError);
|
|
59
|
+
await expect(callbackPromise).rejects.toThrow('timed out');
|
|
60
|
+
server.stop();
|
|
61
|
+
}, 1000);
|
|
62
|
+
it('should return 404 for non-callback paths', async ()=>{
|
|
63
|
+
const server = new CallbackServer({
|
|
64
|
+
port: 3006,
|
|
65
|
+
timeoutMs: 200
|
|
66
|
+
});
|
|
67
|
+
const expectedState = 'test-state-123';
|
|
68
|
+
// Start server
|
|
69
|
+
const callbackPromise = server.waitForCallback(expectedState).catch(()=>{
|
|
70
|
+
// Ignore timeout error
|
|
71
|
+
});
|
|
72
|
+
// Wait a bit for server to start
|
|
73
|
+
await new Promise((resolve)=>setTimeout(resolve, 50));
|
|
74
|
+
// Try to access wrong path
|
|
75
|
+
const response = await fetch('http://localhost:3006/not-callback');
|
|
76
|
+
expect(response.status).toBe(404);
|
|
77
|
+
// Clean up
|
|
78
|
+
server.stop();
|
|
79
|
+
// Wait for promise to complete
|
|
80
|
+
await callbackPromise;
|
|
81
|
+
}, 2000);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=callback-server.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/callback-server.spec.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\n\nimport { CallbackError, CallbackServer } from './callback-server.js'\n\ndescribe('CallbackServer', () => {\n describe('waitForCallback', () => {\n it('should resolve with code and state on successful callback', async () => {\n const server = new CallbackServer({ port: 3001, timeoutMs: 5000 })\n const expectedState = 'test-state-123'\n\n // Start waiting for callback\n const callbackPromise = server.waitForCallback(expectedState)\n\n // Simulate OAuth callback\n await fetch(`http://localhost:3001/callback?code=auth-code-123&state=${expectedState}`)\n\n const result = await callbackPromise\n expect(result.code).toBe('auth-code-123')\n expect(result.state).toBe(expectedState)\n\n server.stop()\n })\n\n it('should reject with CallbackError on state mismatch', async () => {\n const server = new CallbackServer({ port: 3002, timeoutMs: 5000 })\n const expectedState = 'test-state-123'\n\n const callbackPromise = server.waitForCallback(expectedState)\n\n // Send wrong state\n const fetchPromise = fetch(\n `http://localhost:3002/callback?code=auth-code-123&state=wrong-state`,\n )\n\n await expect(callbackPromise).rejects.toThrow(CallbackError)\n await expect(callbackPromise).rejects.toThrow('State mismatch')\n\n // Wait for fetch to complete\n await fetchPromise\n\n server.stop()\n })\n\n it('should reject with CallbackError on OAuth error', async () => {\n const server = new CallbackServer({ port: 3003, timeoutMs: 5000 })\n const expectedState = 'test-state-123'\n\n const callbackPromise = server.waitForCallback(expectedState)\n\n // Send OAuth error\n const fetchPromise = fetch(\n `http://localhost:3003/callback?error=access_denied&error_description=User+denied`,\n )\n\n await expect(callbackPromise).rejects.toThrow(CallbackError)\n await expect(callbackPromise).rejects.toThrow('access_denied')\n\n // Wait for fetch to complete\n await fetchPromise\n\n server.stop()\n })\n\n it('should reject with CallbackError on timeout', async () => {\n const server = new CallbackServer({ port: 3005, timeoutMs: 100 })\n const expectedState = 'test-state-123'\n\n const callbackPromise = server.waitForCallback(expectedState)\n\n // Don't send callback - let it timeout\n await expect(callbackPromise).rejects.toThrow(CallbackError)\n await expect(callbackPromise).rejects.toThrow('timed out')\n\n server.stop()\n }, 1000)\n\n it('should return 404 for non-callback paths', async () => {\n const server = new CallbackServer({ port: 3006, timeoutMs: 200 })\n const expectedState = 'test-state-123'\n\n // Start server\n const callbackPromise = server.waitForCallback(expectedState).catch(() => {\n // Ignore timeout error\n })\n\n // Wait a bit for server to start\n await new Promise((resolve) => setTimeout(resolve, 50))\n\n // Try to access wrong path\n const response = await fetch('http://localhost:3006/not-callback')\n expect(response.status).toBe(404)\n\n // Clean up\n server.stop()\n\n // Wait for promise to complete\n await callbackPromise\n }, 2000)\n })\n})\n"],"names":["describe","expect","it","CallbackError","CallbackServer","server","port","timeoutMs","expectedState","callbackPromise","waitForCallback","fetch","result","code","toBe","state","stop","fetchPromise","rejects","toThrow","catch","Promise","resolve","setTimeout","response","status"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,aAAa,EAAEC,cAAc,QAAQ,uBAAsB;AAEpEJ,SAAS,kBAAkB;IACzBA,SAAS,mBAAmB;QAC1BE,GAAG,6DAA6D;YAC9D,MAAMG,SAAS,IAAID,eAAe;gBAAEE,MAAM;gBAAMC,WAAW;YAAK;YAChE,MAAMC,gBAAgB;YAEtB,6BAA6B;YAC7B,MAAMC,kBAAkBJ,OAAOK,eAAe,CAACF;YAE/C,0BAA0B;YAC1B,MAAMG,MAAM,CAAC,wDAAwD,EAAEH,eAAe;YAEtF,MAAMI,SAAS,MAAMH;YACrBR,OAAOW,OAAOC,IAAI,EAAEC,IAAI,CAAC;YACzBb,OAAOW,OAAOG,KAAK,EAAED,IAAI,CAACN;YAE1BH,OAAOW,IAAI;QACb;QAEAd,GAAG,sDAAsD;YACvD,MAAMG,SAAS,IAAID,eAAe;gBAAEE,MAAM;gBAAMC,WAAW;YAAK;YAChE,MAAMC,gBAAgB;YAEtB,MAAMC,kBAAkBJ,OAAOK,eAAe,CAACF;YAE/C,mBAAmB;YACnB,MAAMS,eAAeN,MACnB,CAAC,mEAAmE,CAAC;YAGvE,MAAMV,OAAOQ,iBAAiBS,OAAO,CAACC,OAAO,CAAChB;YAC9C,MAAMF,OAAOQ,iBAAiBS,OAAO,CAACC,OAAO,CAAC;YAE9C,6BAA6B;YAC7B,MAAMF;YAENZ,OAAOW,IAAI;QACb;QAEAd,GAAG,mDAAmD;YACpD,MAAMG,SAAS,IAAID,eAAe;gBAAEE,MAAM;gBAAMC,WAAW;YAAK;YAChE,MAAMC,gBAAgB;YAEtB,MAAMC,kBAAkBJ,OAAOK,eAAe,CAACF;YAE/C,mBAAmB;YACnB,MAAMS,eAAeN,MACnB,CAAC,gFAAgF,CAAC;YAGpF,MAAMV,OAAOQ,iBAAiBS,OAAO,CAACC,OAAO,CAAChB;YAC9C,MAAMF,OAAOQ,iBAAiBS,OAAO,CAACC,OAAO,CAAC;YAE9C,6BAA6B;YAC7B,MAAMF;YAENZ,OAAOW,IAAI;QACb;QAEAd,GAAG,+CAA+C;YAChD,MAAMG,SAAS,IAAID,eAAe;gBAAEE,MAAM;gBAAMC,WAAW;YAAI;YAC/D,MAAMC,gBAAgB;YAEtB,MAAMC,kBAAkBJ,OAAOK,eAAe,CAACF;YAE/C,uCAAuC;YACvC,MAAMP,OAAOQ,iBAAiBS,OAAO,CAACC,OAAO,CAAChB;YAC9C,MAAMF,OAAOQ,iBAAiBS,OAAO,CAACC,OAAO,CAAC;YAE9Cd,OAAOW,IAAI;QACb,GAAG;QAEHd,GAAG,4CAA4C;YAC7C,MAAMG,SAAS,IAAID,eAAe;gBAAEE,MAAM;gBAAMC,WAAW;YAAI;YAC/D,MAAMC,gBAAgB;YAEtB,eAAe;YACf,MAAMC,kBAAkBJ,OAAOK,eAAe,CAACF,eAAeY,KAAK,CAAC;YAClE,uBAAuB;YACzB;YAEA,iCAAiC;YACjC,MAAM,IAAIC,QAAQ,CAACC,UAAYC,WAAWD,SAAS;YAEnD,2BAA2B;YAC3B,MAAME,WAAW,MAAMb,MAAM;YAC7BV,OAAOuB,SAASC,MAAM,EAAEX,IAAI,CAAC;YAE7B,WAAW;YACXT,OAAOW,IAAI;YAEX,+BAA+B;YAC/B,MAAMP;QACR,GAAG;IACL;AACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a machine-specific encryption key for token storage
|
|
3
|
+
*
|
|
4
|
+
* Creates a deterministic encryption key using PBKDF2 with machine-specific
|
|
5
|
+
* identifiers and a random salt. This provides stronger security than simple
|
|
6
|
+
* hashing by making brute force attacks computationally expensive.
|
|
7
|
+
*
|
|
8
|
+
* Security model - What this protects against:
|
|
9
|
+
* ✅ Accidental file exposure (cloud backups, shared drives)
|
|
10
|
+
* ✅ Non-technical users reading token files
|
|
11
|
+
* ✅ Brute force attacks (100k PBKDF2 iterations)
|
|
12
|
+
* ✅ Key guessing even with known machine identifiers (random salt)
|
|
13
|
+
*
|
|
14
|
+
* Security model - What this DOES NOT protect against:
|
|
15
|
+
* ❌ Local user compromise (same user can run this function)
|
|
16
|
+
* ❌ Root/admin access attacks
|
|
17
|
+
* ❌ Sophisticated attackers with system access
|
|
18
|
+
*
|
|
19
|
+
* For stronger security, use OS keychain (@napi-rs/keyring) instead.
|
|
20
|
+
* This is a fallback for environments without keychain access.
|
|
21
|
+
*
|
|
22
|
+
* Implementation details:
|
|
23
|
+
* - Uses PBKDF2 with SHA-256 and 100,000 iterations (~500ms-1s)
|
|
24
|
+
* - Combines multiple machine identifiers for entropy
|
|
25
|
+
* - Uses persistent random salt unique to each machine
|
|
26
|
+
* - Produces consistent key for same machine (deterministic)
|
|
27
|
+
* - Tokens are not portable between machines (by design)
|
|
28
|
+
*
|
|
29
|
+
* @returns 64-character hex string (32-byte key) suitable for AES-256 encryption
|
|
30
|
+
* @throws Error if unable to derive key due to missing machine information
|
|
31
|
+
*/
|
|
32
|
+
export declare function deriveEncryptionKey(): string;
|
|
33
|
+
//# sourceMappingURL=crypto-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.d.ts","sourceRoot":"","sources":["../../src/auth/crypto-utils.ts"],"names":[],"mappings":"AAqEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAe5C"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
// Configuration store for cryptographic salt
|
|
5
|
+
const cryptoConf = new Conf({
|
|
6
|
+
configName: 'payloadcms-figma-crypto',
|
|
7
|
+
projectName: 'payloadcms-figma'
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Get or generate a random salt for key derivation
|
|
11
|
+
*
|
|
12
|
+
* Generates a cryptographically secure random salt on first run and persists it
|
|
13
|
+
* for future use. This ensures the encryption key is unique per machine and not
|
|
14
|
+
* reproducible even if machine identifiers are known.
|
|
15
|
+
*
|
|
16
|
+
* @returns 32-byte hex string (64 characters)
|
|
17
|
+
*/ function getOrCreateSalt() {
|
|
18
|
+
let salt = cryptoConf.get('salt');
|
|
19
|
+
if (!salt) {
|
|
20
|
+
// Generate cryptographically secure random salt (32 bytes = 256 bits)
|
|
21
|
+
salt = crypto.randomBytes(32).toString('hex');
|
|
22
|
+
cryptoConf.set('salt', salt);
|
|
23
|
+
}
|
|
24
|
+
return salt;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Gather machine-specific entropy sources
|
|
28
|
+
*
|
|
29
|
+
* Collects multiple machine and user identifiers to create a unique input
|
|
30
|
+
* for key derivation. More entropy sources make the key harder to guess.
|
|
31
|
+
*
|
|
32
|
+
* @returns Combined machine identifier string
|
|
33
|
+
* @throws Error if unable to gather sufficient entropy
|
|
34
|
+
*/ function gatherMachineEntropy() {
|
|
35
|
+
try {
|
|
36
|
+
const userInfo = os.userInfo();
|
|
37
|
+
const networkInterfaces = os.networkInterfaces();
|
|
38
|
+
// Get MAC address from first available network interface (if available)
|
|
39
|
+
const macAddress = Object.values(networkInterfaces).flat().find((iface)=>iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')?.mac;
|
|
40
|
+
const entropy = [
|
|
41
|
+
os.hostname(),
|
|
42
|
+
os.homedir(),
|
|
43
|
+
userInfo.username,
|
|
44
|
+
os.platform(),
|
|
45
|
+
os.arch(),
|
|
46
|
+
macAddress
|
|
47
|
+
].filter(Boolean) // Remove any undefined values
|
|
48
|
+
;
|
|
49
|
+
if (entropy.length < 4) {
|
|
50
|
+
throw new Error('Insufficient machine entropy available');
|
|
51
|
+
}
|
|
52
|
+
return entropy.join('::');
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Failed to gather machine entropy: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Derive a machine-specific encryption key for token storage
|
|
59
|
+
*
|
|
60
|
+
* Creates a deterministic encryption key using PBKDF2 with machine-specific
|
|
61
|
+
* identifiers and a random salt. This provides stronger security than simple
|
|
62
|
+
* hashing by making brute force attacks computationally expensive.
|
|
63
|
+
*
|
|
64
|
+
* Security model - What this protects against:
|
|
65
|
+
* ✅ Accidental file exposure (cloud backups, shared drives)
|
|
66
|
+
* ✅ Non-technical users reading token files
|
|
67
|
+
* ✅ Brute force attacks (100k PBKDF2 iterations)
|
|
68
|
+
* ✅ Key guessing even with known machine identifiers (random salt)
|
|
69
|
+
*
|
|
70
|
+
* Security model - What this DOES NOT protect against:
|
|
71
|
+
* ❌ Local user compromise (same user can run this function)
|
|
72
|
+
* ❌ Root/admin access attacks
|
|
73
|
+
* ❌ Sophisticated attackers with system access
|
|
74
|
+
*
|
|
75
|
+
* For stronger security, use OS keychain (@napi-rs/keyring) instead.
|
|
76
|
+
* This is a fallback for environments without keychain access.
|
|
77
|
+
*
|
|
78
|
+
* Implementation details:
|
|
79
|
+
* - Uses PBKDF2 with SHA-256 and 100,000 iterations (~500ms-1s)
|
|
80
|
+
* - Combines multiple machine identifiers for entropy
|
|
81
|
+
* - Uses persistent random salt unique to each machine
|
|
82
|
+
* - Produces consistent key for same machine (deterministic)
|
|
83
|
+
* - Tokens are not portable between machines (by design)
|
|
84
|
+
*
|
|
85
|
+
* @returns 64-character hex string (32-byte key) suitable for AES-256 encryption
|
|
86
|
+
* @throws Error if unable to derive key due to missing machine information
|
|
87
|
+
*/ export function deriveEncryptionKey() {
|
|
88
|
+
const machineId = gatherMachineEntropy();
|
|
89
|
+
const salt = getOrCreateSalt();
|
|
90
|
+
// PBKDF2 with 100k iterations provides strong security against brute force
|
|
91
|
+
// while remaining performant for CLI use (~500ms-1s key derivation time)
|
|
92
|
+
const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
|
|
93
|
+
return key.toString('hex');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=crypto-utils.js.map
|
|
@@ -0,0 +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// 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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.spec.d.ts","sourceRoot":"","sources":["../../src/auth/crypto-utils.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { deriveEncryptionKey } from './crypto-utils.js';
|
|
3
|
+
describe('crypto-utils', ()=>{
|
|
4
|
+
describe('deriveEncryptionKey', ()=>{
|
|
5
|
+
it('should return the same key on multiple calls (deterministic)', ()=>{
|
|
6
|
+
const key1 = deriveEncryptionKey();
|
|
7
|
+
const key2 = deriveEncryptionKey();
|
|
8
|
+
const key3 = deriveEncryptionKey();
|
|
9
|
+
expect(key1).toBe(key2);
|
|
10
|
+
expect(key2).toBe(key3);
|
|
11
|
+
});
|
|
12
|
+
it('should produce keys suitable for AES-256 encryption', ()=>{
|
|
13
|
+
const key = deriveEncryptionKey();
|
|
14
|
+
// AES-256 requires 256 bits = 32 bytes = 64 hex characters
|
|
15
|
+
expect(key.length).toBe(64);
|
|
16
|
+
// Should be valid hex (can be converted to Buffer)
|
|
17
|
+
expect(()=>Buffer.from(key, 'hex')).not.toThrow();
|
|
18
|
+
const buffer = Buffer.from(key, 'hex');
|
|
19
|
+
expect(buffer.length).toBe(32); // 32 bytes
|
|
20
|
+
});
|
|
21
|
+
it('should complete key derivation in reasonable time', ()=>{
|
|
22
|
+
// Test that 100k PBKDF2 iterations complete quickly enough for CLI use
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
// Run multiple times to get average
|
|
25
|
+
for(let i = 0; i < 5; i++){
|
|
26
|
+
deriveEncryptionKey();
|
|
27
|
+
}
|
|
28
|
+
const endTime = Date.now();
|
|
29
|
+
const avgTime = (endTime - startTime) / 5;
|
|
30
|
+
// Each call should take less than 2 seconds on average
|
|
31
|
+
expect(avgTime).toBeLessThan(2000);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=crypto-utils.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/crypto-utils.spec.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\n\nimport { deriveEncryptionKey } from './crypto-utils.js'\n\ndescribe('crypto-utils', () => {\n describe('deriveEncryptionKey', () => {\n it('should return the same key on multiple calls (deterministic)', () => {\n const key1 = deriveEncryptionKey()\n const key2 = deriveEncryptionKey()\n const key3 = deriveEncryptionKey()\n\n expect(key1).toBe(key2)\n expect(key2).toBe(key3)\n })\n\n it('should produce keys suitable for AES-256 encryption', () => {\n const key = deriveEncryptionKey()\n\n // AES-256 requires 256 bits = 32 bytes = 64 hex characters\n expect(key.length).toBe(64)\n\n // Should be valid hex (can be converted to Buffer)\n expect(() => Buffer.from(key, 'hex')).not.toThrow()\n\n const buffer = Buffer.from(key, 'hex')\n expect(buffer.length).toBe(32) // 32 bytes\n })\n\n it('should complete key derivation in reasonable time', () => {\n // Test that 100k PBKDF2 iterations complete quickly enough for CLI use\n const startTime = Date.now()\n\n // Run multiple times to get average\n for (let i = 0; i < 5; i++) {\n deriveEncryptionKey()\n }\n\n const endTime = Date.now()\n const avgTime = (endTime - startTime) / 5\n\n // Each call should take less than 2 seconds on average\n expect(avgTime).toBeLessThan(2000)\n })\n })\n})\n"],"names":["describe","expect","it","deriveEncryptionKey","key1","key2","key3","toBe","key","length","Buffer","from","not","toThrow","buffer","startTime","Date","now","i","endTime","avgTime","toBeLessThan"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,mBAAmB,QAAQ,oBAAmB;AAEvDH,SAAS,gBAAgB;IACvBA,SAAS,uBAAuB;QAC9BE,GAAG,gEAAgE;YACjE,MAAME,OAAOD;YACb,MAAME,OAAOF;YACb,MAAMG,OAAOH;YAEbF,OAAOG,MAAMG,IAAI,CAACF;YAClBJ,OAAOI,MAAME,IAAI,CAACD;QACpB;QAEAJ,GAAG,uDAAuD;YACxD,MAAMM,MAAML;YAEZ,2DAA2D;YAC3DF,OAAOO,IAAIC,MAAM,EAAEF,IAAI,CAAC;YAExB,mDAAmD;YACnDN,OAAO,IAAMS,OAAOC,IAAI,CAACH,KAAK,QAAQI,GAAG,CAACC,OAAO;YAEjD,MAAMC,SAASJ,OAAOC,IAAI,CAACH,KAAK;YAChCP,OAAOa,OAAOL,MAAM,EAAEF,IAAI,CAAC,KAAI,WAAW;QAC5C;QAEAL,GAAG,qDAAqD;YACtD,uEAAuE;YACvE,MAAMa,YAAYC,KAAKC,GAAG;YAE1B,oCAAoC;YACpC,IAAK,IAAIC,IAAI,GAAGA,IAAI,GAAGA,IAAK;gBAC1Bf;YACF;YAEA,MAAMgB,UAAUH,KAAKC,GAAG;YACxB,MAAMG,UAAU,AAACD,CAAAA,UAAUJ,SAAQ,IAAK;YAExC,uDAAuD;YACvDd,OAAOmB,SAASC,YAAY,CAAC;QAC/B;IACF;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { JWTPayload } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* JWT validation error with specific error codes
|
|
4
|
+
*/
|
|
5
|
+
export declare class JWTValidationError extends Error {
|
|
6
|
+
code: 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID';
|
|
7
|
+
cause?: Error | undefined;
|
|
8
|
+
constructor(message: string, code: 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID', cause?: Error | undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validate a project token JWT
|
|
12
|
+
*
|
|
13
|
+
* This function:
|
|
14
|
+
* 1. Gets the JWKS resolver for the environment (cached)
|
|
15
|
+
* 2. Uses jose's jwtVerify to validate signature and claims in one call
|
|
16
|
+
* 3. Maps jose errors to our custom error types
|
|
17
|
+
*
|
|
18
|
+
* Jose automatically:
|
|
19
|
+
* - Fetches JWKS from the URI (with caching)
|
|
20
|
+
* - Matches the key by kid from the JWT header
|
|
21
|
+
* - Verifies the signature using the correct algorithm
|
|
22
|
+
* - Validates standard claims (exp, iat, nbf)
|
|
23
|
+
* - Handles clock skew tolerance
|
|
24
|
+
*
|
|
25
|
+
* @param token - JWT token string
|
|
26
|
+
* @param environment - Figma environment ('production' or 'staging')
|
|
27
|
+
* @returns Promise resolving to validated JWT payload
|
|
28
|
+
* @throws {JWTValidationError} If validation fails
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateProjectToken(token: string, environment: 'production' | 'staging'): Promise<JWTPayload>;
|
|
31
|
+
/**
|
|
32
|
+
* Clear JWKS cache (useful for testing or forcing refresh)
|
|
33
|
+
*
|
|
34
|
+
* This clears the cached JWKS resolvers, forcing fresh fetches
|
|
35
|
+
* on the next validation. Useful for:
|
|
36
|
+
* - Testing with different JWKS
|
|
37
|
+
* - Forcing key rotation updates
|
|
38
|
+
* - Clearing state between test runs
|
|
39
|
+
*/
|
|
40
|
+
export declare function clearJWKSCache(): void;
|
|
41
|
+
//# sourceMappingURL=jwt-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|