@canva/cli 0.0.1-beta.1 → 0.0.1-beta.3

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.
Files changed (43) hide show
  1. package/cli.js +134 -134
  2. package/package.json +1 -1
  3. package/templates/base/backend/routers/oauth.ts +393 -0
  4. package/templates/base/eslint.config.mjs +5 -275
  5. package/templates/base/package.json +1 -1
  6. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
  7. package/templates/base/utils/backend/bearer_middleware/index.ts +1 -0
  8. package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
  9. package/templates/base/utils/use_add_element.ts +48 -0
  10. package/templates/base/utils/use_feature_support.ts +28 -0
  11. package/templates/common/README.md +0 -67
  12. package/templates/common/conf/eslint-general.mjs +277 -0
  13. package/templates/common/conf/eslint-i18n.mjs +23 -0
  14. package/templates/dam/backend/server.ts +0 -7
  15. package/templates/dam/eslint.config.mjs +6 -275
  16. package/templates/dam/package.json +8 -7
  17. package/templates/dam/src/app.tsx +2 -135
  18. package/templates/gen_ai/README.md +40 -1
  19. package/templates/gen_ai/backend/routers/oauth.ts +393 -0
  20. package/templates/gen_ai/backend/server.ts +1 -1
  21. package/templates/gen_ai/eslint.config.mjs +5 -275
  22. package/templates/gen_ai/package.json +7 -6
  23. package/templates/gen_ai/src/api/api.ts +44 -27
  24. package/templates/gen_ai/src/components/footer.tsx +9 -5
  25. package/templates/gen_ai/src/components/image_grid.tsx +9 -7
  26. package/templates/gen_ai/src/components/loading_results.tsx +8 -4
  27. package/templates/gen_ai/src/components/prompt_input.tsx +2 -0
  28. package/templates/gen_ai/src/context/app_context.tsx +8 -2
  29. package/templates/gen_ai/src/services/auth.tsx +5 -10
  30. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
  31. package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +1 -0
  32. package/templates/gen_ai/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
  33. package/templates/hello_world/eslint.config.mjs +5 -275
  34. package/templates/hello_world/package.json +7 -5
  35. package/templates/hello_world/src/app.tsx +5 -3
  36. package/templates/hello_world/utils/use_add_element.ts +48 -0
  37. package/templates/hello_world/utils/use_feature_support.ts +28 -0
  38. package/templates/dam/backend/database/database.ts +0 -42
  39. package/templates/dam/backend/routers/auth.ts +0 -285
  40. package/templates/gen_ai/backend/routers/auth.ts +0 -285
  41. package/templates/gen_ai/utils/backend/jwt_middleware/index.ts +0 -1
  42. package/templates/gen_ai/utils/backend/jwt_middleware/jwt_middleware.ts +0 -229
  43. package/templates/gen_ai/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +0 -630
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canva/cli",
3
- "version": "0.0.1-beta.1",
3
+ "version": "0.0.1-beta.3",
4
4
  "description": "The official Canva CLI.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "Canva Pty Ltd.",
