Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/serious-chicken-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@clerk/backend": minor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Semantic version should be major, not minor (breaking public API).

This rename removes/renames public methods (namespace rename, create→createToken, revoke→revokeToken, verifySecret removed). That’s a breaking change for consumers and warrants a major bump for @clerk/backend.

Apply this diff:

-"@clerk/backend": minor
+"@clerk/backend": major
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@clerk/backend": minor
"@clerk/backend": major
🤖 Prompt for AI Agents
In .changeset/serious-chicken-report.md around line 2, the changeset incorrectly
marks "@clerk/backend" as "minor" even though the rename is a breaking API
change; update the changeset to mark "@clerk/backend" as "major" (replace
"minor" with "major") so the release tooling will generate a major version bump
for that package.

---

Rename M2M namespace from `m2mTokens` to `m2m` in Backend API client

Before:

```ts
clerkClient.m2mTokens.create()

clerkClient.m2mTokens.revoke()

clerkClient.m2mTokens.verifySecret({ secret: 'ak_xxx' })
```

After:

```ts
clerkClient.m2m.createToken()

clerkClient.m2m.revokeToken()

clerkClient.m2m.verifyToken({ token: 'ak_xxx' })
```
Comment on lines +5 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Double-check repo-wide references to old namespace before merging.

Ensure there are no lingering references to m2mTokens or verifySecret in code, docs, or examples beyond the updated tests.


🏁 Script executed:

#!/bin/bash
# From repo root: find lingering references in source, tests, and docs (excluding node_modules, dist, build)
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' $'\\bm2mTokens\\b|\\bverifySecret\\b' -S

Length of output: 2060


Update all lingering m2mTokens and verifySecret references before merging

A global search turned up several remaining instances that need to be renamed or removed:

• packages/backend/src/tokens/verify.ts:235
• packages/backend/src/api/endpoints/APIKeysApi.ts:68
• CHANGELOGs in:
– packages/astro/CHANGELOG.md (line 34)
– packages/tanstack-react-start/CHANGELOG.md (34)
– packages/remix/CHANGELOG.md (34)
– packages/react-router/CHANGELOG.md (34)
– packages/nuxt/CHANGELOG.md (32)
– packages/fastify/CHANGELOG.md (30)
– packages/express/CHANGELOG.md (32)
– packages/nextjs/CHANGELOG.md (42)
– packages/backend/CHANGELOG.md (42, 56, 65, 78, 86)
• Other changesets under .changeset:
– serious-chicken-report.md (lines 5,10,12,14,27)
– small-adults-crash.md (5)
– chatty-kings-raise.md (10, 18)

Please replace all clerkClient.m2mTokens.*() calls with the new clerkClient.m2m.*() variants and update verifySecret(…)verifyToken(…) in these files.

🤖 Prompt for AI Agents
In .changeset/serious-chicken-report.md around lines 5 to 25, there are
lingering references to the old M2M API naming; update all occurrences listed
(packages/backend/src/tokens/verify.ts:235,
packages/backend/src/api/endpoints/APIKeysApi.ts:68, the various CHANGELOGs and
other changesets called out) by replacing clerkClient.m2mTokens.*() with
clerkClient.m2m.*() and rename verifySecret(...) to verifyToken(...), ensuring
any method-name changes follow the new mapping (e.g., create() → createToken(),
revoke() → revokeToken(), verifySecret → verifyToken) and update the text in
changelogs/changesets accordingly.

Comment on lines +20 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm and document any additional verifyToken parameters (if applicable).

If verifyToken supports optional parameters (e.g., machineSecretKey) in this release, it’s worth including a second example here to prevent confusion.

Run this script to locate the source and confirm the signature, then update the example accordingly:


🏁 Script executed:

#!/bin/bash
# Verify verifyToken signature and usages across the repo
rg -n -A2 -B2 $'verifyToken\\s*\\('
rg -n $'verifySecret\\s*\\('

Length of output: 13802


Document optional machineSecretKey in m2m.verifyToken

The M2M verifyToken call accepts an optional machineSecretKey alongside token. Update the example in .changeset/serious-chicken-report.md to show both usages:

 clerkClient.m2m.createToken()

 clerkClient.m2m.revokeToken()

