tacel-canva 1.0.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/README.md +637 -0
- package/canva-api.js +271 -0
- package/canva.css +2928 -0
- package/canva.js +2910 -0
- package/index.js +20 -0
- package/package.json +38 -0
- package/themes.js +705 -0
- package/utils/dom.js +98 -0
- package/utils/pkce.js +57 -0
package/canva-api.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tacel Canva Module - Backend API Handler Factory
|
|
3
|
+
* Registers IPC handlers for Canva Connect API operations
|
|
4
|
+
*
|
|
5
|
+
* Usage in app's main.js:
|
|
6
|
+
* const { initCanvaApi } = require('tacel-canva/canva-api');
|
|
7
|
+
* initCanvaApi({
|
|
8
|
+
* clientId: 'your-client-id',
|
|
9
|
+
* clientSecret: 'your-client-secret',
|
|
10
|
+
* redirectUri: 'http://localhost:3000/canva/callback',
|
|
11
|
+
* channelPrefix: 'canva'
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { ipcMain } = require('electron');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const { URL } = require('url');
|
|
18
|
+
|
|
19
|
+
const CANVA_API_BASE = 'https://api.canva.com/rest/v1';
|
|
20
|
+
const CANVA_AUTH_URL = 'https://www.canva.com/api/oauth/authorize';
|
|
21
|
+
const CANVA_TOKEN_URL = 'https://api.canva.com/rest/v1/oauth/token';
|
|
22
|
+
const CANVA_REVOKE_URL = 'https://api.canva.com/rest/v1/oauth/revoke';
|
|
23
|
+
const CANVA_INTROSPECT_URL = 'https://api.canva.com/rest/v1/oauth/introspect';
|
|
24
|
+
|
|
25
|
+
let config = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize Canva API handlers
|
|
29
|
+
* @param {Object} options - Configuration options
|
|
30
|
+
* @param {string} options.clientId - Canva client ID
|
|
31
|
+
* @param {string} options.clientSecret - Canva client secret
|
|
32
|
+
* @param {string} options.redirectUri - OAuth redirect URI
|
|
33
|
+
* @param {string} options.channelPrefix - IPC channel prefix (default: 'canva')
|
|
34
|
+
*/
|
|
35
|
+
function initCanvaApi(options) {
|
|
36
|
+
if (!options.clientId || !options.clientSecret || !options.redirectUri) {
|
|
37
|
+
throw new Error('Canva API: clientId, clientSecret, and redirectUri are required');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
config = {
|
|
41
|
+
clientId: options.clientId,
|
|
42
|
+
clientSecret: options.clientSecret,
|
|
43
|
+
redirectUri: options.redirectUri,
|
|
44
|
+
channelPrefix: options.channelPrefix || 'canva',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
registerHandlers();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function registerHandlers() {
|
|
51
|
+
const prefix = config.channelPrefix;
|
|
52
|
+
|
|
53
|
+
// Get OAuth authorization URL
|
|
54
|
+
ipcMain.handle(`${prefix}:get-auth-url`, async (event, { codeChallenge, userId }) => {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
code_challenge: codeChallenge,
|
|
57
|
+
code_challenge_method: 'S256',
|
|
58
|
+
response_type: 'code',
|
|
59
|
+
client_id: config.clientId,
|
|
60
|
+
redirect_uri: config.redirectUri,
|
|
61
|
+
scope: 'design:meta:read design:content:read folder:read',
|
|
62
|
+
state: userId, // Pass userId as state for verification
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return `${CANVA_AUTH_URL}?${params.toString()}`;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Exchange authorization code for tokens
|
|
69
|
+
ipcMain.handle(`${prefix}:exchange-token`, async (event, { code, codeVerifier, userId }) => {
|
|
70
|
+
const body = new URLSearchParams({
|
|
71
|
+
grant_type: 'authorization_code',
|
|
72
|
+
code,
|
|
73
|
+
code_verifier: codeVerifier,
|
|
74
|
+
client_id: config.clientId,
|
|
75
|
+
client_secret: config.clientSecret,
|
|
76
|
+
redirect_uri: config.redirectUri,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const tokens = await makeRequest('POST', CANVA_TOKEN_URL, {
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
82
|
+
},
|
|
83
|
+
body: body.toString(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return tokens;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Refresh access token
|
|
90
|
+
ipcMain.handle(`${prefix}:refresh-token`, async (event, { refreshToken }) => {
|
|
91
|
+
const body = new URLSearchParams({
|
|
92
|
+
grant_type: 'refresh_token',
|
|
93
|
+
refresh_token: refreshToken,
|
|
94
|
+
client_id: config.clientId,
|
|
95
|
+
client_secret: config.clientSecret,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const tokens = await makeRequest('POST', CANVA_TOKEN_URL, {
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
101
|
+
},
|
|
102
|
+
body: body.toString(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return tokens;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// List user's designs
|
|
109
|
+
ipcMain.handle(`${prefix}:list-designs`, async (event, { userId, accessToken, query = '', continuation = null }) => {
|
|
110
|
+
const params = new URLSearchParams();
|
|
111
|
+
if (query) params.append('query', query);
|
|
112
|
+
if (continuation) params.append('continuation', continuation);
|
|
113
|
+
|
|
114
|
+
const url = `${CANVA_API_BASE}/designs${params.toString() ? '?' + params.toString() : ''}`;
|
|
115
|
+
|
|
116
|
+
const result = await makeRequest('GET', url, {
|
|
117
|
+
headers: {
|
|
118
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Get design details
|
|
126
|
+
ipcMain.handle(`${prefix}:get-design`, async (event, { userId, accessToken, designId }) => {
|
|
127
|
+
const url = `${CANVA_API_BASE}/designs/${designId}`;
|
|
128
|
+
|
|
129
|
+
const result = await makeRequest('GET', url, {
|
|
130
|
+
headers: {
|
|
131
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Export design
|
|
139
|
+
ipcMain.handle(`${prefix}:export-design`, async (event, { userId, accessToken, designId, format = 'png' }) => {
|
|
140
|
+
const url = `${CANVA_API_BASE}/exports`;
|
|
141
|
+
|
|
142
|
+
const body = JSON.stringify({
|
|
143
|
+
design_id: designId,
|
|
144
|
+
format: {
|
|
145
|
+
type: format,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = await makeRequest('POST', url, {
|
|
150
|
+
headers: {
|
|
151
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
body,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Get export job status
|
|
161
|
+
ipcMain.handle(`${prefix}:get-export`, async (event, { userId, accessToken, exportId }) => {
|
|
162
|
+
const url = `${CANVA_API_BASE}/exports/${exportId}`;
|
|
163
|
+
|
|
164
|
+
const result = await makeRequest('GET', url, {
|
|
165
|
+
headers: {
|
|
166
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// List folders
|
|
174
|
+
ipcMain.handle(`${prefix}:list-folders`, async (event, { userId, accessToken, continuation = null }) => {
|
|
175
|
+
const params = new URLSearchParams();
|
|
176
|
+
if (continuation) params.append('continuation', continuation);
|
|
177
|
+
|
|
178
|
+
const url = `${CANVA_API_BASE}/folders${params.toString() ? '?' + params.toString() : ''}`;
|
|
179
|
+
|
|
180
|
+
const result = await makeRequest('GET', url, {
|
|
181
|
+
headers: {
|
|
182
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Revoke token
|
|
190
|
+
ipcMain.handle(`${prefix}:revoke-token`, async (event, { accessToken }) => {
|
|
191
|
+
const body = new URLSearchParams({
|
|
192
|
+
token: accessToken,
|
|
193
|
+
client_id: config.clientId,
|
|
194
|
+
client_secret: config.clientSecret,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await makeRequest('POST', CANVA_REVOKE_URL, {
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
200
|
+
},
|
|
201
|
+
body: body.toString(),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return { success: true };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Make HTTPS request to Canva API
|
|
210
|
+
* @param {string} method - HTTP method
|
|
211
|
+
* @param {string} url - Full URL
|
|
212
|
+
* @param {Object} options - Request options
|
|
213
|
+
* @returns {Promise<Object>} Response data
|
|
214
|
+
*/
|
|
215
|
+
function makeRequest(method, url, options = {}) {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
const urlObj = new URL(url);
|
|
218
|
+
|
|
219
|
+
// Merge default headers with provided headers
|
|
220
|
+
const defaultHeaders = {
|
|
221
|
+
'User-Agent': 'TacelCanva/1.0.0',
|
|
222
|
+
'Accept': 'application/json',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const requestOptions = {
|
|
226
|
+
hostname: urlObj.hostname,
|
|
227
|
+
path: urlObj.pathname + urlObj.search,
|
|
228
|
+
method,
|
|
229
|
+
headers: { ...defaultHeaders, ...options.headers },
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const req = https.request(requestOptions, (res) => {
|
|
233
|
+
let data = '';
|
|
234
|
+
|
|
235
|
+
res.on('data', (chunk) => {
|
|
236
|
+
data += chunk;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
res.on('end', () => {
|
|
240
|
+
try {
|
|
241
|
+
// Log raw response for debugging
|
|
242
|
+
console.log(`[Canva API] Status: ${res.statusCode}, Response:`, data.substring(0, 500));
|
|
243
|
+
|
|
244
|
+
const parsed = data ? JSON.parse(data) : {};
|
|
245
|
+
|
|
246
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
247
|
+
resolve(parsed);
|
|
248
|
+
} else {
|
|
249
|
+
reject(new Error(`Canva API error ${res.statusCode}: ${parsed.error || parsed.error_description || data}`));
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
reject(new Error(`Failed to parse response (Status ${res.statusCode}): ${err.message}. Raw response: ${data.substring(0, 200)}`));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
req.on('error', (err) => {
|
|
258
|
+
reject(new Error(`Request failed: ${err.message}`));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (options.body) {
|
|
262
|
+
req.write(options.body);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
req.end();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
initCanvaApi,
|
|
271
|
+
};
|