@shipfox/client-runners 5.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +49 -0
  3. package/dist/components/create-manual-registration-token-form.d.ts +3 -3
  4. package/dist/components/create-manual-registration-token-form.d.ts.map +1 -1
  5. package/dist/components/create-manual-registration-token-form.js +8 -23
  6. package/dist/components/create-manual-registration-token-form.js.map +1 -1
  7. package/dist/components/create-provisioner-token-form.d.ts +3 -3
  8. package/dist/components/create-provisioner-token-form.d.ts.map +1 -1
  9. package/dist/components/create-provisioner-token-form.js +8 -23
  10. package/dist/components/create-provisioner-token-form.js.map +1 -1
  11. package/dist/components/manual-registration-token-errors.d.ts.map +1 -1
  12. package/dist/components/manual-registration-token-errors.js +0 -1
  13. package/dist/components/manual-registration-token-errors.js.map +1 -1
  14. package/dist/components/manual-registration-token-format.d.ts +0 -2
  15. package/dist/components/manual-registration-token-format.d.ts.map +1 -1
  16. package/dist/components/manual-registration-token-format.js +0 -3
  17. package/dist/components/manual-registration-token-format.js.map +1 -1
  18. package/dist/components/manual-registration-token-list.d.ts +2 -2
  19. package/dist/components/manual-registration-token-list.d.ts.map +1 -1
  20. package/dist/components/manual-registration-token-list.js +12 -19
  21. package/dist/components/manual-registration-token-list.js.map +1 -1
  22. package/dist/components/manual-registration-tokens-settings-section.d.ts.map +1 -1
  23. package/dist/components/manual-registration-tokens-settings-section.js +1 -1
  24. package/dist/components/manual-registration-tokens-settings-section.js.map +1 -1
  25. package/dist/components/provisioner-token-errors.d.ts.map +1 -1
  26. package/dist/components/provisioner-token-errors.js +0 -1
  27. package/dist/components/provisioner-token-errors.js.map +1 -1
  28. package/dist/components/provisioner-token-format.d.ts +0 -18
  29. package/dist/components/provisioner-token-format.d.ts.map +1 -1
  30. package/dist/components/provisioner-token-format.js +0 -25
  31. package/dist/components/provisioner-token-format.js.map +1 -1
  32. package/dist/components/provisioner-token-list.d.ts +2 -2
  33. package/dist/components/provisioner-token-list.d.ts.map +1 -1
  34. package/dist/components/provisioner-token-list.js +9 -19
  35. package/dist/components/provisioner-token-list.js.map +1 -1
  36. package/dist/components/provisioner-tokens-settings-section.js +2 -2
  37. package/dist/components/provisioner-tokens-settings-section.js.map +1 -1
  38. package/dist/components/token-expiration-select.d.ts +7 -0
  39. package/dist/components/token-expiration-select.d.ts.map +1 -1
  40. package/dist/components/token-expiration-select.js +8 -0
  41. package/dist/components/token-expiration-select.js.map +1 -1
  42. package/dist/components/token-settings-sections.stories.d.ts.map +1 -1
  43. package/dist/components/token-settings-sections.stories.js +66 -23
  44. package/dist/components/token-settings-sections.stories.js.map +1 -1
  45. package/dist/core/token.d.ts +66 -0
  46. package/dist/core/token.d.ts.map +1 -0
  47. package/dist/core/token.js +37 -0
  48. package/dist/core/token.js.map +1 -0
  49. package/dist/feature.d.ts +13 -0
  50. package/dist/feature.d.ts.map +1 -1
  51. package/dist/feature.js +17 -16
  52. package/dist/feature.js.map +1 -1
  53. package/dist/hooks/api/manual-registration-tokens.d.ts +13 -71
  54. package/dist/hooks/api/manual-registration-tokens.d.ts.map +1 -1
  55. package/dist/hooks/api/manual-registration-tokens.js +40 -19
  56. package/dist/hooks/api/manual-registration-tokens.js.map +1 -1
  57. package/dist/hooks/api/provisioner-tokens.d.ts +17 -115
  58. package/dist/hooks/api/provisioner-tokens.d.ts.map +1 -1
  59. package/dist/hooks/api/provisioner-tokens.js +59 -32
  60. package/dist/hooks/api/provisioner-tokens.js.map +1 -1
  61. package/dist/hooks/api/token-mapper.d.ts +12 -0
  62. package/dist/hooks/api/token-mapper.d.ts.map +1 -0
  63. package/dist/hooks/api/token-mapper.js +61 -0
  64. package/dist/hooks/api/token-mapper.js.map +1 -0
  65. package/dist/tsconfig.test.tsbuildinfo +1 -1
  66. package/package.json +9 -9
  67. package/src/components/create-manual-registration-token-form.tsx +11 -21
  68. package/src/components/create-provisioner-token-form.tsx +11 -21
  69. package/src/components/form-errors.test.ts +2 -2
  70. package/src/components/manual-registration-token-errors.ts +0 -1
  71. package/src/components/manual-registration-token-format.test.ts +4 -4
  72. package/src/components/manual-registration-token-format.ts +0 -7
  73. package/src/components/manual-registration-token-list.tsx +13 -22
  74. package/src/components/manual-registration-tokens-settings-section.tsx +3 -5
  75. package/src/components/provisioner-token-errors.ts +0 -1
  76. package/src/components/provisioner-token-form-errors.test.ts +2 -2
  77. package/src/components/provisioner-token-format.test.ts +15 -14
  78. package/src/components/provisioner-token-format.ts +0 -29
  79. package/src/components/provisioner-token-list.tsx +14 -23
  80. package/src/components/provisioner-tokens-settings-section.tsx +4 -4
  81. package/src/components/token-expiration-select.test.ts +15 -0
  82. package/src/components/token-expiration-select.tsx +6 -0
  83. package/src/components/token-settings-sections.stories.tsx +72 -23
  84. package/src/core/token.test.ts +30 -0
  85. package/src/core/token.ts +83 -0
  86. package/src/feature.ts +19 -17
  87. package/src/hooks/api/manual-registration-tokens.test.ts +8 -5
  88. package/src/hooks/api/manual-registration-tokens.ts +71 -24
  89. package/src/hooks/api/provisioner-tokens.test.ts +10 -7
  90. package/src/hooks/api/provisioner-tokens.ts +100 -36
  91. package/src/hooks/api/token-mapper.test.ts +161 -0
  92. package/src/hooks/api/token-mapper.ts +79 -0
  93. package/tsconfig.build.tsbuildinfo +1 -1
  94. package/vercel.json +5 -0