@@ -0,0 +1,393 @@
1
+ import * as crypto from "crypto";
2
+ import "dotenv/config";
3
+ import * as express from "express";
4
+ import * as basicAuth from "express-basic-auth";
5
+ import { JSONFileDatabase } from "../database/database";
6
+ import { createBearerMiddleware } from "../../utils/backend/bearer_middleware";
7
+ import * as debug from "debug";
8
+
9
+ /**
10
+ * Prefix your start command with `DEBUG=express:route:oauth` to enable debug logging
11
+ * for this middleware
12
+ */
13
+ const debugLogger = debug("express:route:oauth");
14
+
15
+ /**
16
+ * These are the hard-coded credentials for logging in to this template.
17
+ */
18
+ const USERNAME = "username";
19
+ const PASSWORD = "password";
20
+
21
+ const COOKIE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
22
+ const TOKEN_EXPIRY_S = 4 * 60 * 60; // 4 hours
23
+ const REFRESH_EXPIRY_S = 30 * 24 * 3600; // 30 days
24
+
25
+ const CLIENT_ID = "client_id";
26
+ const CLIENT_SECRET = "client_secret";
27
+ const REDIRECT_URI = "https://www.canva.com/apps/oauth/authorized";
28
+
29
+ /**
30
+ * For simplicity, we simply generate random token values and store them in our database
31
+ * for production you may want to use JWTs or other stateless methods.
32
+ */
33
+ interface Token {
34
+ value: string;
35
+ expiry: number; // Unix timestamp
36
+ }
37
+
38
+ export interface User {
39
+ id: string;
40
+ authorizationCode?: Token;
41
+ accessToken?: Token;
42
+ refreshToken?: Token;
43
+ }
44
+
45
+ interface RedirectUriRecord {
46
+ redirectUri: string;
47
+ authorizationCode: string;
48
+ }
49
+
50
+ export interface Data {
51
+ users: User[];
52
+ redirect_uris: RedirectUriRecord[];
53
+ }
54
+
55
+ /**
56
+ * For more information on Authentication, refer to our documentation: {@link https://www.canva.dev/docs/apps/authenticating-users/#types-of-authentication}.
57
+ */
58
+ export const createAuthRouter = () => {
59
+ /**
60
+ * Set up a database with a "users" table. In this example code, the
61
+ * database is simply a JSON file.
62
+ */
63
+ const db = new JSONFileDatabase<Data>({ users: [], redirect_uris: [] });
64
+
65
+ const router = express.Router();
66
+
67
+ /**
68
+ * The urlencoded middleware allows us to parse form data from the request body.
69
+ * This is required for the OAuth 2.0 Token Exchange endpoint.
70
+ */
71
+ router.use(express.urlencoded({ extended: true }));
72
+
73
+ /**
74
+ * This is the OAuth 2.0 Authorization endpoint
75
+ * https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
76
+ *
77
+ * This endpoint does not implement PKCE, which you would want to do
78
+ * for production.
79
+ */
80
+ router.get(
81
+ "/auth/authorize",
82
+ basicAuth({
83
+ users: { [USERNAME]: PASSWORD },
84
+ challenge: true,
85
+ }),
86
+ async (req, res) => {
87
+ const redirectUri = decodeURIComponent(
88
+ req.query.redirect_uri?.toString() ?? REDIRECT_URI,
89
+ );
90
+ const clientId = req.query.client_id?.toString();
91
+ const responseType = req.query.response_type?.toString();
92
+ const state = req.query.state?.toString();
93
+
94
+ if (!clientId) {
95
+ return res.status(400).send("Missing required client ID parameter");
96
+ }
97
+
98
+ if (clientId !== CLIENT_ID) {
99
+ return res.status(400).send("Invalid client ID");
100
+ }
101
+
102
+ if (redirectUri !== REDIRECT_URI) {
103
+ return res.status(400).send(`Invalid redirect URI ${redirectUri}`);
104
+ }
105
+
106
+ const failureResponse = (error: string) => {
107
+ const params = new URLSearchParams({
108
+ error,
109
+ state: state || "",
110
+ });
111
+ res.redirect(302, `${redirectUri}?${params}`);
112
+ };
113
+
114
+ if (responseType !== "code") {
115
+ return failureResponse("unsupported_response_type");
116
+ }
117
+
118
+ // You should implement PKCE to protect against authorization code interception attacks
119
+ // https://datatracker.ietf.org/doc/html/rfc7636
120
+ // this is left off for simplicity in this example
121
+
122
+ // Generate a unique authorization code
123
+ const authorizationCode = crypto.randomUUID();
124
+
125
+ // Load the database
126
+ const data = await db.read();
127
+
128
+ // Add the user to the database if they aren't there already
129
+ if (!data.users.find((user) => user.id === USERNAME)) {
130
+ data.users.push({
131
+ id: USERNAME,
132
+ authorizationCode: {
133
+ value: authorizationCode,
134
+ expiry: Date.now() + COOKIE_EXPIRY_MS,
135
+ },
136
+ });
137
+ } else {
138
+ // If the user is there, update the authorization code
139
+ // In a production system you would allow at least one authorization code
140
+ // per user per client, but here we simplify and have one per user.
141
+ data.users = data.users.map((user) => {
142
+ if (user.id === USERNAME) {
143
+ return {
144
+ ...user,
145
+ authorizationCode: {
146
+ value: authorizationCode,
147
+ expiry: Date.now() + COOKIE_EXPIRY_MS,
148
+ },
149
+ };
150
+ }
151
+ return user;
152
+ });
153
+ }
154
+ if (
155
+ !data.redirect_uris.find((record) => record.redirectUri === redirectUri)
156
+ ) {
157
+ data.redirect_uris.push({ redirectUri, authorizationCode });
158
+ } else {
159
+ data.redirect_uris = data.redirect_uris.map((record) => {
160
+ if (record.redirectUri === redirectUri) {
161
+ return { ...record, authorizationCode };
162
+ }
163
+ return record;
164
+ });
165
+ }
166
+ await db.write(data);
167
+
168
+ res.redirect(
169
+ 302,
170
+ `${redirectUri}?code=${authorizationCode}&state=${state}`,
171
+ );
172
+ },
173
+ );
174
+
175
+ interface AuthorizationExchangeRequest {
176
+ grant_type: "authorization_code";
177
+ code: string;
178
+ redirect_uri?: string;
179
+ }
180
+
181
+ interface RefreshTokenExchangeRequest {
182
+ grant_type: "refresh_token";
183
+ refresh_token: string;
184
+ }
185
+
186
+ const failureResponse = (res: express.Response, error: string) => {
187
+ return res.status(400).send(`{"error": "${error}"}`);
188
+ };
189
+
190
+ const authorizationCodeExchange = async (
191
+ res: express.Response,
192
+ body: AuthorizationExchangeRequest,
193
+ ) => {
194
+ const refresh_token = crypto.randomUUID();
195
+ const access_token = crypto.randomUUID();
196
+
197
+ const code = body.code?.toString();
198
+
199
+ if (!code) {
200
+ return failureResponse(res, "invalid_request");
201
+ }
202
+
203
+ // Load the database
204
+ const data = await db.read();
205
+ const lastRedirectUrl = data.redirect_uris.find(
206
+ (record) => record.authorizationCode === code,
207
+ )?.redirectUri;
208
+
209
+ // The specification requires that if the redirect uri was present in the authorization request,
210
+ // it must be included and the same in the token exchange request
211
+ if (lastRedirectUrl !== body.redirect_uri?.toString()) {
212
+ return failureResponse(res, "invalid_request");
213
+ }
214
+
215
+ // Find the user who was issued this authorization code
216
+ const user = data.users.find(
217
+ (user) => user.authorizationCode?.value === code,
218
+ );
219
+
220
+ if (!user || !user.authorizationCode) {
221
+ return failureResponse(res, "invalid_grant");
222
+ }
223
+
224
+ if (user.authorizationCode.expiry < Date.now()) {
225
+ return failureResponse(res, "invalid_grant");
226
+ }
227
+
228
+ // Update the user with the new access token and refresh token
229
+ // and clear out their authorization code. Authorization codes
230
+ // are single use, so attempting to use the old one will now fail
231
+ data.users = data.users.map((user) => {
232
+ if (user.id === USERNAME) {
233
+ return {
234
+ ...user,
235
+ authorizationCode: undefined,
236
+ accessToken: {
237
+ value: access_token,
238
+ expiry: Date.now() + TOKEN_EXPIRY_S * 1000,
239
+ },
240
+ refreshToken: {
241
+ value: refresh_token,
242
+ expiry: Date.now() + REFRESH_EXPIRY_S * 1000,
243
+ },
244
+ };
245
+ }
246
+ return user;
247
+ });
248
+ await db.write(data);
249
+
250
+ return res.status(200).send(
251
+ JSON.stringify({
252
+ access_token,
253
+ refresh_token,
254
+ token_type: "bearer",
255
+ expires_in: TOKEN_EXPIRY_S,
256
+ }),
257
+ );
258
+ };
259
+
260
+ const refreshTokenExchange = async (
261
+ res: express.Response,
262
+ body: RefreshTokenExchangeRequest,
263
+ ) => {
264
+ const refresh_token = crypto.randomUUID();
265
+ const access_token = crypto.randomUUID();
266
+
267
+ const old_refresh_token = body.refresh_token?.toString();
268
+ if (!old_refresh_token) {
269
+ return failureResponse(res, "invalid_request");
270
+ }
271
+
272
+ // Find the user who was issued this refresh token
273
+ const data = await db.read();
274
+ const user = data.users.find(
275
+ (user) => user.refreshToken?.value === old_refresh_token,
276
+ );
277
+
278
+ if (!user) {
279
+ return failureResponse(res, "invalid_grant");
280
+ }
281
+
282
+ // Update the user with the new refresh token
283
+ // refresh tokens are single use, so attempting
284
+ // to use the old one will now fail
285
+ data.users = data.users.map((u) => {
286
+ if (u.id === user.id) {
287
+ return {
288
+ ...u,
289
+ accessToken: {
290
+ value: access_token,
291
+ expiry: Date.now() + TOKEN_EXPIRY_S * 1000,
292
+ },
293
+ refreshToken: {
294
+ value: refresh_token,
295
+ expiry: Date.now() + REFRESH_EXPIRY_S * 1000,
296
+ },
297
+ };
298
+ }
299
+ return u;
300
+ });
301
+ await db.write(data);
302
+
303
+ return res.status(200).send(
304
+ JSON.stringify({
305
+ access_token,
306
+ refresh_token,
307
+ token_type: "bearer",
308
+ expires_in: TOKEN_EXPIRY_S,
309
+ }),
310
+ );
311
+ };
312
+
313
+ /**
314
+ * This endpoint is the Oauth 2.0 Token endpoint
315
+ * https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
316
+ * It serves both exchanging authorization codes for access tokens (and refresh tokens), and
317
+ * exchanging refresh tokens for new access tokens (and refresh tokens)
318
+ */
319
+ router.post(
320
+ "/auth/token",
321
+ basicAuth({
322
+ users: { [CLIENT_ID]: CLIENT_SECRET },
323
+ challenge: false,
324
+ }),
325
+ async (req, res) => {
326
+ const body = (await req.body) as
327
+ | AuthorizationExchangeRequest
328
+ | RefreshTokenExchangeRequest;
329
+
330
+ const grantType = body.grant_type?.toString();
331
+
332
+ if (!grantType) {
333
+ return failureResponse(res, "invalid_request");
334
+ }
335
+
336
+ if (grantType !== "authorization_code" && grantType !== "refresh_token") {
337
+ return failureResponse(res, "unsupported_grant_type");
338
+ }
339
+
340
+ if (grantType === "refresh_token") {
341
+ refreshTokenExchange(res, body as RefreshTokenExchangeRequest);
342
+ }
343
+
344
+ return authorizationCodeExchange(
345
+ res,
346
+ body as AuthorizationExchangeRequest,
347
+ );
348
+ },
349
+ );
350
+
351
+ /**
352
+ * TODO: Add this middleware to all routes that will receive requests from
353
+ * your app.
354
+ */
355
+ const bearerMiddleware = createBearerMiddleware(
356
+ async (access_token: string): Promise<string | undefined> => {
357
+ const data = await db.read();
358
+ const user = data.users.find(
359
+ (user) => user.accessToken?.value === access_token,
360
+ );
361
+ debugLogger(
362
+ `User found: ${user?.id}, expires ${user?.accessToken?.expiry} compare ${Date.now()}`,
363
+ );
364
+
365
+ const isAuthenticated =
366
+ user && user.accessToken && user.accessToken?.expiry > Date.now();
367
+ if (!isAuthenticated) {
368
+ return;
369
+ }
370
+ return user.id;
371
+ },
372
+ );
373
+
374
+ /**
375
+ * All routes that start with /api will be protected by bearer authentication
376
+ */
377
+ router.use("/api", bearerMiddleware);
378
+
379
+ /**
380
+ * This endpoint checks if a user is authenticated.
381
+ */
382
+ router.post("/api/authentication/status", async (req, res) => {
383
+ // Check if the user is authenticated
384
+ const isAuthenticated = !!req.user_id;
385
+
386
+ // Return the authentication status
387
+ res.send({
388
+ isAuthenticated,
389
+ });
390
+ });
391
+
392
+ return router;
393
+ };
@@ -1,11 +1,9 @@
1
- import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
- import jest from "eslint-plugin-jest";
3
- import react from "eslint-plugin-react";
4
- import globals from "globals";
5
1
  import path from "node:path";
