@canva/cli 0.0.1-beta.6 → 0.0.1-beta.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canva/cli",
3
- "version": "0.0.1-beta.6",
3
+ "version": "0.0.1-beta.8",
4
4
  "description": "The official Canva CLI.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "Canva Pty Ltd.",
@@ -6,9 +6,9 @@
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
7
  "author": "Canva Pty Ltd.",
8
8
  "dependencies": {
9
- "@canva/app-ui-kit": "^4.2.0",
9
+ "@canva/app-ui-kit": "^4.3.0",
10
10
  "@canva/asset": "^2.0.0",
11
- "@canva/design": "^2.1.0",
11
+ "@canva/design": "^2.2.1",
12
12
  "@canva/error": "^2.0.0",
13
13
  "@canva/platform": "^2.0.0",
14
14
  "@canva/user": "^2.0.0",
@@ -22,14 +22,14 @@
22
22
  "@types/debug": "4.1.12",
23
23
  "@types/express": "4.17.21",
24
24
  "@types/express-serve-static-core": "4.19.6",
25
- "@types/jest": "29.5.13",
25
+ "@types/jest": "29.5.14",
26
26
  "@types/jsonwebtoken": "9.0.7",
27
27
  "@types/node": "20.10.0",
28
28
  "@types/node-fetch": "2.6.11",
29
29
  "@types/node-forge": "1.3.11",
30
30
  "@types/nodemon": "1.19.6",
31
31
  "@types/prompts": "2.4.9",
32
- "@types/react": "18.3.11",
32
+ "@types/react": "18.3.12",
33
33
  "@types/react-dom": "18.3.1",
34
34
  "@types/webpack-env": "1.18.5",
35
35
  "chalk": "4.1.2",
@@ -45,7 +45,7 @@
45
45
  "jest": "29.7.0",
46
46
  "jsonwebtoken": "9.0.2",
47
47
  "jwks-rsa": "3.1.0",
48
- "mini-css-extract-plugin": "2.9.1",
48
+ "mini-css-extract-plugin": "2.9.2",
49
49
  "node-fetch": "3.3.2",
50
50
  "node-forge": "1.3.1",
51
51
  "nodemon": "3.0.1",
@@ -59,7 +59,7 @@
59
59
  "ts-node": "10.9.2",
60
60
  "typescript": "5.5.4",
61
61
  "url-loader": "4.1.1",
62
- "webpack": "5.95.0",
62
+ "webpack": "5.96.1",
63
63
  "webpack-cli": "5.1.4",
64
64
  "webpack-dev-server": "5.1.0",
65
65
  "yargs": "17.7.2"
@@ -0,0 +1,104 @@
1
+ /* eslint-disable no-console */
2
+ import * as express from "express";
3
+ import * as http from "http";
4
+ import * as https from "https";
5
+ import * as fs from "fs";
6
+ import type { Request, Response, NextFunction } from "express";
7
+ import debug from "debug";
8
+
9
+ const serverDebug = debug("server");
10
+
11
+ interface BaseServer {
12
+ app: express.Express;
13
+
14
+ /**
15
+ * Starts the server on the address or port provided
16
+ * @param address port number or string address or if left undefined express defaults to port 3000
17
+ */
18
+ start: (address: number | string | undefined) => void;
19
+ }
20
+
21
+ /**
22
+ * createBaseServer instantiates a customised express server with:
23
+ * - json body handling
24
+ * - health check endpoint
25
+ * - catchall endpoint
26
+ * - error handler catch route
27
+ * - process termination handling
28
+ * - debug logging - prefix starting your server with `DEBUG=server npm run XXX`
29
+ *
30
+ * @returns BaseServer object containing the express app and a start function
31
+ */
32
+ export function createBaseServer(router: express.Router): BaseServer {
33
+ const SHOULD_ENABLE_HTTPS = process.env?.SHOULD_ENABLE_HTTPS === "true";
34
+ const HTTPS_CERT_FILE = process.env?.HTTPS_CERT_FILE;
35
+ const HTTPS_KEY_FILE = process.env?.HTTPS_KEY_FILE;
36
+
37
+ const app = express();
38
+ app.use(express.json());
39
+
40
+ // It can help to provide an extra layer of obsecurity to reduce server fingerprinting.
41
+ app.disable("x-powered-by");
42
+
43
+ // Health check endpoint
44
+ app.get("/healthz", (req, res: Response) => {
45
+ res.sendStatus(200);
46
+ });
47
+
48
+ // logging middleware
49
+ app.use((req: Request, res: Response, next: NextFunction) => {
50
+ serverDebug(`${new Date().toISOString()}: ${req.method} ${req.url}`);
51
+ next();
52
+ });
53
+
54
+ // Custom routes router
55
+ app.use(router);
56
+
57
+ // catch all router
58
+ app.all("*", (req, res) => {
59
+ res.status(404).send({
60
+ error: `unhandled '${req.method}' on '${req.url}'`,
61
+ });
62
+ });
63
+
64
+ // default error handler
65
+ app.use((err, req, res, next) => {
66
+ console.error(err.stack);
67
+ res.status(500).send({
68
+ error: "something went wrong",
69
+ });
70
+ });
71
+
72
+ let server;
73
+ if (SHOULD_ENABLE_HTTPS) {
74
+ if (!HTTPS_CERT_FILE || !HTTPS_KEY_FILE) {
75
+ throw new Error(
76
+ "Looks like you're running the example with --use-https flag, but SSL certificates haven't been generated. Please remove the .ssl/ folder and re-run the command again.",
77
+ );
78
+ }
79
+
80
+ server = https.createServer(
81
+ {
82
+ key: fs.readFileSync(HTTPS_KEY_FILE),
83
+ cert: fs.readFileSync(HTTPS_CERT_FILE),
84
+ },
85
+ app,
86
+ );
87
+ } else {
88
+ server = http.createServer(app);
89
+ }
90
+
91
+ return {
92
+ app,
93
+ start: (address: number | string | undefined) => {
94
+ console.log(`Listening on '${address}'`);
95
+ server.listen(address);
96
+ process.on("SIGTERM", () => {
97
+ serverDebug("SIGTERM signal received: closing HTTP server");
98
+ server.close(() => {
99
+ serverDebug("HTTP server closed");
100
+ });
101
+ });
102
+ },
103
+ };
104
+ }
@@ -0,0 +1 @@
1
+ export { createJwtMiddleware } from "./jwt_middleware";
@@ -0,0 +1,229 @@
1
+ /* eslint-disable no-console */
2
+ import * as chalk from "chalk";
3
+ import * as debug from "debug";
4
+ import type { Request, Response, NextFunction } from "express";
5
+ import * as jwt from "jsonwebtoken";
6
+ import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ import Express from "express-serve-static-core";
9
+
10
+ /**
11
+ * Prefix your start command with `DEBUG=express:middleware:jwt` to enable debug logging
12
+ * for this middleware
13
+ */
14
+ const debugLogger = debug("express:middleware:jwt");
15
+
16
+ const CANVA_BASE_URL = "https://api.canva.com";
17
+
18
+ /**
19
+ * Augment the Express request context to include the appId/userId/brandId fields decoded
20
+ * from the JWT.
21
+ */
22
+ declare module "express-serve-static-core" {
23
+ export interface Request {
24
+ canva: {
25
+ appId: string;
26
+ userId: string;
27
+ brandId: string;
28
+ };
29
+ }
30
+ }
31
+
32
+ type CanvaJwt = Omit<jwt.Jwt, "payload"> & {
33
+ payload: {
34
+ aud?: string;
35
+ userId?: string;
36
+ brandId?: string;
37
+ };
38
+ };
39
+
40
+ const PUBLIC_KEY_DEFAULT_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutes
41
+ const PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS = 30 * 1_000; // 30 seconds
42
+
43
+ const sendUnauthorizedResponse = (res: Response, message?: string) =>
44
+ res.status(401).json({ error: "unauthorized", message });
45
+
46
+ const createJwksUrl = (appId: string) =>
47
+ `${CANVA_BASE_URL}/rest/v1/apps/${appId}/jwks`;
48
+
49
+ /**
50
+ * An Express.js middleware for decoding and verifying a JSON Web Token (JWT).
51
+ * By default, this middleware extracts the token from the `Authorization` header.
52
+ *
53
+ * @remarks
54
+ * If a JWT is successfully decoded, the following properties are added to the request object:
55
+ * - `request.canva.appId` - The ID of the app.
56
+ * - `request.canva.brandId` - The ID of the user's team.
57
+ * - `request.canva.userId` - The ID of the user.
58
+ *
59
+ * @param appId - The ID of the app.
60
+ * @param getTokenFromRequest - A function that extracts a token from the request. If a token isn't found, throw a `JWTAuthorizationError`.
61
+ * @returns An Express.js middleware for verifying and decoding JWTs.
62
+ */
63
+ export function createJwtMiddleware(
64
+ appId: string,
65
+ getTokenFromRequest: GetTokenFromRequest = getTokenFromHttpHeader,
66
+ ): (req: Request, res: Response, next: NextFunction) => void {
67
+ const jwksClient = new JwksClient({
68
+ cache: true,
69
+ cacheMaxAge: PUBLIC_KEY_DEFAULT_EXPIRY_MS,
70
+ timeout: PUBLIC_KEY_DEFAULT_FETCH_TIMEOUT_MS,
71
+ rateLimit: true,
72
+ jwksUri: createJwksUrl(appId),
73
+ });
74
+
75
+ return async (req, res, next) => {
76
+ try {
77
+ debugLogger(`processing JWT for '${req.url}'`);
78
+
79
+ const token = await getTokenFromRequest(req);
80
+ const unverifiedDecodedToken = jwt.decode(token, {
81
+ complete: true,
82
+ });
83
+
84
+ if (unverifiedDecodedToken?.header?.kid == null) {
85
+ console.trace(
86
+ `jwtMiddleware: expected token to contain 'kid' claim header`,
87
+ );
88
+ return sendUnauthorizedResponse(res);
89
+ }
90
+
91
+ const key = await jwksClient.getSigningKey(
92
+ unverifiedDecodedToken.header.kid,
93
+ );
94
+ const publicKey = key.getPublicKey();
95
+ const verifiedToken = jwt.verify(token, publicKey, {
96
+ audience: appId,
97
+ complete: true,
98
+ }) as CanvaJwt;
99
+ const { payload } = verifiedToken;
100
+ debugLogger("payload: %O", payload);
101
+
102
+ if (
103
+ payload.userId == null ||
104
+ payload.brandId == null ||
105
+ payload.aud == null
106
+ ) {
107
+ console.trace(
108
+ "jwtMiddleware: failed to decode jwt missing fields from payload",
109
+ );
110
+ return sendUnauthorizedResponse(res);
111
+ }
112
+
113
+ req.canva = {
114
+ appId: payload.aud,
115
+ brandId: payload.brandId,
116
+ userId: payload.userId,
117
+ };
118
+
119
+ next();
120
+ } catch (e) {
121
+ if (e instanceof JWTAuthorizationError) {
122
+ return sendUnauthorizedResponse(res, e.message);
123
+ }
124
+
125
+ if (e instanceof SigningKeyNotFoundError) {
126
+ return sendUnauthorizedResponse(
127
+ res,
128
+ `Public key not found. ${chalk.bgRedBright(
129
+ "Ensure you have the correct App_ID set",
130
+ )}.`,
131
+ );
132
+ }
133
+
134
+ if (e instanceof jwt.JsonWebTokenError) {
135
+ return sendUnauthorizedResponse(res, "Token is invalid");
136
+ }
137
+
138
+ if (e instanceof jwt.TokenExpiredError) {
139
+ return sendUnauthorizedResponse(res, "Token expired");
140
+ }
141
+
142
+ next(e);
143
+ }
144
+ };
145
+ }
146
+
147
+ export type GetTokenFromRequest = (req: Request) => Promise<string> | string;
148
+
149
+ export const getTokenFromQueryString: GetTokenFromRequest = (
150
+ req: Request,
151
+ ): string => {
152
+ // The name of a query string parameter bearing the JWT
153
+ const tokenQueryStringParamName = "canva_user_token";
154
+
155
+ const queryParam = req.query[tokenQueryStringParamName];
156
+ if (!queryParam || typeof queryParam !== "string") {
157
+ console.trace(
158
+ `jwtMiddleware: missing "${tokenQueryStringParamName}" query parameter`,
159
+ );
160
+ throw new JWTAuthorizationError(
161
+ `Missing "${tokenQueryStringParamName}" query parameter`,
162
+ );
163
+ }
164
+
165
+ if (!looksLikeJWT(queryParam)) {
166
+ console.trace(
167
+ `jwtMiddleware: invalid "${tokenQueryStringParamName}" query parameter`,
168
+ );
169
+ throw new JWTAuthorizationError(
170
+ `Invalid "${tokenQueryStringParamName}" query parameter`,
171
+ );
172
+ }
173
+
174
+ return queryParam;
175
+ };
176
+
177
+ export const getTokenFromHttpHeader: GetTokenFromRequest = (
178
+ req: Request,
179
+ ): string => {
180
+ // The names of a HTTP header bearing the JWT, and a scheme
181
+ const headerName = "Authorization";
182
+ const schemeName = "Bearer";
183
+
184
+ const header = req.header(headerName);
185
+ if (!header) {
186
+ throw new JWTAuthorizationError(`Missing the "${headerName}" header`);
187
+ }
188
+
189
+ if (!header.match(new RegExp(`^${schemeName}\\s+[^\\s]+$`, "i"))) {
190
+ console.trace(
191
+ `jwtMiddleware: failed to match token in "${headerName}" header`,
192
+ );
193
+ throw new JWTAuthorizationError(
194
+ `Missing a "${schemeName}" token in the "${headerName}" header`,
195
+ );
196
+ }
197
+
198
+ const token = header.replace(new RegExp(`^${schemeName}\\s+`, "i"), "");
199
+ if (!token || !looksLikeJWT(token)) {
200
+ throw new JWTAuthorizationError(
201
+ `Invalid "${schemeName}" token in the "${headerName}" header`,
202
+ );
203
+ }
204
+
205
+ return token;
206
+ };
207
+
208
+ /**
209
+ * A class representing JWT validation errors in the JWT middleware.
210
+ * The error message provided to the constructor will be forwarded to the
211
+ * API consumer trying to access a JWT-protected endpoint.
212
+ * @private
213
+ */
214
+ export class JWTAuthorizationError extends Error {
215
+ constructor(message: string) {
216
+ super(message);
217
+
218
+ Object.setPrototypeOf(this, JWTAuthorizationError.prototype);
219
+ }
220
+ }
221
+
222
+ const looksLikeJWT = (
223
+ token: string,
224
+ ): boolean => // Base64 alphabet includes
225
+ // - letters (a-z and A-Z)
226
+ // - digits (0-9)
227
+ // - two special characters (+/ or -_)
228
+ // - padding (=)
229
+ token.match(/^[a-z0-9+/\-_=.]+$/i) != null;