- clerkClient.m2m.verifyToken({ token: 'mt_xxx' })
+ // Verify with the default API key configured on the client
+ clerkClient.m2m.verifyToken({ token: 'mt_xxx' })
+
+ // Verify using an explicit machineSecretKey
+ clerkClient.m2m.verifyToken({
+   token: 'mt_xxx',
+   machineSecretKey: 'sk_test_machine_secret'
+ })

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .changeset/serious-chicken-report.md around lines 20 to 25, the example for
m2m.verifyToken only shows passing token; update the example to document the
optional machineSecretKey parameter by providing two usages: one calling
clerkClient.m2m.verifyToken({ token: 'ak_xxx' }) and another calling
clerkClient.m2m.verifyToken({ token: 'ak_xxx', machineSecretKey: 'sk_xxx' });
ensure surrounding backticks/formatting remain valid and the examples are
adjacent to the existing createToken and revokeToken lines.


The `verifySecret()` method is removed. Please use `.verifyToken()` instead.
38 changes: 7 additions & 31 deletions integration/tests/machine-auth/m2m.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,7 @@ test.describe('machine-to-machine auth @machine', () => {
const token = req.get('Authorization')?.split(' ')[1];

try {
const m2mToken = await clerkClient.m2mTokens.verifyToken({ token });
res.send('Protected response ' + m2mToken.id);
} catch {
res.status(401).send('Unauthorized');
}
});

