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/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
+ };