Skip to content

Commit 0bdd0df

Browse files
authored
chore(backend): Add scoping and secret key retrieval to machines BAPI (#6417)
1 parent 232d7d3 commit 0bdd0df

File tree

9 files changed

+353
-2
lines changed

9 files changed

+353
-2
lines changed

.changeset/five-jokes-clap.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@clerk/backend": patch
3+
---
4+
5+
Adds scoping and secret key retrieval to machines BAPI methods:
6+
7+
```ts
8+
// Creates a new machine scope
9+
clerkClient.machines.createScope('machine_id', 'to_machine_id')
10+
11+
// Deletes a machine scope
12+
clerkClient.machines.deleteScope('machine_id', 'other_machine_id')
13+
14+
// Retrieve a secret key
15+
clerkClient.machines.getSecretKey('machine_id')
16+
```
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { server, validateHeaders } from '../../mock-server';
5+
import { createBackendApiClient } from '../factory';
6+
7+
describe('MachineAPI', () => {
8+
const apiClient = createBackendApiClient({
9+
apiUrl: 'https://api.clerk.test',
10+
secretKey: 'deadbeef',
11+
});
12+
13+
const machineId = 'machine_123';
14+
const otherMachineId = 'machine_456';
15+
16+
const mockSecondMachine = {
17+
object: 'machine',
18+
id: otherMachineId,
19+
name: 'Second Machine',
20+
instance_id: 'inst_456',
21+
created_at: 1640995200,
22+
updated_at: 1640995200,
23+
};
24+
25+
const mockMachine = {
26+
object: 'machine',
27+
id: machineId,
28+
name: 'Test Machine',
29+
instance_id: 'inst_123',
30+
created_at: 1640995200,
31+
updated_at: 1640995200,
32+
scoped_machines: [mockSecondMachine],
33+
};
34+
35+
const mockMachineScope = {
36+
object: 'machine_scope',
37+
from_machine_id: machineId,
38+
to_machine_id: otherMachineId,
39+
created_at: 1640995200,
40+
};
41+
42+
const mockMachineSecretKey = {
43+
secret: 'ak_test_...',
44+
};
45+
46+
const mockPaginatedResponse = {
47+
data: [mockMachine],
48+
total_count: 1,
49+
};
50+
51+
it('fetches a machine by ID', async () => {
52+
server.use(
53+
http.get(
54+
`https://api.clerk.test/v1/machines/${machineId}`,
55+
validateHeaders(() => {
56+
return HttpResponse.json(mockMachine);
57+
}),
58+
),
59+
);
60+
61+
const response = await apiClient.machines.get(machineId);
62+
63+
expect(response.id).toBe(machineId);
64+
expect(response.name).toBe('Test Machine');
65+
});
66+
67+
it('fetches machines list with query parameters', async () => {
68+
server.use(
69+
http.get(
70+
'https://api.clerk.test/v1/machines',
71+
validateHeaders(({ request }) => {
72+
const url = new URL(request.url);
73+
expect(url.searchParams.get('limit')).toBe('10');
74+
expect(url.searchParams.get('offset')).toBe('5');
75+
expect(url.searchParams.get('query')).toBe('test');
76+
return HttpResponse.json(mockPaginatedResponse);
77+
}),
78+
),
79+
);
80+
81+
const response = await apiClient.machines.list({
82+
limit: 10,
83+
offset: 5,
84+
query: 'test',
85+
});
86+
87+
expect(response.data).toHaveLength(1);
88+
expect(response.totalCount).toBe(1);
89+
});
90+
91+
it('creates a machine with scoped machines', async () => {
92+
const createParams = {
93+
name: 'New Machine',
94+
scoped_machines: [otherMachineId],
95+
default_token_ttl: 7200,
96+
};
97+
98+
server.use(
99+
http.post(
100+
'https://api.clerk.test/v1/machines',
101+
validateHeaders(async ({ request }) => {
102+
const body = await request.json();
103+
expect(body).toEqual(createParams);
104+
return HttpResponse.json(mockMachine);
105+
}),
106+
),
107+
);
108+
109+
const response = await apiClient.machines.create(createParams);
110+
111+
expect(response.id).toBe(machineId);
112+
expect(response.name).toBe('Test Machine');
113+
expect(response.scopedMachines).toHaveLength(1);
114+
expect(response.scopedMachines[0].id).toBe(otherMachineId);
115+
expect(response.scopedMachines[0].name).toBe('Second Machine');
116+
});
117+
118+
it('updates a machine with partial parameters', async () => {
119+
const updateParams = {
120+
machineId,
121+
name: 'Updated Machine',
122+
};
123+
124+
server.use(
125+
http.patch(
126+
`https://api.clerk.test/v1/machines/${machineId}`,
127+
validateHeaders(async ({ request }) => {
128+
const body = await request.json();
129+
expect(body).toEqual({ name: 'Updated Machine' });
130+
return HttpResponse.json(mockMachine);
131+
}),
132+
),
133+
);
134+
135+
const response = await apiClient.machines.update(updateParams);
136+
137+
expect(response.id).toBe(machineId);
138+
expect(response.name).toBe('Test Machine');
139+
});
140+
141+
it('deletes a machine', async () => {
142+
server.use(
143+
http.delete(
144+
`https://api.clerk.test/v1/machines/${machineId}`,
145+
validateHeaders(() => {
146+
return HttpResponse.json(mockMachine);
147+
}),
148+
),
149+
);
150+
151+
const response = await apiClient.machines.delete(machineId);
152+
153+
expect(response.id).toBe(machineId);
154+
});
155+
156+
it('fetches machine secret key', async () => {
157+
server.use(
158+
http.get(
159+
`https://api.clerk.test/v1/machines/${machineId}/secret_key`,
160+
validateHeaders(() => {
161+
return HttpResponse.json(mockMachineSecretKey);
162+
}),
163+
),
164+
);
165+
166+
const response = await apiClient.machines.getSecretKey(machineId);
167+
168+
expect(response.secret).toBe('ak_test_...');
169+
});
170+
171+
it('creates a machine scope', async () => {
172+
server.use(
173+
http.post(
174+
`https://api.clerk.test/v1/machines/${machineId}/scopes`,
175+
validateHeaders(async ({ request }) => {
176+
const body = await request.json();
177+
expect(body).toEqual({ to_machine_id: otherMachineId });
178+
return HttpResponse.json(mockMachineScope);
179+
}),
180+
),
181+
);
182+
183+
const response = await apiClient.machines.createScope(machineId, otherMachineId);
184+
185+
expect(response.fromMachineId).toBe(machineId);
186+
expect(response.toMachineId).toBe(otherMachineId);
187+
});
188+
189+
it('deletes a machine scope', async () => {
190+
server.use(
191+
http.delete(
192+
`https://api.clerk.test/v1/machines/${machineId}/scopes/${otherMachineId}`,
193+
validateHeaders(() => {
194+
return HttpResponse.json(mockMachineScope);
195+
}),
196+
),
197+
);
198+
199+
const response = await apiClient.machines.deleteScope(machineId, otherMachineId);
200+
201+
expect(response.fromMachineId).toBe(machineId);
202+
expect(response.toMachineId).toBe(otherMachineId);
203+
});
204+
});

packages/backend/src/api/endpoints/MachineApi.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,40 @@
11
import { joinPaths } from '../../util/path';
22
import type { PaginatedResourceResponse } from '../resources/Deserializer';
33
import type { Machine } from '../resources/Machine';
4+
import type { MachineScope } from '../resources/MachineScope';
5+
import type { MachineSecretKey } from '../resources/MachineSecretKey';
46
import { AbstractAPI } from './AbstractApi';
57

68
const basePath = '/machines';
79

810
type CreateMachineParams = {
11+
/**
12+
* The name of the machine.
13+
*/
914
name: string;
15+
/**
16+
* Array of machine IDs that this machine will have access to.
17+
*/
18+
scopedMachines?: string[];
19+
/**
20+
* The default time-to-live (TTL) in seconds for tokens created by this machine.
21+
*/
22+
defaultTokenTtl?: number;
1023
};
1124

1225
type UpdateMachineParams = {
26+
/**
27+
* The ID of the machine to update.
28+
*/
1329
machineId: string;
14-
name: string;
30+
/**
31+
* The name of the machine.
32+
*/
33+
name?: string;
34+
/**
35+
* The default time-to-live (TTL) in seconds for tokens created by this machine.
36+
*/
37+
defaultTokenTtl?: number;
1538
};
1639

1740
type GetMachineListParams = {
@@ -62,4 +85,43 @@ export class MachineApi extends AbstractAPI {
6285
path: joinPaths(basePath, machineId),
6386
});
6487
}
88+
89+
async getSecretKey(machineId: string) {
90+
this.requireId(machineId);
91+
return this.request<MachineSecretKey>({
92+
method: 'GET',
93+
path: joinPaths(basePath, machineId, 'secret_key'),
94+
});
95+
}
96+
97+
/**
98+
* Creates a new machine scope, allowing the specified machine to access another machine.
99+
*
100+
* @param machineId - The ID of the machine that will have access to another machine.
101+
* @param toMachineId - The ID of the machine that will be scoped to the current machine.
102+
*/
103+
async createScope(machineId: string, toMachineId: string) {
104+
this.requireId(machineId);
105+
return this.request<MachineScope>({
106+
method: 'POST',
107+
path: joinPaths(basePath, machineId, 'scopes'),
108+
bodyParams: {
109+
toMachineId,
110+
},
111+
});
112+
}
113+
114+
/**
115+
* Deletes a machine scope, removing access from one machine to another.
116+
*
117+
* @param machineId - The ID of the machine that has access to another machine.
118+
* @param otherMachineId - The ID of the machine that is being accessed.
119+
*/
120+
async deleteScope(machineId: string, otherMachineId: string) {
121+
this.requireId(machineId);
122+
return this.request<MachineScope>({
123+
method: 'DELETE',
124+
path: joinPaths(basePath, machineId, 'scopes', otherMachineId),
125+
});
126+
}
65127
}