6
2
  import { fileURLToPath } from "node:url";
7
3
  import js from "@eslint/js";
8
4
  import { FlatCompat } from "@eslint/eslintrc";
5
+ import general from "./conf/eslint-general.mjs";
6
+ import i18n from "./conf/eslint-i18n.mjs";
9
7
 
10
8
  const __filename = fileURLToPath(import.meta.url);
11
9
  const __dirname = path.dirname(__filename);
@@ -34,276 +32,8 @@ export default [
34
32
  "plugin:@typescript-eslint/strict",
35
33
  "plugin:@typescript-eslint/stylistic",
36
34
  "plugin:react/recommended",
37
- "plugin:jest/recommended"
35
+ "plugin:jest/recommended",
38
36
  ),
39
- {
40
- plugins: {
41
- "@typescript-eslint": typescriptEslint,
42
- jest,
43
- react,
44
- },
45
- languageOptions: {
46
- globals: {
47
- ...globals.serviceworker,
48
- ...globals.browser,
49
- },
50
- },
51
- settings: {
52
- react: {
53
- version: "detect",
54
- },
55
- },
56
- rules: {
57
- "@typescript-eslint/no-non-null-assertion": "warn",
58
- "@typescript-eslint/no-empty-function": "off",
59
- "@typescript-eslint/consistent-type-imports": "error",
60
- "@typescript-eslint/no-explicit-any": "warn",
61
- "@typescript-eslint/no-empty-interface": "warn",
62
- "@typescript-eslint/consistent-type-definitions": "off",
63
- "@typescript-eslint/explicit-member-accessibility": [
64
- "error",
65
- {
66
- accessibility: "no-public",
67
- overrides: {
68
- parameterProperties: "off",
69
- },
70
- },
71
- ],
72
- "@typescript-eslint/naming-convention": [
73
- "error",
74
- {
75
- selector: "typeLike",
76
- format: ["PascalCase"],
77
- leadingUnderscore: "allow",
78
- },
79
- ],
80
- "no-invalid-this": "off",
81
- "@typescript-eslint/no-invalid-this": "error",
82
- "@typescript-eslint/no-unused-expressions": [
83
- "error",
84
- {
85
- allowShortCircuit: true,
86
- allowTernary: true,
87
- },
88
- ],
89
- "no-unused-vars": "off",
90
- "@typescript-eslint/no-unused-vars": [
91
- "error",
92
- {
93
- vars: "all",
94
- varsIgnorePattern: "^_",
95
- args: "none",
96
- ignoreRestSiblings: true,
97
- },
98
- ],
99
- "@typescript-eslint/no-require-imports": "error",
100
- "jest/no-restricted-matchers": [
101
- "error",
102
- {
103
- toContainElement:
104
- "toContainElement is not recommended as it encourages testing the internals of the components",
105
- toContainHTML:
106
- "toContainHTML is not recommended as it encourages testing the internals of the components",
107
- toHaveAttribute:
108
- "toHaveAttribute is not recommended as it encourages testing the internals of the components",
109
- toHaveClass:
110
- "toHaveClass is not recommended as it encourages testing the internals of the components",
111
- toHaveStyle:
112
- "toHaveStyle is not recommended as it encourages testing the internals of the components",
113
- },
114
- ],
115
- "react/jsx-curly-brace-presence": [
116
- "error",
117
- {
118
- props: "never",
119
- children: "never",
120
- },
121
- ],
122
- "react/jsx-tag-spacing": [
123
- "error",
124
- {
125
- closingSlash: "never",
126
- beforeSelfClosing: "allow",
127
- afterOpening: "never",
128
- beforeClosing: "allow",
129
- },
130
- ],
131
- "react/self-closing-comp": "error",
132
- "react/no-unescaped-entities": "off",
133
- "react/jsx-uses-react": "off",
134
- "react/react-in-jsx-scope": "off",
135
- "default-case": "error",
136
- eqeqeq: [
137
- "error",
138
- "always",
139
- {
140
- null: "never",
141
- },
142
- ],
143
- "no-caller": "error",
144
- "no-console": "error",
145
- "no-eval": "error",
146
- "no-inner-declarations": "error",
147
- "no-new-wrappers": "error",
148
- "no-restricted-globals": [
149
- "error",
150
- {
151
- name: "fit",
152
- message: "Don't focus tests",
153
- },
154
- {
155
- name: "fdescribe",
156
- message: "Don't focus tests",
157
- },
158
- {
159
- name: "length",
160
- message:
161
- "Undefined length - Did you mean to use window.length instead?",
162
- },
163
- {
164
- name: "name",
165
- message: "Undefined name - Did you mean to use window.name instead?",
166
- },
167
- {
168
- name: "status",
169
- message:
170
- "Undefined status - Did you mean to use window.status instead?",
171
- },
172
- {
173
- name: "spyOn",
174
- message: "Don't use spyOn directly, use jest.spyOn",
175
- },
176
- ],
177
- "no-restricted-properties": [
178
- "error",
179
- {
180
- property: "bind",
181
- message: "Don't o.f.bind(o, ...), use () => o.f(...)",
182
- },
183
- {
184
- object: "ReactDOM",
185
- property: "findDOMNode",
186
- message: "Don't use ReactDOM.findDOMNode() as it is deprecated",
187
- },
188
- ],
189
- "no-restricted-syntax": [
190
- "error",
191
- {
192
- selector: "AccessorProperty, TSAbstractAccessorProperty",
193
- message:
194
- "Accessor property syntax is not allowed, use getter and setters.",
195
- },
196
- {
197
- selector: "PrivateIdentifier",
198
- message:
199
- "Private identifiers are not allowed, use TypeScript private fields.",
200
- },
201
- {
202
- selector:
203
- "JSXOpeningElement[name.name = /^[A-Z]/] > JSXAttribute[name.name = /-/]",
204
- message:
205
- "Passing hyphenated props to custom components is not type-safe. Prefer a camelCased equivalent if available. (See https://github.com/microsoft/TypeScript/issues/55182)",
206
- },
207
- {
208
- selector:
209
- "CallExpression[callee.object.name='window'][callee.property.name='open']",
210
- message:
211
- "Apps are currently not allowed to open popups, or new tabs via browser APIs. Please use `requestOpenExternalUrl` from `@canva/platform` to link to external URLs. To learn more, see https://www.canva.dev/docs/apps/api/platform-request-open-external-url/",
212
- },
213
- ],
214
- "no-return-await": "error",
215
- "no-throw-literal": "error",
216
- "no-undef-init": "error",
217
- "no-var": "error",
218
- "object-shorthand": "error",
219
- "prefer-const": [
220
- "error",
221
- {
222
- destructuring: "all",
223
- },
224
- ],
225
- "prefer-object-spread": "error",
226
- "prefer-rest-params": "error",
227
- "prefer-spread": "error",
228
- radix: "error",
229
- },
230
- },
231
- {
232
- files: ["**/*.tsx"],
233
- rules: {
234
- "react/no-deprecated": "error",
235
- "react/forbid-elements": [
236
- "error",
237
- {
238
- forbid: [
239
- {
240
- element: "video",
241
- message:
242
- "Don't use HTML video directly. Instead, use the App UI Kit <VideoCard /> as this respects users' auto-playing preferences",
243
- },
244
- {
245
- element: "em",
246
- message:
247
- "Don't use <em> to italicize text. Canva's UI fonts don't support italic font style.",
248
- },
249
- {
250
- element: "i",
251
- message:
252
- "Don't use <i> to italicize text. Canva's UI fonts don't support italic font style.",
253
- },
254
- {
255
- element: "iframe",
256
- message:
257
- "Canva Apps aren't allowed to contain iframes. You should either recreate the UI you want to show in the iframe in the app directly, or link to your page via a `<Link>` tag. For more info see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
258
- },
259
- {
260
- element: "script",
261
- message:
262
- "Script tags are not allowed in Canva SDK Apps. You should import JavaScript modules instead. For more info see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
263
- },
264
- {
265
- element: "a",
266
- message:
267
- "Don't use <a> tags. Instead, use the <Link> component from the App UI Kit, and remember to open the url via the requestOpenExternalUrl method from @canva/platform.",
268
- },
269
- {
270
- element: "img",
271
- message:
272
- "Have you considered using <ImageCard /> from the App UI Kit instead?",
273
- },
274
- {
275
- element: "embed",
276
- message:
277
- "Have you considered using <EmbedCard /> from the App UI Kit instead?",
278
- },
279
- {
280
- element: "audio",
281
- message:
282
- "Have you considered using <AudioCard /> from the App UI Kit instead?",
283
- },
284
- {
285
- element: "button",
286
- message:
287
- "Rather than using the native HTML <button> element, use the <Button> component from the App UI Kit for consistency and accessibility.",
288
- },
289
- {
290
- element: "input",
291
- message:
292
- "Wherever possible, prefer using the form inputs from the App UI Kit for consistency and accessibility (TextInput, Checkbox, FileInput, etc).",
293
- },
294
- {
295
- element: "base",
296
- message:
297
- "The <base> tag is not supported in Canva Apps. We recommend using hash-based routing. For more on what is and isn't allowed in Canva Apps see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
298
- },
299
- {
300
- element: "link",
301
- message:
302
- "If you're trying to include a css stylesheet, we recommend importing css using React, or using embedded stylesheets. For more on what is and isn't allowed in Canva Apps see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
303
- },
304
- ],
305
- },
306
- ],
307
- },
308
- },
37
+ ...general,
38
+ ...i18n,
309
39
  ];
@@ -6,7 +6,7 @@
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
7
  "author": "Canva Pty Ltd.",
8
8
  "dependencies": {
9
- "@canva/app-ui-kit": "^3.8.0",
9
+ "@canva/app-ui-kit": "^4.0.0",
10
10
  "@canva/asset": "^1.7.1",
11
11
  "@canva/design": "^1.10.0",
12
12
  "@canva/error": "^1.1.0",