app.get('/api/protected-deprecated', async (req, res) => {
const secret = req.get('Authorization')?.split(' ')[1];

try {
const m2mToken = await clerkClient.m2mTokens.verifySecret({ secret });
const m2mToken = await clerkClient.m2m.verifyToken({ token });
res.send('Protected response ' + m2mToken.id);
} catch {
res.status(401).send('Unauthorized');
Expand All @@ -81,7 +70,7 @@ test.describe('machine-to-machine auth @machine', () => {
name: `${fakeCompanyName} Email Server`,
scopedMachines: [primaryApiServer.id],
});
emailServerM2MToken = await client.m2mTokens.create({
emailServerM2MToken = await client.m2m.createToken({
machineSecretKey: emailServer.secretKey,
secondsUntilExpiration: 60 * 30,
});
Expand All @@ -91,7 +80,7 @@ test.describe('machine-to-machine auth @machine', () => {
name: `${fakeCompanyName} Analytics Server`,
// No scoped machines
});
analyticsServerM2MToken = await client.m2mTokens.create({
analyticsServerM2MToken = await client.m2m.createToken({
machineSecretKey: analyticsServer.secretKey,
secondsUntilExpiration: 60 * 30,
});
Expand All @@ -102,10 +91,10 @@ test.describe('machine-to-machine auth @machine', () => {
secretKey: instanceKeys.get('with-api-keys').sk,
});

await client.m2mTokens.revoke({
await client.m2m.revokeToken({
m2mTokenId: emailServerM2MToken.id,
});
await client.m2mTokens.revoke({
await client.m2m.revokeToken({
m2mTokenId: analyticsServerM2MToken.id,
});
await client.machines.delete(emailServer.id);
Expand Down Expand Up @@ -164,7 +153,7 @@ test.describe('machine-to-machine auth @machine', () => {

// Analytics server can access primary API server after adding scope
await u.services.clerk.machines.createScope(analyticsServer.id, primaryApiServer.id);
const m2mToken = await u.services.clerk.m2mTokens.create({
const m2mToken = await u.services.clerk.m2m.createToken({
machineSecretKey: analyticsServer.secretKey,
secondsUntilExpiration: 60 * 30,
});
Expand All @@ -176,21 +165,8 @@ test.describe('machine-to-machine auth @machine', () => {
});
expect(res2.status()).toBe(200);
expect(await res2.text()).toBe('Protected response ' + m2mToken.id);
await u.services.clerk.m2mTokens.revoke({
await u.services.clerk.m2m.revokeToken({
m2mTokenId: m2mToken.id,
});
});

test('authorizes M2M requests with deprecated verifySecret method', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected-deprecated', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
expect(await res.text()).toBe('Protected response ' + emailServerM2MToken.id);
});
});
97 changes: 10 additions & 87 deletions packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('M2MToken', () => {
),
);

const response = await apiClient.m2mTokens.create({
const response = await apiClient.m2m.createToken({
secondsUntilExpiration: 3600,
});

Expand All @@ -65,7 +65,7 @@ describe('M2MToken', () => {
),
);

const response = await apiClient.m2mTokens.create({
const response = await apiClient.m2m.createToken({
machineSecretKey: 'ak_xxxxx',
secondsUntilExpiration: 3600,
});
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('M2MToken', () => {
),
);

const errResponse = await apiClient.m2mTokens.create().catch(err => err);
const errResponse = await apiClient.m2m.createToken().catch(err => err);

expect(errResponse.status).toBe(401);
expect(errResponse.errors[0].code).toBe('machine_secret_key_invalid');
Expand Down Expand Up @@ -143,7 +143,7 @@ describe('M2MToken', () => {
),
);

const response = await apiClient.m2mTokens.revoke({
const response = await apiClient.m2m.revokeToken({
m2mTokenId: m2mId,
revocationReason: 'revoked by test',
});
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('M2MToken', () => {
),
);

const response = await apiClient.m2mTokens.revoke({
const response = await apiClient.m2m.revokeToken({
m2mTokenId: m2mId,
revocationReason: 'revoked by test',
});
Expand All @@ -195,8 +195,8 @@ describe('M2MToken', () => {
),
);

const errResponse = await apiClient.m2mTokens
.revoke({
const errResponse = await apiClient.m2m
.revokeToken({
m2mTokenId: m2mId,
revocationReason: 'revoked by test',
})
Expand All @@ -223,7 +223,7 @@ describe('M2MToken', () => {
),
);

const response = await apiClient.m2mTokens.verifyToken({
const response = await apiClient.m2m.verifyToken({
token: m2mSecret,
});

Expand All @@ -249,7 +249,7 @@ describe('M2MToken', () => {
),
);

const response = await apiClient.m2mTokens.verifyToken({
const response = await apiClient.m2m.verifyToken({
token: m2mSecret,
});

Expand All @@ -273,7 +273,7 @@ describe('M2MToken', () => {
),
);

const errResponse = await apiClient.m2mTokens
const errResponse = await apiClient.m2m
.verifyToken({
token: m2mSecret,
})
Expand All @@ -282,81 +282,4 @@ describe('M2MToken', () => {
expect(errResponse.status).toBe(401);
});
});

describe('verifySecret (deprecated)', () => {
it('verifies a m2m token using machine secret', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
machineSecretKey: 'ak_xxxxx',
});

server.use(
http.post(
'https://api.clerk.test/m2m_tokens/verify',
validateHeaders(({ request }) => {
expect(request.headers.get('Authorization')).toBe('Bearer ak_xxxxx');
return HttpResponse.json(mockM2MToken);
}),
),
);

const response = await apiClient.m2mTokens.verifySecret({
secret: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});

it('verifies a m2m token using instance secret', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_xxxxx',
});

server.use(
http.post(
'https://api.clerk.test/m2m_tokens/verify',
validateHeaders(({ request }) => {
expect(request.headers.get('Authorization')).toBe('Bearer sk_xxxxx');
return HttpResponse.json(mockM2MToken);
}),
),
);

const response = await apiClient.m2mTokens.verifySecret({
secret: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});

it('requires a machine secret or instance secret to verify a m2m token', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
});

server.use(
http.post(
'https://api.clerk.test/m2m_tokens/verify',
validateHeaders(() => {
return HttpResponse.json(mockM2MToken);
}),
),
);

const errResponse = await apiClient.m2mTokens
.verifySecret({
secret: m2mSecret,
})
.catch(err => err);

expect(errResponse.status).toBe(401);
});
});
});
6 changes: 3 additions & 3 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifyToken({
const response = await apiClient.m2m.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
token: 'mt_secret_test',
});
Expand Down Expand Up @@ -353,7 +353,7 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifyToken({
const response = await apiClient.m2m.verifyToken({
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
Expand Down Expand Up @@ -425,7 +425,7 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifyToken({
const response = await apiClient.m2m.verifyToken({
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
Expand Down
39 changes: 2 additions & 37 deletions packages/backend/src/api/endpoints/M2MTokenApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { deprecated } from '@clerk/shared/deprecated';

import { joinPaths } from '../../util/path';
import type { ClerkBackendApiRequestOptions } from '../request';
import type { M2MToken } from '../resources/M2MToken';
Expand Down Expand Up @@ -33,17 +31,6 @@ type RevokeM2MTokenParams = {
revocationReason?: string | null;
};

type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token secret to verify.
*/
secret: string;
};

type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
Expand All @@ -70,7 +57,7 @@ export class M2MTokenApi extends AbstractAPI {
return options;
}

async create(params?: CreateM2MTokenParams) {
async createToken(params?: CreateM2MTokenParams) {
const { claims = null, machineSecretKey, secondsUntilExpiration = null } = params || {};

const requestOptions = this.#createRequestOptions(
Expand All @@ -88,7 +75,7 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async revoke(params: RevokeM2MTokenParams) {
async revokeToken(params: RevokeM2MTokenParams) {
const { m2mTokenId, revocationReason = null, machineSecretKey } = params;

this.requireId(m2mTokenId);
Expand All @@ -107,28 +94,6 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

/**
* Verify a machine-to-machine token.
*
* @deprecated Use {@link verifyToken} instead.
*/
async verifySecret(params: VerifyM2MTokenParamsDeprecated) {
const { secret, machineSecretKey } = params;

deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');

const requestOptions = this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { secret },
},
machineSecretKey,
);

return this.request<M2MToken>(requestOptions);
}

async verifyToken(params: VerifyM2MTokenParams) {
const { token, machineSecretKey } = params;

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/api/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
jwks: new JwksAPI(request),
jwtTemplates: new JwtTemplatesApi(request),
machines: new MachineApi(request),
m2mTokens: new M2MTokenApi(
m2m: new M2MTokenApi(
buildRequest({
...options,
skipApiVersionInUrl: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/tokens/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async function verifyM2MToken(
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifyToken({ token });
const verifiedToken = await client.m2m.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
Comment on lines +209 to 210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Pass per-call machineSecretKey and align the exported API type

The low-level API supports a per-call machineSecretKey, but it’s not forwarded here. Also, verifyMachineAuthToken’s options type currently excludes machineSecretKey, preventing consumers from passing it even though verifyM2MToken supports it.

Forward the per-call secret:

-    const verifiedToken = await client.m2m.verifyToken({ token });
+    const verifiedToken = await client.m2m.verifyToken({ token, machineSecretKey: options.machineSecretKey });

And update the exported function type (outside this hunk; Lines 248-249) to accept the machine secret:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
+export async function verifyMachineAuthToken(
+  token: string,
+  options: VerifyTokenOptions & { machineSecretKey?: string },
+) {

This preserves generic usage while allowing proper auth for M2M verification without requiring an instance secret.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const verifiedToken = await client.m2m.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
// … around line 209 …
- const verifiedToken = await client.m2m.verifyToken({ token });
+ const verifiedToken = await client.m2m.verifyToken({
+ token,
+ machineSecretKey: options.machineSecretKey,
+ });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
// …
Suggested change
const verifiedToken = await client.m2m.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
// … around lines 248–249 …
-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
+export async function verifyMachineAuthToken(
+ token: string,
+ options: VerifyTokenOptions & { machineSecretKey?: string },
+) {
// function body…
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 209 to 210, the per-call
machineSecretKey supported by the low-level client isn't being forwarded to
client.m2m.verifyToken and the exported verifyMachineAuthToken options type
(around lines 248-249) currently excludes machineSecretKey; update the call to
pass through the machineSecretKey from options into client.m2m.verifyToken and
modify the exported function/options type to include machineSecretKey (while
preserving existing generics and other fields) so callers can provide a per-call
secret for M2M verification without requiring an instance secret.

} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand Down
Loading