@@ -26,6 +26,12 @@ export const expirationOptions: Array<{value: TokenExpirationOption; label: stri
26
26
  {value: 'never', label: 'Never'},
27
27
  ];
28
28
 
29
+ export function expirationCommand(expiration: TokenExpirationOption) {
30
+ return expiration === 'never'
31
+ ? ({kind: 'never'} as const)
32
+ : ({kind: 'expires-after', seconds: Number(expiration)} as const);
33
+ }
34
+
29
35
  export function expirationHint(expiration: TokenExpirationOption): string {
30
36
  if (expiration === 'never') return 'Token will not expire.';
31
37
  const expiresAt = new Date(Date.now() + Number(expiration) * 1000);
@@ -7,6 +7,11 @@ import type {Decorator, Meta, StoryObj} from '@storybook/react';
7
7
  import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
8
8
  import type {ReactNode} from 'react';
9
9
  import {within} from 'storybook/test';
10
+ import type {
11
+ CreatedProvisionerToken,
12
+ ManualRegistrationToken,
13
+ ProvisionerToken,
14
+ } from '#core/token.js';
10
15
  import {CreatedManualRegistrationTokenPanel} from './create-manual-registration-token-form.js';
11
16
  import {CreatedProvisionerTokenPanel} from './create-provisioner-token-form.js';
12
17
  import {
@@ -115,20 +120,20 @@ export const Statuses: Story = {
115
120
  <ProvisionerTokenList
116
121
  workspaceId={WORKSPACE_ID}
117
122
  tokens={[
118
- provisionerToken({
123
+ provisionerTokenModel({
119
124
  id: '33333333-3333-4333-8333-333333333333',
120
125
  name: 'Docker autoscaler',
121
- last_seen_at: NOW,
126
+ lastSeenAt: NOW,
122
127
  }),
123
- provisionerToken({
128
+ provisionerTokenModel({
124
129
  id: '44444444-4444-4444-8444-444444444444',
125
130
  name: 'Kubernetes spot pool',
126
- last_seen_at: EIGHT_MINUTES_AGO,
131
+ lastSeenAt: EIGHT_MINUTES_AGO,
127
132
  }),
128
- provisionerToken({
133
+ provisionerTokenModel({
129
134
  id: '55555555-5555-4555-8555-555555555555',
130
135
  name: 'Buildkite migration pool',
131
- last_seen_at: null,
136
+ lastSeenAt: null,
132
137
  }),
133
138
  ]}
134
139
  activeIds={new Set(['33333333-3333-4333-8333-333333333333'])}
@@ -151,18 +156,18 @@ export const Content: Story = {
151
156
  <ManualRegistrationTokenList
152
157
  workspaceId={WORKSPACE_ID}
153
158
  tokens={[
154
- manualToken({name: 'Deploy runner', prefix: 'sf_mrt_deploy'}),
155
- manualToken({
159
+ manualRegistrationToken({name: 'Deploy runner', prefix: 'sf_mrt_deploy'}),
160
+ manualRegistrationToken({
156
161
  id: '66666666-6666-4666-8666-666666666666',
157
162
  name: 'macOS build host',
158
163
  prefix: 'sf_mrt_macos',
159
- expires_at: null,
164
+ expiresAt: null,
160
165
  }),
161
166
  ]}
162
167
  />
163
168
  <ProvisionerTokenList
164
169
  workspaceId={WORKSPACE_ID}
165
- tokens={provisionerTokens()}
170
+ tokens={provisionerTokenModels()}
166
171
  activeIds={new Set(['33333333-3333-4333-8333-333333333333'])}
167
172
  />
168
173
  </div>
@@ -172,7 +177,7 @@ export const Content: Story = {
172
177
  <ManualRegistrationTokenList
173
178
  workspaceId={WORKSPACE_ID}
174
179
  tokens={[
175
- manualToken({
180
+ manualRegistrationToken({
176
181
  name: 'self-hosted-runner-for-production-release-candidate-validation-on-metal',
177
182
  prefix: 'sf_mrt_release_candidate_validation',
178
183
  }),
@@ -181,10 +186,10 @@ export const Content: Story = {
181
186
  <ProvisionerTokenList
182
187
  workspaceId={WORKSPACE_ID}
183
188
  tokens={[
184
- provisionerToken({
189
+ provisionerTokenModel({
185
190
  name: 'docker-provisioner-for-west-coast-gpu-burst-capacity-and-fallback',
186
191
  prefix: 'sf_pt_west_coast_gpu_burst',
187
- last_seen_at: EIGHT_MINUTES_AGO,
192
+ lastSeenAt: EIGHT_MINUTES_AGO,
188
193
  }),
189
194
  ]}
190
195
  activeIds={new Set()}
@@ -202,22 +207,17 @@ export const CreatedTokenPanels: Story = {
202
207
  <CreatedManualRegistrationTokenPanel
203
208
  token={{
204
209
  id: '77777777-7777-4777-8777-777777777777',
205
- raw_token: 'sf_mrt_4nJvNrs23ExampleManualTokenValue',
210
+ token: 'sf_mrt_4nJvNrs23ExampleManualTokenValue',
206
211
  prefix: 'sf_mrt_4nJv',
207
212
  name: 'Deploy runner',
208
- workspace_id: WORKSPACE_ID,
209
- expires_at: EXPIRES_AT,
210
- created_at: CREATED_AT,
213
+ workspaceId: WORKSPACE_ID,
214
+ expiresAt: EXPIRES_AT,
215
+ createdAt: CREATED_AT,
211
216
  }}
212
217
  />
213
218
  </StateExample>
214
219
  <StateExample label="Provisioner token reveal">
215
- <CreatedProvisionerTokenPanel
216
- token={{
217
- ...provisionerToken({name: 'Docker autoscaler'}),
218
- raw_token: 'sf_pt_Rv8YwrExampleProvisionerTokenValue',
219
- }}
220
- />
220
+ <CreatedProvisionerTokenPanel token={createdProvisionerToken()} />
221
221
  </StateExample>
222
222
  </StorySurface>
223
223
  ),
@@ -308,6 +308,55 @@ function provisionerTokens(): ProvisionerTokenDto[] {
308
308
  ];
309
309
  }
310
310
 
311
+ function manualRegistrationToken(
312
+ overrides: Partial<ManualRegistrationToken> = {},
313
+ ): ManualRegistrationToken {
314
+ return {
315
+ id: '22222222-2222-4222-8222-222222222222',
316
+ workspaceId: WORKSPACE_ID,
317
+ prefix: 'sf_mrt_deploy',
318
+ name: 'Deploy runner',
319
+ expiresAt: EXPIRES_AT,
320
+ revokedAt: null,
321
+ createdAt: CREATED_AT,
322
+ updatedAt: CREATED_AT,
323
+ ...overrides,
324
+ };
325
+ }
326
+
327
+ function provisionerTokenModel(overrides: Partial<ProvisionerToken> = {}): ProvisionerToken {
328
+ return {
329
+ ...manualRegistrationToken(),
330
+ createdByUserId: '99999999-9999-4999-8999-999999999999',
331
+ revokedByUserId: null,
332
+ lastSeenAt: NOW,
333
+ ...overrides,
334
+ };
335
+ }
336
+
337
+ function provisionerTokenModels(): ProvisionerToken[] {
338
+ return [
339
+ provisionerTokenModel({id: '33333333-3333-4333-8333-333333333333', name: 'Docker autoscaler'}),
340
+ provisionerTokenModel({
341
+ id: '44444444-4444-4444-8444-444444444444',
342
+ name: 'Kubernetes spot pool',
343
+ lastSeenAt: EIGHT_MINUTES_AGO,
344
+ }),
345
+ provisionerTokenModel({
346
+ id: '55555555-5555-4555-8555-555555555555',
347
+ name: 'Buildkite migration pool',
348
+ lastSeenAt: null,
349
+ }),
350
+ ];
351
+ }
352
+
353
+ function createdProvisionerToken(): CreatedProvisionerToken {
354
+ return {
355
+ ...provisionerTokenModel({name: 'Docker autoscaler'}),
356
+ token: 'sf_pt_Rv8YwrExampleProvisionerTokenValue',
357
+ };
358
+ }
359
+
311
360
  function manualToken(
312
361
  overrides: Partial<ManualRegistrationTokenDto> = {},
313
362
  ): ManualRegistrationTokenDto {
@@ -0,0 +1,30 @@
1
+ import {
2
+ createTokenCommand,
3
+ provisionerConnectionStatus,
4
+ provisionerTokenDisplayName,
5
+ } from './token.js';
6
+
7
+ describe('token domain policies', () => {
8
+ test('omits a blank token name while keeping its expiration policy', () => {
9
+ const result = createTokenCommand(' ', {kind: 'expires-after', seconds: 86_400});
10
+
11
+ expect(result).toEqual({expiration: {kind: 'expires-after', seconds: 86_400}});
12
+ });
13
+
14
+ test('derives provisioner status from active and last-seen state', () => {
15
+ const token = {id: 'token-1', name: null, lastSeenAt: '2026-07-22T10:00:00.000Z'};
16
+
17
+ expect(provisionerConnectionStatus(token, new Set(['token-1']))).toEqual({
18
+ kind: 'connected',
19
+ dotVariant: 'success',
20
+ label: 'Connected',
21
+ });
22
+ expect(provisionerConnectionStatus(token, new Set())).toEqual({
23
+ kind: 'last-seen',
24
+ dotVariant: 'neutral',
25
+ label: 'Last seen',
26
+ lastSeenAt: token.lastSeenAt,
27
+ });
28
+ expect(provisionerTokenDisplayName(token)).toBe('Unnamed provisioner');
29
+ });
30
+ });
@@ -0,0 +1,83 @@
1
+ export interface ManualRegistrationToken {
2
+ id: string;
3
+ workspaceId: string;
4
+ prefix: string;
5
+ name: string | null;
6
+ expiresAt: string | null;
7
+ revokedAt: string | null;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+
12
+ export interface ProvisionerToken extends ManualRegistrationToken {
13
+ createdByUserId: string;
14
+ revokedByUserId: string | null;
15
+ lastSeenAt: string | null;
16
+ }
17
+
18
+ export interface ActiveProvisioner {
19
+ id: string;
20
+ name: string | null;
21
+ prefix: string;
22
+ lastSeenAt: string;
23
+ }
24
+
25
+ export interface CreatedManualRegistrationToken {
26
+ token: string;
27
+ id: string;
28
+ prefix: string;
29
+ name: string | null;
30
+ workspaceId: string;
31
+ expiresAt: string | null;
32
+ createdAt: string;
33
+ }
34
+
35
+ export interface CreatedProvisionerToken extends CreatedManualRegistrationToken {
36
+ createdByUserId: string;
37
+ revokedByUserId: string | null;
38
+ revokedAt: string | null;
39
+ lastSeenAt: string | null;
40
+ updatedAt: string;
41
+ }
42
+
43
+ export type TokenExpiration = {kind: 'never'} | {kind: 'expires-after'; seconds: number};
44
+
45
+ export interface CreateTokenCommand {
46
+ name?: string;
47
+ expiration: TokenExpiration;
48
+ }
49
+
50
+ export type ProvisionerConnectionStatus =
51
+ | {kind: 'connected'; dotVariant: 'success'; label: 'Connected'}
52
+ | {kind: 'last-seen'; dotVariant: 'neutral'; label: 'Last seen'; lastSeenAt: string}
53
+ | {kind: 'never-connected'; dotVariant: 'neutral'; label: 'Never connected'};
54
+
55
+ export function createTokenCommand(name: string, expiration: TokenExpiration): CreateTokenCommand {
56
+ const trimmedName = name.trim();
57
+ return {expiration, ...(trimmedName ? {name: trimmedName} : {})};
58
+ }
59
+
60
+ export function tokenDisplayName(token: Pick<ManualRegistrationToken, 'name'>): string {
61
+ return token.name || 'Unnamed token';
62
+ }
63
+
64
+ export function provisionerTokenDisplayName(token: Pick<ProvisionerToken, 'name'>): string {
65
+ return token.name || 'Unnamed provisioner';
66
+ }
67
+
68
+ export function provisionerConnectionStatus(
69
+ token: Pick<ProvisionerToken, 'id' | 'lastSeenAt'>,
70
+ activeIds: ReadonlySet<string>,
71
+ ): ProvisionerConnectionStatus {
72
+ if (activeIds.has(token.id))
73
+ return {kind: 'connected', dotVariant: 'success', label: 'Connected'};
74
+ if (token.lastSeenAt) {
75
+ return {
76
+ kind: 'last-seen',
77
+ dotVariant: 'neutral',
78
+ label: 'Last seen',
79
+ lastSeenAt: token.lastSeenAt,
80
+ };
81
+ }
82
+ return {kind: 'never-connected', dotVariant: 'neutral', label: 'Never connected'};
83
+ }
package/src/feature.ts CHANGED
@@ -1,4 +1,21 @@
1
- import {defineClientFeature} from '@shipfox/client-shell';
1
+ import {defineClientFeature, type SettingsSectionEntry} from '@shipfox/client-shell';
2
+
3
+ export const runnersSettingsSections = [
4
+ {
5
+ id: 'settings.runners',
6
+ pathSegment: 'runners',
7
+ label: 'Runners',
8
+ icon: 'settings3Line',
9
+ order: 200,
10
+ },
11
+ {
12
+ id: 'settings.provisioners',
13
+ pathSegment: 'provisioners',
14
+ label: 'Runner provisioners',
15
+ icon: 'serverLine',
16
+ order: 300,
17
+ },
18
+ ] as const satisfies readonly SettingsSectionEntry[];
2
19
 
3
20
  export const runnersFeature = defineClientFeature({
4
21
  id: 'shipfox.runners',
@@ -14,20 +31,5 @@ export const runnersFeature = defineClientFeature({
14
31
  impl: '@shipfox/client-runners/routes/provisioners-settings',
15
32
  },
16
33
  ],
17
- settingsSections: [
18
- {
19
- id: 'settings.runners',
20
- pathSegment: 'runners',
21
- label: 'Runners',
22
- icon: 'settings3Line',
23
- order: 200,
24
- },
25
- {
26
- id: 'settings.provisioners',
27
- pathSegment: 'provisioners',
28
- label: 'Runner provisioners',
29
- icon: 'serverLine',
30
- order: 300,
31
- },
32
- ],
34
+ settingsSections: runnersSettingsSections,
33
35
  });
@@ -35,20 +35,23 @@ describe('createManualRegistrationToken', () => {
35
35
  );
36
36
  });
37
37
  configureApiClient({fetchImpl});
38
- const body = {name: 'Local runner', ttl_seconds: 86_400};
38
+ const command = {
39
+ name: 'Local runner',
40
+ expiration: {kind: 'expires-after' as const, seconds: 86_400},
41
+ };
39
42
 
40
43
  const result = await createManualRegistrationToken({
41
44
  workspaceId: '11111111-1111-4111-8111-111111111111',
42
- body,
45
+ command,
43
46
  });
44
47
 
45
48
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
46
- expect(result.raw_token).toBe('sf_mrt_raw-created-token');
49
+ expect(result.token).toBe('sf_mrt_raw-created-token');
47
50
  expect(request.url).toBe(
48
51
  'https://api.example.test/workspaces/11111111-1111-4111-8111-111111111111/runners/manual-registration-tokens',
49
52
  );
50
53
  expect(request.method).toBe('POST');
51
- expect(requestBody).toEqual(body);
54
+ expect(requestBody).toEqual({name: 'Local runner', ttl_seconds: 86_400});
52
55
  });
53
56
  });
54
57
 
@@ -78,7 +81,7 @@ describe('revokeManualRegistrationToken', () => {
78
81
  });
79
82
 
80
83
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
81
- expect(result.revoked_at).toBe('2026-05-08T01:00:00.000Z');
84
+ expect(result.revokedAt).toBe('2026-05-08T01:00:00.000Z');
82
85
  expect(request.url).toBe(
83
86
  'https://api.example.test/workspaces/11111111-1111-4111-8111-111111111111/runners/manual-registration-tokens/33333333-3333-4333-8333-333333333333/revoke',
84
87
  );
@@ -1,11 +1,26 @@
1
- import type {
2
- CreateManualRegistrationTokenBodyDto,
3
- CreateManualRegistrationTokenResponseDto,
4
- ListManualRegistrationTokensResponseDto,
5
- RevokeManualRegistrationTokenResponseDto,
1
+ import {
2
+ createManualRegistrationTokenResponseSchema,
3
+ listManualRegistrationTokensResponseSchema,
4
+ revokeManualRegistrationTokenResponseSchema,
6
5
  } from '@shipfox/api-runners-dto';
7
- import {apiRequest} from '@shipfox/client-api';
8
- import {useMutation, useQuery} from '@tanstack/react-query';
6
+ import {checkedApiRequest} from '@shipfox/client-api';
7
+ import {
8
+ type FetchQueryOptions,
9
+ queryOptions,
10
+ useMutation,
11
+ useQuery,
12
+ useQueryClient,
13
+ } from '@tanstack/react-query';
14
+ import type {
15
+ CreatedManualRegistrationToken,
16
+ CreateTokenCommand,
17
+ ManualRegistrationToken,
18
+ } from '#core/token.js';
19
+ import {
20
+ toCreatedManualRegistrationToken,
21
+ toCreateTokenBody,
22
+ toManualRegistrationToken,
23
+ } from './token-mapper.js';
9
24
 
10
25
  export const manualRegistrationTokenQueryKeys = {
11
26
  all: ['manual-registration-tokens'] as const,
@@ -13,30 +28,41 @@ export const manualRegistrationTokenQueryKeys = {
13
28
  [...manualRegistrationTokenQueryKeys.all, 'list', workspaceId] as const,
14
29
  };
15
30
 
31
+ type ManualRegistrationTokensQueryOptions = FetchQueryOptions<
32
+ ManualRegistrationToken[],
33
+ Error,
34
+ ManualRegistrationToken[],
35
+ ReturnType<typeof manualRegistrationTokenQueryKeys.list>
36
+ >;
37
+
16
38
  export async function listManualRegistrationTokens({
17
39
  workspaceId,
18
40
  signal,
19
41
  }: {
20
42
  workspaceId: string;
21
43
  signal?: AbortSignal;
22
- }) {
23
- return await apiRequest<ListManualRegistrationTokensResponseDto>(
44
+ }): Promise<ManualRegistrationToken[]> {
45
+ const response = await checkedApiRequest(
46
+ listManualRegistrationTokensResponseSchema,
24
47
  `/workspaces/${workspaceId}/runners/manual-registration-tokens`,
25
48
  {signal},
26
49
  );
50
+ return response.manual_registration_tokens.map(toManualRegistrationToken);
27
51
  }
28
52
 
29
53
  export async function createManualRegistrationToken({
30
54
  workspaceId,
31
- body,
55
+ command,
32
56
  }: {
33
57
  workspaceId: string;
34
- body: CreateManualRegistrationTokenBodyDto;
35
- }) {
36
- return await apiRequest<CreateManualRegistrationTokenResponseDto>(
58
+ command: CreateTokenCommand;
59
+ }): Promise<CreatedManualRegistrationToken> {
60
+ const response = await checkedApiRequest(
61
+ createManualRegistrationTokenResponseSchema,
37
62
  `/workspaces/${workspaceId}/runners/manual-registration-tokens`,
38
- {method: 'POST', body},
63
+ {method: 'POST', body: toCreateTokenBody(command)},
39
64
  );
65
+ return toCreatedManualRegistrationToken(response);
40
66
  }
41
67
 
42
68
  export async function revokeManualRegistrationToken({
@@ -45,27 +71,48 @@ export async function revokeManualRegistrationToken({
45
71
  }: {
46
72
  workspaceId: string;
47
73
  tokenId: string;
48
- }) {
49
- return await apiRequest<RevokeManualRegistrationTokenResponseDto>(
74
+ }): Promise<ManualRegistrationToken> {
75
+ const response = await checkedApiRequest(
76
+ revokeManualRegistrationTokenResponseSchema,
50
77
  `/workspaces/${workspaceId}/runners/manual-registration-tokens/${tokenId}/revoke`,
51
78
  {method: 'POST'},
52
79
  );
80
+ return toManualRegistrationToken(response);
81
+ }
82
+
83
+ export function manualRegistrationTokensQueryOptions(
84
+ workspaceId: string,
85
+ ): ManualRegistrationTokensQueryOptions {
86
+ return queryOptions({
87
+ queryKey: manualRegistrationTokenQueryKeys.list(workspaceId),
88
+ queryFn: ({signal}) => listManualRegistrationTokens({workspaceId, signal}),
89
+ });
53
90
  }
54
91
 
55
92
  export function useManualRegistrationTokensQuery(workspaceId: string | undefined) {
56
93
  return useQuery({
57
- queryKey: workspaceId
58
- ? manualRegistrationTokenQueryKeys.list(workspaceId)
59
- : [...manualRegistrationTokenQueryKeys.all, 'list'],
94
+ ...manualRegistrationTokensQueryOptions(workspaceId ?? ''),
60
95
  enabled: Boolean(workspaceId),
61
- queryFn: ({signal}) => listManualRegistrationTokens({workspaceId: workspaceId ?? '', signal}),
62
96
  });
63
97
  }
64
98
 
65
- export function useCreateManualRegistrationTokenMutation() {
66
- return useMutation({mutationFn: createManualRegistrationToken});
99
+ export function useCreateManualRegistrationTokenMutation(workspaceId: string) {
100
+ const queryClient = useQueryClient();
101
+ return useMutation({
102
+ mutationFn: (command: CreateTokenCommand) =>
103
+ createManualRegistrationToken({workspaceId, command}),
104
+ onSuccess: async () => {
105
+ await queryClient.invalidateQueries(manualRegistrationTokensQueryOptions(workspaceId));
106
+ },
107
+ });
67
108
  }
68
109
 
69
- export function useRevokeManualRegistrationTokenMutation() {
70
- return useMutation({mutationFn: revokeManualRegistrationToken});
110
+ export function useRevokeManualRegistrationTokenMutation(workspaceId: string) {
111
+ const queryClient = useQueryClient();
112
+ return useMutation({
113
+ mutationFn: (tokenId: string) => revokeManualRegistrationToken({workspaceId, tokenId}),
114
+ onSuccess: async () => {
115
+ await queryClient.invalidateQueries(manualRegistrationTokensQueryOptions(workspaceId));
116
+ },
117
+ });
71
118
  }
@@ -29,7 +29,7 @@ describe('provisioner token transports', () => {
29
29
  const result = await listProvisionerTokens({workspaceId});
30
30
 
31
31
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
32
- expect(result.tokens).toEqual([]);
32
+ expect(result).toEqual([]);
33
33
  expect(request.url).toBe(
34
34
  `https://api.example.test/workspaces/${workspaceId}/provisioners/tokens`,
35
35
  );
@@ -60,17 +60,20 @@ describe('provisioner token transports', () => {
60
60
  );
61
61
  });
62
62
  configureApiClient({fetchImpl});
63
- const body = {name: 'Docker provisioner', ttl_seconds: 86_400};
63
+ const command = {
64
+ name: 'Docker provisioner',
65
+ expiration: {kind: 'expires-after' as const, seconds: 86_400},
66
+ };
64
67
 
65
- const result = await createProvisionerToken({workspaceId, body});
68
+ const result = await createProvisionerToken({workspaceId, command});
66
69
 
67
70
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
68
- expect(result.raw_token).toBe('sf_pt_raw-created-token');
71
+ expect(result.token).toBe('sf_pt_raw-created-token');
69
72
  expect(request.url).toBe(
70
73
  `https://api.example.test/workspaces/${workspaceId}/provisioners/tokens`,
71
74
  );
72
75
  expect(request.method).toBe('POST');
73
- expect(requestBody).toEqual(body);
76
+ expect(requestBody).toEqual({name: 'Docker provisioner', ttl_seconds: 86_400});
74
77
  });
75
78
 
76
79
  test('posts to the token revoke endpoint', async () => {
@@ -95,7 +98,7 @@ describe('provisioner token transports', () => {
95
98
  const result = await revokeProvisionerToken({workspaceId, tokenId});
96
99
 
97
100
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
98
- expect(result.revoked_at).toBe('2026-05-08T01:00:00.000Z');
101
+ expect(result.revokedAt).toBe('2026-05-08T01:00:00.000Z');
99
102
  expect(request.url).toBe(
100
103
  `https://api.example.test/workspaces/${workspaceId}/provisioners/tokens/${tokenId}/revoke`,
101
104
  );
@@ -120,7 +123,7 @@ describe('provisioner token transports', () => {
120
123
  const result = await listActiveProvisioners({workspaceId});
121
124
 
122
125
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
123
- expect(result.provisioners).toHaveLength(1);
126
+ expect(result).toHaveLength(1);
124
127
  expect(request.url).toBe(
125
128
  `https://api.example.test/workspaces/${workspaceId}/provisioners/active`,
126
129
  );