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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
5 changes: 5 additions & 0 deletions .changeset/small-adults-crash.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/backend": minor
---

Deprecates `clerkClient.m2mTokens.verifySecret({ secret: 'mt_xxx' })` in favor or `clerkClient.m2mTokens.verifyToken({ token: 'mt_xxx' })`
30 changes: 27 additions & 3 deletions integration/tests/machine-auth/m2m.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,17 @@ test.describe('machine-to-machine auth @machine', () => {
const app = express();

app.get('/api/protected', async (req, res) => {
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 {
Expand DownExpand Up@@ -129,7 +140,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${analyticsServerM2MToken.secret}`,
Authorization: `Bearer ${analyticsServerM2MToken.token}`,
},
});
expect(res.status()).toBe(401);
Expand All@@ -145,7 +156,7 @@ test.describe('machine-to-machine auth @machine', () => {
// Email server can access primary API server
const res = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${emailServerM2MToken.secret}`,
Authorization: `Bearer ${emailServerM2MToken.token}`,
},
});
expect(res.status()).toBe(200);
Expand All@@ -160,7 +171,7 @@ test.describe('machine-to-machine auth @machine', () => {

const res2 = await u.page.request.get(app.serverUrl + '/api/protected', {
headers: {
Authorization: `Bearer ${m2mToken.secret}`,
Authorization: `Bearer ${m2mToken.token}`,
},
});
expect(res2.status()).toBe(200);
Expand All@@ -169,4 +180,17 @@ test.describe('machine-to-machine auth @machine', () => {
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);
});
});
82 changes: 81 additions & 1 deletion packages/backend/src/api/__tests__/M2MTokenApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,9 @@ describe('M2MToken', () => {
subject: 'mch_xxxxx',
scopes: ['mch_1xxxxx', 'mch_2xxxxx'],
claims: { foo: 'bar' },
// Deprecated in favor of `token`
secret: m2mSecret,
token: m2mSecret,
revoked: false,
revocation_reason: null,
expired: false,
Expand DownExpand Up@@ -46,6 +48,7 @@ describe('M2MToken', () => {

expect(response.id).toBe(m2mId);
expect(response.secret).toBe(m2mSecret);
expect(response.token).toBe(m2mSecret);
expect(response.scopes).toEqual(['mch_1xxxxx', 'mch_2xxxxx']);
expect(response.claims).toEqual({ foo: 'bar' });
});
Expand DownExpand Up@@ -206,7 +209,84 @@ describe('M2MToken', () => {
});
});

describe('verifySecret', () => {
describe('verifyToken', () => {
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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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.verifyToken({
token: m2mSecret,
});

expect(response.id).toBe(m2mId);
expect(response.secret).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
.verifyToken({
token: m2mSecret,
})
.catch(err => err);

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',
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -325,9 +325,9 @@ describe('api.client', () => {
),
);

const response = await apiClient.m2mTokens.verifySecret({
const response = await apiClient.m2mTokens.verifyToken({
machineSecretKey: 'ak_test_in_header_params', // this will be added to headerParams.Authorization
secret: 'mt_secret_test',
token: 'mt_secret_test',
});
expect(response.id).toBe('mt_test');
});
Expand All@@ -353,8 +353,8 @@ describe('api.client', () => {
),
);

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

it('prioritizes machine secret key over secret key when both are provided and useMachineSecretKey is true', async () => {
it('prioritizes machine secret key over instance secret key when both are provided and useMachineSecretKey is true', async () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'sk_test_xxx',
Expand All@@ -425,8 +425,8 @@ describe('api.client', () => {
),
);

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

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

type VerifyM2MTokenParams = {
type VerifyM2MTokenParamsDeprecated = {
/**
* Custom machine secret key for authentication.
*/
Expand All@@ -42,6 +44,17 @@ type VerifyM2MTokenParams = {
secret: string;
};

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

Comment on lines +47 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Export the new VerifyM2MTokenParams type

Same rationale as above: export the public-facing type.

-type VerifyM2MTokenParams = {+export type VerifyM2MTokenParams = {
/**
* Custom machine secret key for authentication.
*/
machineSecretKey?: string;
/**
* Machine-to-machine token to verify.
*/
token: string;
};
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 47 to 57, the
VerifyM2MTokenParams type is declared but not exported; change its declaration
to export the type (i.e., export type VerifyM2MTokenParams = { ... }) so it
becomes part of the public API, and update any files that should import it to
import the exported type rather than relying on local duplication.

export class M2MTokenApi extends AbstractAPI {
#createRequestOptions(options: ClerkBackendApiRequestOptions, machineSecretKey?: string) {
if (machineSecretKey) {
Expand DownExpand Up@@ -94,9 +107,16 @@ export class M2MTokenApi extends AbstractAPI {
return this.request<M2MToken>(requestOptions);
}

async verifySecret(params: VerifyM2MTokenParams) {
/**
* 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(
Comment on lines +110 to 120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type and explicit access modifier to public API

Per guidelines, public APIs should declare explicit return types and use explicit access modifiers. Also, consider including a removal timeline in the deprecation message to set expectations.

- async verifySecret(params: VerifyM2MTokenParamsDeprecated) {+ public async verifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken> {
const { secret, machineSecretKey } = params;
- deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');+ deprecated('verifySecret', 'Use `verifyToken({ token: mt_xxx })` instead');
📝 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
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
asyncverifySecret(params: VerifyM2MTokenParamsDeprecated){
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
/**
*Verifyamachine-to-machinetoken.
*
* @deprecatedUse{@linkverifyToken}instead.
*/
publicasyncverifySecret(params: VerifyM2MTokenParamsDeprecated): Promise<M2MToken>{
const{ secret, machineSecretKey }=params;
deprecated('verifySecret','Use `verifyToken({ token: mt_xxx })` instead');
constrequestOptions=this.#createRequestOptions(
// …
);
// …
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 110 to 120,
the public deprecated method verifySecret lacks an explicit access modifier and
an explicit return type and its deprecation message doesn't include a removal
timeline; update the method signature to include an explicit access modifier
(public) and an explicit return type matching the actual resolved promise type
(e.g., Promise<VerifyTokenResult> or the correct interface used elsewhere), and
update the deprecated(...) call text to include a clear removal timeline (for
example: "deprecated: remove in vX.Y - use `verifyToken({ token: mt_xxx })`
instead") so the API surface is explicit and users know when it will be removed.

{
method: 'POST',
Expand All@@ -108,4 +128,19 @@ export class M2MTokenApi extends AbstractAPI {

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

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

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

return this.request<M2MToken>(requestOptions);
}
Comment on lines +132 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document verifyToken and add explicit return type with access modifier

Add JSDoc for the new public method and specify the return type. This improves DX and aligns with the project’s TypeScript/JSDoc guidelines.

- async verifyToken(params: VerifyM2MTokenParams) {+ /**+ * Verify a machine-to-machine token.+ *+ * Verifies the provided M2M token against the Clerk API.+ * By default, the client uses the configured Machine Secret Key.+ * You can override it per-call by passing `machineSecretKey`.+ *+ * @param params - The verification parameters.+ * @returns The verified M2MToken resource.+ */+ public async verifyToken(params: VerifyM2MTokenParams): Promise<M2MToken> {
const { token, machineSecretKey } = params;
📝 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
asyncverifyToken(params: VerifyM2MTokenParams){
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
/**
*Verifyamachine-to-machinetoken.
*
*VerifiestheprovidedM2MtokenagainsttheClerkAPI.
*Bydefault,theclientusestheconfiguredMachineSecretKey.
*Youcanoverrideitper-callbypassing`machineSecretKey`.
*
* @paramparams-Theverificationparameters.
* @returnsTheverifiedM2MTokenresource.
*/
publicasyncverifyToken(params: VerifyM2MTokenParams): Promise<M2MToken>{
const{ token, machineSecretKey }=params;
constrequestOptions=this.#createRequestOptions(
{
method: 'POST',
path: joinPaths(basePath,'verify'),
bodyParams: {token},
},
machineSecretKey,
);
returnthis.request<M2MToken>(requestOptions);
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/M2MTokenApi.ts around lines 132 to 145,
the new verifyToken method is missing JSDoc and an explicit public return type;
add a JSDoc block describing the method, its parameters, and the returned
M2MToken, and change the signature to include the access modifier and explicit
return type (e.g., public async verifyToken(params: VerifyM2MTokenParams):
Promise<M2MToken>) so the method is documented and types are explicit.

}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -734,7 +734,11 @@ export interface MachineSecretKeyJSON {

export interface M2MTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.M2MToken;
/**
* @deprecated Use {@link token} instead.
*/
secret?: string;
token?: string;
subject: string;
scopes: string[];
claims: Record<string, any> | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/resources/M2MToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export class M2MToken {
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
readonly token?: string,
readonly secret?: string,
) {}
Comment on lines +15 to 17

Copy link
Copy Markdown
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

Avoid a silent breaking change in the constructor parameter order

Inserting token before secret changes the positional index of secret. Any direct instantiation like new M2MToken(..., updatedAt, secret) will now set token instead of secret, leaving secret undefined. This can cause subtle bugs without type errors.

To preserve backward compatibility, keep secret in its original position and add token after it. Update fromJSON accordingly.

- readonly updatedAt: number,- readonly token?: string,- readonly secret?: string,+ readonly updatedAt: number,+ readonly secret?: string,+ readonly token?: string,
@@
- data.updated_at,- data.token,- data.secret,+ data.updated_at,+ data.secret,+ data.token,

Also applies to: 31-33

🤖 Prompt for AI Agents
In packages/backend/src/api/resources/M2MToken.ts around lines 15-17 (and
similarly adjust lines 31-33), the constructor parameter order was changed to
place token before secret which breaks positional instantiation; restore the
original parameter order so secret remains before token (e.g., ..., updatedAt,
secret, token) and update the fromJSON factory to map JSON.secret to the secret
parameter and JSON.token to the token parameter in that same order to preserve
backward compatibility.


Expand All@@ -27,6 +28,7 @@ export class M2MToken {
data.expiration,
data.created_at,
data.updated_at,
data.token,
data.secret,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/tokens/verify.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,13 +200,13 @@ function handleClerkAPIError(
};
}

async function verifyMachineToken(
secret: string,
async function verifyM2MToken(
token: string,
options: VerifyTokenOptions & { machineSecretKey?: string },
): Promise<MachineTokenReturnType<M2MToken, MachineTokenVerificationError>> {
try {
const client = createBackendApiClient(options);
const verifiedToken = await client.m2mTokens.verifySecret({ secret });
const verifiedToken = await client.m2mTokens.verifyToken({ token });
return { data: verifiedToken, tokenType: TokenType.M2MToken, errors: undefined };
} catch (err: any) {
return handleClerkAPIError(TokenType.M2MToken, err, 'Machine token not found');
Expand DownExpand Up@@ -247,7 +247,7 @@ async function verifyAPIKey(
*/
export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyMachineToken(token, options);
return verifyM2MToken(token, options);
}
Comment on lines 248 to 251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit return type to the public API

Public APIs should declare explicit return types. Make verifyMachineAuthToken’s return type a union over supported token data.

Apply this diff:

-export async function verifyMachineAuthToken(token: string, options: VerifyTokenOptions) {+export async function verifyMachineAuthToken(+ token: string,+ options: VerifyTokenOptions,+): Promise<MachineTokenReturnType<M2MToken | IdPOAuthAccessToken | APIKey, MachineTokenVerificationError>> {
if (token.startsWith(M2M_TOKEN_PREFIX)) {
return verifyM2MToken(token, options);
📝 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
exportasyncfunctionverifyMachineAuthToken(token: string,options: VerifyTokenOptions){
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyMachineToken(token,options);
returnverifyM2MToken(token,options);
}
exportasyncfunctionverifyMachineAuthToken(
token: string,
options: VerifyTokenOptions,
): Promise<
MachineTokenReturnType<
M2MToken|IdPOAuthAccessToken|APIKey,
MachineTokenVerificationError
>
>{
if(token.startsWith(M2M_TOKEN_PREFIX)){
returnverifyM2MToken(token,options);
}
// ...rest of implementation...
}
🤖 Prompt for AI Agents
In packages/backend/src/tokens/verify.ts around lines 248 to 251, the exported
function verifyMachineAuthToken lacks an explicit return type; update its
signature to declare a Promise returning an explicit union of all supported
token data types (for example Promise<M2MTokenData | UserTokenData | null> or
whatever concrete types the underlying verify functions return), import or
reference those types at the top of the file, and update the function signature
to use that union so the public API has a concrete, exported return type.

if (token.startsWith(OAUTH_TOKEN_PREFIX)) {
return verifyOAuthToken(token, options);
Expand Down
Loading