packages/backend/src/api/resources/Deserializer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
Invitation,
1717
JwtTemplate,
1818
Machine,
19+
MachineScope,
20+
MachineSecretKey,
1921
MachineToken,
2022
OauthAccessToken,
2123
OAuthApplication,
@@ -135,6 +137,10 @@ function jsonToObject(item: any): any {
135137
return JwtTemplate.fromJSON(item);
136138
case ObjectType.Machine:
137139
return Machine.fromJSON(item);
140+
case ObjectType.MachineScope:
141+
return MachineScope.fromJSON(item);
142+
case ObjectType.MachineSecretKey:
143+
return MachineSecretKey.fromJSON(item);
138144
case ObjectType.MachineToken:
139145
return MachineToken.fromJSON(item);
140146
case ObjectType.OauthAccessToken:

packages/backend/src/api/resources/JSON.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const ObjectType = {
3535
InstanceSettings: 'instance_settings',
3636
Invitation: 'invitation',
3737
Machine: 'machine',
38+
MachineScope: 'machine_scope',
39+
MachineSecretKey: 'machine_secret_key',
3840
MachineToken: 'machine_to_machine_token',
3941
JwtTemplate: 'jwt_template',
4042
OauthAccessToken: 'oauth_access_token',
@@ -710,6 +712,21 @@ export interface MachineJSON extends ClerkResourceJSON {
710712
instance_id: string;
711713
created_at: number;
712714
updated_at: number;
715+
default_token_ttl: number;
716+
scoped_machines: MachineJSON[];
717+
}
718+
719+
export interface MachineScopeJSON {
720+
object: typeof ObjectType.MachineScope;
721+
from_machine_id: string;
722+
to_machine_id: string;
723+
created_at?: number;
724+
deleted?: boolean;
725+
}
726+
727+
export interface MachineSecretKeyJSON {
728+
object: typeof ObjectType.MachineSecretKey;
729+
secret: string;
713730
}
714731

715732
export interface MachineTokenJSON extends ClerkResourceJSON {

packages/backend/src/api/resources/Machine.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,30 @@ export class Machine {
77
readonly instanceId: string,
88
readonly createdAt: number,
99
readonly updatedAt: number,
10+
readonly scopedMachines: Machine[],
11+
readonly defaultTokenTtl: number,
1012
) {}
1113

1214
static fromJSON(data: MachineJSON): Machine {
13-
return new Machine(data.id, data.name, data.instance_id, data.created_at, data.updated_at);
15+
return new Machine(
16+
data.id,
17+
data.name,
18+
data.instance_id,
19+
data.created_at,
20+
data.updated_at,
21+
data.scoped_machines.map(
22+
m =>
23+
new Machine(
24+
m.id,
25+
m.name,
26+
m.instance_id,
27+
m.created_at,
28+
m.updated_at,
29+
[], // Nested machines don't have scoped_machines
30+
m.default_token_ttl,
31+
),
32+
),
33+
data.default_token_ttl,
34+
);
1435
}
1536
}

0 commit comments

Comments
 (0)