Skip to content

Commit 4e9e9b4

Browse files
[2026-03-25] Release: Update Connect API Starter Kit (#32)
Release: Update Connect API Starter Kit Generated on: Wed Mar 25 23:13:40 UTC 2026 Source commit: 5ae1436ccb190c6a32d44e795a589280a7829bc5 Co-authored-by: canva-sdk-releases[bot] <227329455+canva-sdk-releases[bot]@users.noreply.github.com>
1 parent df4b13e commit 4e9e9b4

15 files changed

Lines changed: 2650 additions & 1957 deletions

File tree

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20.14.0
1+
lts/krypton

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 2026-03-26
4+
5+
### 🔧 Changed
6+
7+
- Updated the `.nvmrc` to recommend Node.js lts/krypton (v24) and all `@types/node` dependencies to `24.12.0`.
8+
- Upgrade examples/backend/typescript-express `better-sqlite3` from `8.5.0` -> `12.8.0` (to suit later node.js versions 22 & 24).
9+
10+
### 🐞 Fixed
11+
12+
- Fixed OAuth token exchange in the demos failing with `bad_request_body`. The `bodySerializer` override passed to `OauthService.exchangeAccessToken` and `OauthService.revokeTokens` was calling `.toString()` on a plain object, producing `"[object Object]"` as the request body. The SDK already applies the correct `urlSearchParamsBodySerializer` to these endpoints, so the manual overrides have been removed.
13+
314
## 2026-03-19
415

516
### 🔧 Changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ This repo contains our OpenAPI specifications, as well as a demo ecommerce web a
66

77
## Requirements
88

9-
- Node.js `v20.14.0`
10-
- npm `v9` or `v10`
9+
- Node.js `v24`
10+
- npm `v11`
1111

1212
**Note:** To make sure you're running the correct version of Node.js, we recommend using a version manager, such as [nvm](https://github.com/nvm-sh/nvm#intro). The [.nvmrc](/.nvmrc) file in the root directory of this repo will ensure the correct version is used once you run `nvm install`.
1313

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { OauthService } from "@canva/connect-api-ts";
2+
import { createClient } from "@canva/connect-api-ts/client";
3+
4+
const mockClient = createClient({
5+
baseUrl: "https://api.canva.com",
6+
headers: { Authorization: "Basic aaaaaaaa" },
7+
});
8+
9+
describe("OAuth service body serialization", () => {
10+
let mockFetch: jest.Mock;
11+
12+
beforeEach(() => {
13+
mockFetch = jest.fn();
14+
global.fetch = mockFetch;
15+
});
16+
17+
afterEach(() => {
18+
jest.restoreAllMocks();
19+
});
20+
21+
describe("exchangeAccessToken", () => {
22+
it("serializes the request body as application/x-www-form-urlencoded", async () => {
23+
mockFetch.mockResolvedValue(
24+
new Response(
25+
JSON.stringify({
26+
access_token: "tok",
27+
refresh_token: "ref",
28+
token_type: "Bearer",
29+
expires_in: 3600,
30+
}),
31+
{ status: 200, headers: { "Content-Type": "application/json" } },
32+
),
33+
);
34+
35+
await OauthService.exchangeAccessToken({
36+
client: mockClient,
37+
body: {
38+
grant_type: "authorization_code",
39+
code: "test-code",
40+
code_verifier: "test-verifier",
41+
redirect_uri: "http://localhost:3001/oauth/redirect",
42+
},
43+
});
44+
45+
const request = mockFetch.mock.calls[0][0] as Request;
46+
const body = await request.text();
47+
const parsed = Object.fromEntries(new URLSearchParams(body));
48+
49+
expect(parsed.grant_type).toBe("authorization_code");
50+
expect(parsed.code).toBe("test-code");
51+
expect(parsed.code_verifier).toBe("test-verifier");
52+
expect(parsed.redirect_uri).toBe("http://localhost:3001/oauth/redirect");
53+
// Regression guard: plain object .toString() returns "[object Object]"
54+
expect(body).not.toBe("[object Object]");
55+
});
56+
});
57+
58+
describe("revokeTokens", () => {
59+
it("serializes the request body as application/x-www-form-urlencoded", async () => {
60+
mockFetch.mockResolvedValue(new Response(null, { status: 200 }));
61+
62+
await OauthService.revokeTokens({
63+
client: mockClient,
64+
body: {
65+
client_id: "my-client-id",
66+
client_secret: "my-client-secret",
67+
token: "test-refresh-token",
68+
},
69+
});
70+
71+
const request = mockFetch.mock.calls[0][0] as Request;
72+
const body = await request.text();
73+
const parsed = Object.fromEntries(new URLSearchParams(body));
74+
75+
expect(parsed.client_id).toBe("my-client-id");
76+
expect(parsed.token).toBe("test-refresh-token");
77+
// Regression guard: plain object .toString() returns "[object Object]"
78+
expect(body).not.toBe("[object Object]");
79+
});
80+
});
81+
});

demos/common/backend/services/client.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,7 @@ export async function getAccessTokenForUser(
3535

3636
const result = await OauthService.exchangeAccessToken({
3737
client: getBasicAuthClient(),
38-
// by default, the body is JSON stringified, but given this endpoint expects form URL encoded data
39-
// we need to override the `bodySerializer`
4038
body: params,
41-
bodySerializer: (params) => params.toString(),
42-
headers: {
43-
"Content-Type": "application/x-www-form-urlencoded",
44-
},
4539
baseUrl: process.env.BASE_CANVA_CONNECT_API_URL,
4640
});
4741

demos/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"@types/cors": "2.8.19",
4040
"@types/express": "4.17.21",
4141
"@types/multer": "2.0.0",
42-
"@types/node": "20.19.2",
42+
"@types/node": "24.12.0",
4343
"@types/nodemon": "1.19.6",
4444
"@types/yargs": "17.0.33",
4545
"prettier": "3.6.2",

demos/ecommerce_shop/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@types/cors": "2.8.19",
2828
"@types/express": "4.17.21",
2929
"@types/multer": "2.0.0",
30-
"@types/node": "20.19.2",
30+
"@types/node": "24.12.0",
3131
"prettier": "3.6.2",
3232
"typescript": "5.9.2"
3333
}

demos/ecommerce_shop/backend/routes/auth.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,6 @@ router.get(endpoints.REDIRECT, async (req, res) => {
7676
const result = await OauthService.exchangeAccessToken({
7777
client: getBasicAuthClient(),
7878
body: params,
79-
// by default, the body is JSON stringified, but given this endpoint expects form URL encoded data
80-
// we need to override the `bodySerializer`
81-
bodySerializer: (params) => params.toString(),
82-
headers: {
83-
"Content-Type": "application/x-www-form-urlencoded",
84-
},
8579
});
8680

8781
if (result.error) {
@@ -207,13 +201,7 @@ router.get(endpoints.REVOKE, async (req, res) => {
207201

208202
await OauthService.revokeTokens({
209203
client: getBasicAuthClient(),
210-
// by default, the body is JSON stringified, but given this endpoint expects form URL encoded data
211-
// we need to override the `bodySerializer`
212204
body: params,
213-
bodySerializer: (params) => params.toString(),
214-
headers: {
215-
"Content-Type": "application/x-www-form-urlencoded",
216-
},
217205
});
218206
} catch (e) {
219207
console.log(e);

demos/ecommerce_shop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@eslint/js": "9.34.0",
3434
"@types/cookie-parser": "1.4.9",
3535
"@types/jest": "30.0.0",
36-
"@types/node": "20.19.2",
36+
"@types/node": "24.12.0",
3737
"@types/nodemon": "1.19.6",
3838
"@types/yargs": "17.0.33",
3939
"@typescript-eslint/eslint-plugin": "8.41.0",

demos/playground/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@types/cors": "2.8.19",
2828
"@types/express": "4.17.21",
2929
"@types/multer": "2.0.0",
30-
"@types/node": "20.19.2",
30+
"@types/node": "24.12.0",
3131
"prettier": "3.6.2",
3232
"typescript": "5.9.2"
3333
}

0 commit comments

Comments
 (0)