@shipfox/client-runners 5.0.0 → 6.0.1

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 +38 -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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-runners",
3
3
  "license": "MIT",
4
- "version": "5.0.0",
4
+ "version": "6.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -10,9 +10,6 @@
10
10
  "type": "module",
11
11
  "main": "dist/index.js",
12
12
  "types": "dist/index.d.ts",
13
- "imports": {
14
- "#*": "./dist/*"
15
- },
16
13
  "exports": {
17
14
  ".": {
18
15
  "types": "./dist/index.d.ts",
@@ -30,17 +27,20 @@
30
27
  "dependencies": {
31
28
  "@swc/helpers": "^0.5.17",
32
29
  "@tanstack/react-form": "^1.32.0",
33
- "@shipfox/api-runners-dto": "7.0.1",
34
- "@shipfox/client-api": "4.0.0",
35
- "@shipfox/client-shell": "5.0.0",
36
- "@shipfox/client-ui": "4.0.0",
37
- "@shipfox/react-ui": "0.3.5"
30
+ "@shipfox/api-runners-dto": "9.0.1",
31
+ "@shipfox/client-api": "6.0.1",
32
+ "@shipfox/client-shell": "6.0.1",
33
+ "@shipfox/client-ui": "6.0.1",
34
+ "@shipfox/react-ui": "0.3.6"
38
35
  },
39
36
  "peerDependencies": {
40
37
  "@tanstack/react-query": "^5.101.0",
41
38
  "react": "^19.0.0",
42
39
  "react-dom": "^19.0.0"
43
40
  },
41
+ "imports": {
42
+ "#*": "./dist/*"
43
+ },
44
44
  "scripts": {
45
45
  "build": "shipfox-swc",
46
46
  "check": "shipfox-biome-check",
@@ -1,4 +1,3 @@
1
- import type {CreateManualRegistrationTokenResponseDto} from '@shipfox/api-runners-dto';
2
1
  import {Button} from '@shipfox/react-ui/button';
3
2
  import {Callout, CalloutContent, CalloutDescription, CalloutTitle} from '@shipfox/react-ui/callout';
4
3
  import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
@@ -6,15 +5,14 @@ import {useCopyToClipboard} from '@shipfox/react-ui/hooks';
6
5
  import {ModalBody, ModalFooter} from '@shipfox/react-ui/modal';
7
6
  import {Code, Text} from '@shipfox/react-ui/typography';
8
7
  import {useForm} from '@tanstack/react-form';
9
- import {useQueryClient} from '@tanstack/react-query';
10
8
  import {useState} from 'react';
11
- import {
12
- manualRegistrationTokenQueryKeys,
13
- useCreateManualRegistrationTokenMutation,
14
- } from '#hooks/api/manual-registration-tokens.js';
9
+ import type {CreatedManualRegistrationToken} from '#core/token.js';
10
+ import {createTokenCommand} from '#core/token.js';
11
+ import {useCreateManualRegistrationTokenMutation} from '#hooks/api/manual-registration-tokens.js';
15
12
  import {manualRegistrationTokenCreateErrorToFormError} from './form-errors.js';
16
13
  import {
17
14
  ExpirationSelect,
15
+ expirationCommand,
18
16
  expirationHint,
19
17
  type TokenExpirationOption,
20
18
  } from './token-expiration-select.js';
@@ -26,27 +24,19 @@ export function CreateManualRegistrationTokenForm({
26
24
  onCreated,
27
25
  }: {
28
26
  workspaceId: string;
29
- onCreated: (token: CreateManualRegistrationTokenResponseDto) => void;
27
+ onCreated: (token: CreatedManualRegistrationToken) => void;
30
28
  }) {
31
- const queryClient = useQueryClient();
32
- const createToken = useCreateManualRegistrationTokenMutation();
29
+ const createToken = useCreateManualRegistrationTokenMutation(workspaceId);
33
30
  const [formError, setFormError] = useState<string | undefined>();
34
31
 
35
32
  const form = useForm({
36
33
  defaultValues: {name: '', expiration: '86400' as TokenExpirationOption},
37
34
  onSubmit: async ({value}) => {
38
35
  setFormError(undefined);
39
- const trimmedName = value.name.trim();
40
- const body = {
41
- ...(trimmedName ? {name: trimmedName} : {}),
42
- ...(value.expiration === 'never' ? {} : {ttl_seconds: Number(value.expiration)}),
43
- };
36
+ const command = createTokenCommand(value.name, expirationCommand(value.expiration));
44
37
 
45
38
  try {
46
- const token = await createToken.mutateAsync({workspaceId, body});
47
- await queryClient.invalidateQueries({
48
- queryKey: manualRegistrationTokenQueryKeys.list(workspaceId),
49
- });
39
+ const token = await createToken.mutateAsync(command);
50
40
  onCreated(token);
51
41
  } catch (error) {
52
42
  const mapped = manualRegistrationTokenCreateErrorToFormError(error);
@@ -144,11 +134,11 @@ export function CreateManualRegistrationTokenForm({
144
134
  export function CreatedManualRegistrationTokenPanel({
145
135
  token,
146
136
  }: {
147
- token: CreateManualRegistrationTokenResponseDto;
137
+ token: CreatedManualRegistrationToken;
148
138
  }) {
149
139
  const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
150
140
  const {copy} = useCopyToClipboard({
151
- text: token.raw_token,
141
+ text: token.token,
152
142
  onCopy: () => {
153
143
  setCopyState('copied');
154
144
  window.setTimeout(() => setCopyState('idle'), 1500);
@@ -175,7 +165,7 @@ export function CreatedManualRegistrationTokenPanel({
175
165
  </div>
176
166
  <div className="flex items-center gap-8 max-[640px]:flex-col max-[640px]:items-stretch">
177
167
  <Code variant="paragraph" className="min-w-0 flex-1 break-all">
178
- {token.raw_token}
168
+ {token.token}
179
169
  </Code>
180
170
  <Button
181
171
  size="sm"
@@ -1,4 +1,3 @@
1
- import type {CreateProvisionerTokenResponseDto} from '@shipfox/api-runners-dto';
2
1
  import {Button} from '@shipfox/react-ui/button';
3
2
  import {Callout, CalloutContent, CalloutDescription, CalloutTitle} from '@shipfox/react-ui/callout';
4
3
  import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
@@ -6,15 +5,14 @@ import {useCopyToClipboard} from '@shipfox/react-ui/hooks';
6
5
  import {ModalBody, ModalFooter} from '@shipfox/react-ui/modal';
7
6
  import {Code, Text} from '@shipfox/react-ui/typography';
8
7
  import {useForm} from '@tanstack/react-form';
9
- import {useQueryClient} from '@tanstack/react-query';
10
8
  import {useState} from 'react';
11
- import {
12
- provisionerTokenQueryKeys,
13
- useCreateProvisionerTokenMutation,
14
- } from '#hooks/api/provisioner-tokens.js';
9
+ import type {CreatedProvisionerToken} from '#core/token.js';
10
+ import {createTokenCommand} from '#core/token.js';
11
+ import {useCreateProvisionerTokenMutation} from '#hooks/api/provisioner-tokens.js';
15
12
  import {provisionerTokenCreateErrorToFormError} from './provisioner-token-form-errors.js';
16
13
  import {
17
14
  ExpirationSelect,
15
+ expirationCommand,
18
16
  expirationHint,
19
17
  type TokenExpirationOption,
20
18
  } from './token-expiration-select.js';
@@ -26,27 +24,19 @@ export function CreateProvisionerTokenForm({
26
24
  onCreated,
27
25
  }: {
28
26
  workspaceId: string;
29
- onCreated: (token: CreateProvisionerTokenResponseDto) => void;
27
+ onCreated: (token: CreatedProvisionerToken) => void;
30
28
  }) {
31
- const queryClient = useQueryClient();
32
- const createToken = useCreateProvisionerTokenMutation();
29
+ const createToken = useCreateProvisionerTokenMutation(workspaceId);
33
30
  const [formError, setFormError] = useState<string | undefined>();
34
31
 
35
32
  const form = useForm({
36
33
  defaultValues: {name: '', expiration: '86400' as TokenExpirationOption},
37
34
  onSubmit: async ({value}) => {
38
35
  setFormError(undefined);
39
- const trimmedName = value.name.trim();
40
- const body = {
41
- ...(trimmedName ? {name: trimmedName} : {}),
42
- ...(value.expiration === 'never' ? {} : {ttl_seconds: Number(value.expiration)}),
43
- };
36
+ const command = createTokenCommand(value.name, expirationCommand(value.expiration));
44
37
 
45
38
  try {
46
- const token = await createToken.mutateAsync({workspaceId, body});
47
- await queryClient.invalidateQueries({
48
- queryKey: provisionerTokenQueryKeys.list(workspaceId),
49
- });
39
+ const token = await createToken.mutateAsync(command);
50
40
  onCreated(token);
51
41
  } catch (error) {
52
42
  const mapped = provisionerTokenCreateErrorToFormError(error);
@@ -141,10 +131,10 @@ export function CreateProvisionerTokenForm({
141
131
  );
142
132
  }
143
133
 
144
- export function CreatedProvisionerTokenPanel({token}: {token: CreateProvisionerTokenResponseDto}) {
134
+ export function CreatedProvisionerTokenPanel({token}: {token: CreatedProvisionerToken}) {
145
135
  const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
146
136
  const {copy} = useCopyToClipboard({
147
- text: token.raw_token,
137
+ text: token.token,
148
138
  onCopy: () => {
149
139
  setCopyState('copied');
150
140
  window.setTimeout(() => setCopyState('idle'), 1500);
@@ -171,7 +161,7 @@ export function CreatedProvisionerTokenPanel({token}: {token: CreateProvisionerT
171
161
  </div>
172
162
  <div className="flex items-center gap-8 max-[640px]:flex-col max-[640px]:items-stretch">
173
163
  <Code variant="paragraph" className="min-w-0 flex-1 break-all">
174
- {token.raw_token}
164
+ {token.token}
175
165
  </Code>
176
166
  <Button
177
167
  size="sm"
@@ -10,10 +10,10 @@ describe('manualRegistrationTokenCreateErrorToFormError', () => {
10
10
  expect(result).toEqual({kind: 'form', message: 'Try later'});
11
11
  });
12
12
 
13
- test('routes Error to a form-level alert with the error message', () => {
13
+ test('does not expose an unknown Error message', () => {
14
14
  const result = manualRegistrationTokenCreateErrorToFormError(new Error('boom'));
15
15
 
16
- expect(result).toEqual({kind: 'form', message: 'boom'});
16
+ expect(result).toEqual({kind: 'form', message: 'Something went wrong. Try again.'});
17
17
  });
18
18
 
19
19
  test('falls back to generic copy for non-Error throwables', () => {
@@ -8,6 +8,5 @@ export function manualRegistrationTokenErrorMessage(error: unknown): string {
8
8
  }
9
9
  return error.message;
10
10
  }
11
- if (error instanceof Error) return error.message;
12
11
  return 'Something went wrong. Try again.';
13
12
  }
@@ -1,18 +1,18 @@
1
+ import {tokenDisplayName} from '#core/token.js';
1
2
  import {
2
3
  formatManualRegistrationTokenDate,
3
4
  formatManualRegistrationTokenTimestamp,
4
- manualRegistrationTokenDisplayName,
5
5
  } from './manual-registration-token-format.js';
6
6
 
7
- describe('manualRegistrationTokenDisplayName', () => {
7
+ describe('tokenDisplayName', () => {
8
8
  test('uses the token name when present', () => {
9
- const result = manualRegistrationTokenDisplayName({name: 'Deploy runner'});
9
+ const result = tokenDisplayName({name: 'Deploy runner'});
10
10
 
11
11
  expect(result).toBe('Deploy runner');
12
12
  });
13
13
 
14
14
  test('falls back for unnamed tokens', () => {
15
- const result = manualRegistrationTokenDisplayName({name: null});
15
+ const result = tokenDisplayName({name: null});
16
16
 
17
17
  expect(result).toBe('Unnamed token');
18
18
  });
@@ -1,4 +1,3 @@
1
- import type {ManualRegistrationTokenDto} from '@shipfox/api-runners-dto';
2
1
  import {formatDate, formatTimestamp} from '@shipfox/react-ui/utils';
3
2
 
4
3
  export function formatManualRegistrationTokenDate(value: string | null): string {
@@ -10,9 +9,3 @@ export function formatManualRegistrationTokenTimestamp(value: string | null): st
10
9
  if (!value) return undefined;
11
10
  return formatTimestamp(value);
12
11
  }
13
-
14
- export function manualRegistrationTokenDisplayName(
15
- token: Pick<ManualRegistrationTokenDto, 'name'>,
16
- ): string {
17
- return token.name || 'Unnamed token';
18
- }
@@ -1,4 +1,3 @@
1
- import type {ManualRegistrationTokenDto} from '@shipfox/api-runners-dto';
2
1
  import {Button, IconButton} from '@shipfox/react-ui/button';
3
2
  import {Callout} from '@shipfox/react-ui/callout';
4
3
  import {
@@ -26,17 +25,13 @@ import {
26
25
  TableRow,
27
26
  } from '@shipfox/react-ui/table';
28
27
  import {Code, Text} from '@shipfox/react-ui/typography';
29
- import {useQueryClient} from '@tanstack/react-query';
30
28
  import {useState} from 'react';
31
- import {
32
- manualRegistrationTokenQueryKeys,
33
- useRevokeManualRegistrationTokenMutation,
34
- } from '#hooks/api/manual-registration-tokens.js';
29
+ import {type ManualRegistrationToken, tokenDisplayName} from '#core/token.js';
30
+ import {useRevokeManualRegistrationTokenMutation} from '#hooks/api/manual-registration-tokens.js';
35
31
  import {manualRegistrationTokenErrorMessage} from './manual-registration-token-errors.js';
36
32
  import {
37
33
  formatManualRegistrationTokenDate,
38
34
  formatManualRegistrationTokenTimestamp,
39
- manualRegistrationTokenDisplayName,
40
35
  } from './manual-registration-token-format.js';
41
36
  import {TokenDate} from './token-date.js';
42
37
  import {TokenName} from './token-name.js';
@@ -46,7 +41,7 @@ export function ManualRegistrationTokenList({
46
41
  tokens,
47
42
  }: {
48
43
  workspaceId: string;
49
- tokens: ManualRegistrationTokenDto[];
44
+ tokens: ManualRegistrationToken[];
50
45
  }) {
51
46
  return (
52
47
  <>
@@ -65,7 +60,7 @@ export function ManualRegistrationTokenList({
65
60
  {tokens.map((token) => (
66
61
  <TableRow key={token.id}>
67
62
  <TableCell>
68
- <TokenName name={manualRegistrationTokenDisplayName(token)} />
63
+ <TokenName name={tokenDisplayName(token)} />
69
64
  </TableCell>
70
65
  <TableCell>
71
66
  <Code variant="paragraph" className="block truncate">
@@ -73,10 +68,10 @@ export function ManualRegistrationTokenList({
73
68
  </Code>
74
69
  </TableCell>
75
70
  <TableCell>
76
- <ManualRegistrationTokenDate value={token.expires_at} />
71
+ <ManualRegistrationTokenDate value={token.expiresAt} />
77
72
  </TableCell>
78
73
  <TableCell>
79
- <ManualRegistrationTokenDate value={token.created_at} />
74
+ <ManualRegistrationTokenDate value={token.createdAt} />
80
75
  </TableCell>
81
76
  <TableCell className="text-right">
82
77
  <RevokeManualRegistrationTokenButton workspaceId={workspaceId} token={token} />
@@ -97,7 +92,7 @@ export function ManualRegistrationTokenList({
97
92
  >
98
93
  <div className="flex items-start justify-between gap-12">
99
94
  <div className="min-w-0 flex-1">
100
- <TokenName name={manualRegistrationTokenDisplayName(token)} />
95
+ <TokenName name={tokenDisplayName(token)} />
101
96
  <Code variant="paragraph" className="block truncate text-foreground-neutral-muted">
102
97
  {token.prefix}
103
98
  </Code>
@@ -108,13 +103,13 @@ export function ManualRegistrationTokenList({
108
103
  <div>
109
104
  <dt className="text-foreground-neutral-muted">Expires</dt>
110
105
  <dd>
111
- <ManualRegistrationTokenDate value={token.expires_at} />
106
+ <ManualRegistrationTokenDate value={token.expiresAt} />
112
107
  </dd>
113
108
  </div>
114
109
  <div>
115
110
  <dt className="text-foreground-neutral-muted">Created</dt>
116
111
  <dd>
117
- <ManualRegistrationTokenDate value={token.created_at} />
112
+ <ManualRegistrationTokenDate value={token.createdAt} />
118
113
  </dd>
119
114
  </div>
120
115
  </dl>
@@ -140,19 +135,15 @@ function RevokeManualRegistrationTokenButton({
140
135
  token,
141
136
  }: {
142
137
  workspaceId: string;
143
- token: ManualRegistrationTokenDto;
138
+ token: ManualRegistrationToken;
144
139
  }) {
145
- const queryClient = useQueryClient();
146
- const revokeToken = useRevokeManualRegistrationTokenMutation();
140
+ const revokeToken = useRevokeManualRegistrationTokenMutation(workspaceId);
147
141
  const [open, setOpen] = useState(false);
148
- const tokenName = manualRegistrationTokenDisplayName(token);
142
+ const tokenName = tokenDisplayName(token);
149
143
 
150
144
  async function handleRevoke() {
151
145
  try {
152
- await revokeToken.mutateAsync({workspaceId, tokenId: token.id});
153
- await queryClient.invalidateQueries({
154
- queryKey: manualRegistrationTokenQueryKeys.list(workspaceId),
155
- });
146
+ await revokeToken.mutateAsync(token.id);
156
147
  setOpen(false);
157
148
  } catch {
158
149
  // React Query stores the error for the inline modal alert.
@@ -1,4 +1,3 @@
1
- import type {CreateManualRegistrationTokenResponseDto} from '@shipfox/api-runners-dto';
2
1
  import {QueryLoadError} from '@shipfox/client-ui';
3
2
  import {Button} from '@shipfox/react-ui/button';
4
3
  import {Icon} from '@shipfox/react-ui/icon';
@@ -12,6 +11,7 @@ import {
12
11
  } from '@shipfox/react-ui/modal';
13
12
  import {Header, Text} from '@shipfox/react-ui/typography';
14
13
  import {useState} from 'react';
14
+ import type {CreatedManualRegistrationToken} from '#core/token.js';
15
15
  import {useManualRegistrationTokensQuery} from '#hooks/api/manual-registration-tokens.js';
16
16
  import {
17
17
  CreatedManualRegistrationTokenPanel,
@@ -30,10 +30,8 @@ export function WorkspaceManualRegistrationTokensSettingsSection({
30
30
  }) {
31
31
  const tokensQuery = useManualRegistrationTokensQuery(workspaceId);
32
32
  const [isModalOpen, setIsModalOpen] = useState(false);
33
- const [createdToken, setCreatedToken] = useState<CreateManualRegistrationTokenResponseDto | null>(
34
- null,
35
- );
36
- const tokens = tokensQuery.data?.manual_registration_tokens ?? [];
33
+ const [createdToken, setCreatedToken] = useState<CreatedManualRegistrationToken | null>(null);
34
+ const tokens = tokensQuery.data ?? [];
37
35
 
38
36
  function handleOpenChange(nextOpen: boolean) {
39
37
  setIsModalOpen(nextOpen);
@@ -7,6 +7,5 @@ export function provisionerTokenErrorMessage(error: unknown): string {
7
7
  }
8
8
  return error.message;
9
9
  }
10
- if (error instanceof Error) return error.message;
11
10
  return 'Something went wrong. Try again.';
12
11
  }
@@ -21,10 +21,10 @@ describe('provisionerTokenCreateErrorToFormError', () => {
21
21
  });
22
22
  });
23
23
 
24
- test('routes Error to a form-level alert with the error message', () => {
24
+ test('does not expose an unknown Error message', () => {
25
25
  const result = provisionerTokenCreateErrorToFormError(new Error('boom'));
26
26
 
27
- expect(result).toEqual({kind: 'form', message: 'boom'});
27
+ expect(result).toEqual({kind: 'form', message: 'Something went wrong. Try again.'});
28
28
  });
29
29
 
30
30
  test('falls back to generic copy for non-Error throwables', () => {
@@ -1,24 +1,25 @@
1
- import type {ProvisionerTokenDto} from '@shipfox/api-runners-dto';
2
1
  import {
3
- formatProvisionerTokenDate,
4
- formatProvisionerTokenTimestamp,
2
+ type ProvisionerToken,
5
3
  provisionerConnectionStatus,
6
4
  provisionerTokenDisplayName,
5
+ } from '#core/token.js';
6
+ import {
7
+ formatProvisionerTokenDate,
8
+ formatProvisionerTokenTimestamp,
7
9
  } from './provisioner-token-format.js';
8
10
 
9
- const token: ProvisionerTokenDto = {
11
+ const token: ProvisionerToken = {
10
12
  id: '33333333-3333-4333-8333-333333333333',
11
- scope: 'workspace',
12
- workspace_id: '11111111-1111-4111-8111-111111111111',
13
+ workspaceId: '11111111-1111-4111-8111-111111111111',
13
14
  prefix: 'sf_pt_abcde',
14
15
  name: 'Docker provisioner',
15
- created_by_user_id: '22222222-2222-4222-8222-222222222222',
16
- revoked_by_user_id: null,
17
- expires_at: '2026-05-09T00:00:00.000Z',
18
- revoked_at: null,
19
- last_seen_at: null,
20
- created_at: '2026-05-08T00:00:00.000Z',
21
- updated_at: '2026-05-08T00:00:00.000Z',
16
+ createdByUserId: '22222222-2222-4222-8222-222222222222',
17
+ revokedByUserId: null,
18
+ expiresAt: '2026-05-09T00:00:00.000Z',
19
+ revokedAt: null,
20
+ lastSeenAt: null,
21
+ createdAt: '2026-05-08T00:00:00.000Z',
22
+ updatedAt: '2026-05-08T00:00:00.000Z',
22
23
  };
23
24
 
24
25
  describe('provisionerTokenDisplayName', () => {
@@ -73,7 +74,7 @@ describe('provisionerConnectionStatus', () => {
73
74
 
74
75
  test('returns last seen when inactive with a last seen timestamp', () => {
75
76
  const result = provisionerConnectionStatus(
76
- {...token, last_seen_at: '2026-05-08T01:00:00.000Z'},
77
+ {...token, lastSeenAt: '2026-05-08T01:00:00.000Z'},
77
78
  new Set(),
78
79
  );
79
80
 
@@ -1,12 +1,5 @@
1
- import type {ProvisionerTokenDto} from '@shipfox/api-runners-dto';
2
- import type {DotVariant} from '@shipfox/react-ui/dot';
3
1
  import {formatDate, formatTimestamp} from '@shipfox/react-ui/utils';
4
2
 
5
- export type ProvisionerConnectionStatus =
6
- | {kind: 'connected'; dotVariant: DotVariant; label: 'Connected'}
7
- | {kind: 'last-seen'; dotVariant: DotVariant; label: 'Last seen'; lastSeenAt: string}
8
- | {kind: 'never-connected'; dotVariant: DotVariant; label: 'Never connected'};
9
-
10
3
  export function formatProvisionerTokenDate(value: string | null): string {
11
4
  if (!value) return 'Never';
12
5
  return formatDate(value);
@@ -16,25 +9,3 @@ export function formatProvisionerTokenTimestamp(value: string | null): string |
16
9
  if (!value) return undefined;
17
10
  return formatTimestamp(value);
18
11
  }
19
-
20
- export function provisionerTokenDisplayName(token: Pick<ProvisionerTokenDto, 'name'>): string {
21
- return token.name || 'Unnamed provisioner';
22
- }
23
-
24
- export function provisionerConnectionStatus(
25
- token: Pick<ProvisionerTokenDto, 'id' | 'last_seen_at'>,
26
- activeIds: ReadonlySet<string>,
27
- ): ProvisionerConnectionStatus {
28
- if (activeIds.has(token.id)) {
29
- return {kind: 'connected', dotVariant: 'success', label: 'Connected'};
30
- }
31
- if (token.last_seen_at) {
32
- return {
33
- kind: 'last-seen',
34
- dotVariant: 'neutral',
35
- label: 'Last seen',
36
- lastSeenAt: token.last_seen_at,
37
- };
38
- }
39
- return {kind: 'never-connected', dotVariant: 'neutral', label: 'Never connected'};
40
- }
@@ -1,4 +1,3 @@
1
- import type {ProvisionerTokenDto} from '@shipfox/api-runners-dto';
2
1
  import {Button, IconButton} from '@shipfox/react-ui/button';
3
2
  import {Callout} from '@shipfox/react-ui/callout';
4
3
  import {Dot} from '@shipfox/react-ui/dot';
@@ -28,18 +27,17 @@ import {
28
27
  TableRow,
29
28
  } from '@shipfox/react-ui/table';
30
29
  import {Code, Text} from '@shipfox/react-ui/typography';
31
- import {useQueryClient} from '@tanstack/react-query';
32
30
  import {useState} from 'react';
33
31
  import {
34
- provisionerTokenQueryKeys,
35
- useRevokeProvisionerTokenMutation,
36
- } from '#hooks/api/provisioner-tokens.js';
32
+ type ProvisionerToken,
33
+ provisionerConnectionStatus,
34
+ provisionerTokenDisplayName,
35
+ } from '#core/token.js';
36
+ import {useRevokeProvisionerTokenMutation} from '#hooks/api/provisioner-tokens.js';
37
37
  import {provisionerTokenErrorMessage} from './provisioner-token-errors.js';
38
38
  import {
39
39
  formatProvisionerTokenDate,
40
40
  formatProvisionerTokenTimestamp,
41
- provisionerConnectionStatus,
42
- provisionerTokenDisplayName,
43
41
  } from './provisioner-token-format.js';
44
42
  import {TokenDate} from './token-date.js';
45
43
  import {TokenName} from './token-name.js';
@@ -50,7 +48,7 @@ export function ProvisionerTokenList({
50
48
  activeIds,
51
49
  }: {
52
50
  workspaceId: string;
53
- tokens: ProvisionerTokenDto[];
51
+ tokens: ProvisionerToken[];
54
52
  activeIds: ReadonlySet<string>;
55
53
  }) {
56
54
  return (
@@ -82,10 +80,10 @@ export function ProvisionerTokenList({
82
80
  <ProvisionerStatusCell token={token} activeIds={activeIds} />
83
81
  </TableCell>
84
82
  <TableCell>
85
- <ProvisionerTokenDate value={token.expires_at} />
83
+ <ProvisionerTokenDate value={token.expiresAt} />
86
84
  </TableCell>
87
85
  <TableCell>
88
- <ProvisionerTokenDate value={token.created_at} />
86
+ <ProvisionerTokenDate value={token.createdAt} />
89
87
  </TableCell>
90
88
  <TableCell className="text-right">
91
89
  <RevokeProvisionerTokenButton workspaceId={workspaceId} token={token} />
@@ -120,13 +118,13 @@ export function ProvisionerTokenList({
120
118
  <div>
121
119
  <dt className="text-foreground-neutral-muted">Expires</dt>
122
120
  <dd>
123
- <ProvisionerTokenDate value={token.expires_at} />
121
+ <ProvisionerTokenDate value={token.expiresAt} />
124
122
  </dd>
125
123
  </div>
126
124
  <div>
127
125
  <dt className="text-foreground-neutral-muted">Created</dt>
128
126
  <dd>
129
- <ProvisionerTokenDate value={token.created_at} />
127
+ <ProvisionerTokenDate value={token.createdAt} />
130
128
  </dd>
131
129
  </div>
132
130
  </dl>
@@ -151,7 +149,7 @@ function ProvisionerStatusCell({
151
149
  token,
152
150
  activeIds,
153
151
  }: {
154
- token: ProvisionerTokenDto;
152
+ token: ProvisionerToken;
155
153
  activeIds: ReadonlySet<string>;
156
154
  }) {
157
155
  const status = provisionerConnectionStatus(token, activeIds);
@@ -175,22 +173,15 @@ function RevokeProvisionerTokenButton({
175
173
  token,
176
174
  }: {
177
175
  workspaceId: string;
178
- token: ProvisionerTokenDto;
176
+ token: ProvisionerToken;
179
177
  }) {
180
- const queryClient = useQueryClient();
181
- const revokeToken = useRevokeProvisionerTokenMutation();
178
+ const revokeToken = useRevokeProvisionerTokenMutation(workspaceId);
182
179
  const [open, setOpen] = useState(false);
183
180
  const tokenName = provisionerTokenDisplayName(token);
184
181
 
185
182
  async function handleRevoke() {
186
183
  try {
187
- await revokeToken.mutateAsync({workspaceId, tokenId: token.id});
188
- await queryClient.invalidateQueries({
189
- queryKey: provisionerTokenQueryKeys.list(workspaceId),
190
- });
191
- await queryClient.invalidateQueries({
192
- queryKey: provisionerTokenQueryKeys.active(workspaceId),
193
- });
184
+ await revokeToken.mutateAsync(token.id);
194
185
  setOpen(false);
195
186
  } catch {
196
187
  // React Query stores the error for the inline modal alert.
@@ -1,4 +1,3 @@
1
- import type {CreateProvisionerTokenResponseDto} from '@shipfox/api-runners-dto';
2
1
  import {QueryLoadError} from '@shipfox/client-ui';
3
2
  import {Button} from '@shipfox/react-ui/button';
4
3
  import {Icon} from '@shipfox/react-ui/icon';
@@ -13,6 +12,7 @@ import {
13
12
  import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
14
13
  import {Header, Text} from '@shipfox/react-ui/typography';
15
14
  import {useMemo, useState} from 'react';
15
+ import type {CreatedProvisionerToken} from '#core/token.js';
16
16
  import {
17
17
  useActiveProvisionersQuery,
18
18
  useProvisionerTokensQuery,
@@ -31,10 +31,10 @@ export function WorkspaceProvisionerTokensSettingsSection({workspaceId}: {worksp
31
31
  const tokensQuery = useProvisionerTokensQuery(workspaceId);
32
32
  const activeProvisionersQuery = useActiveProvisionersQuery(workspaceId);
33
33
  const [isModalOpen, setIsModalOpen] = useState(false);
34
- const [createdToken, setCreatedToken] = useState<CreateProvisionerTokenResponseDto | null>(null);
35
- const tokens = tokensQuery.data?.tokens ?? [];
34
+ const [createdToken, setCreatedToken] = useState<CreatedProvisionerToken | null>(null);
35
+ const tokens = tokensQuery.data ?? [];
36
36
  const activeIds = useMemo(
37
- () => new Set(activeProvisionersQuery.data?.provisioners.map((provisioner) => provisioner.id)),
37
+ () => new Set(activeProvisionersQuery.data?.map((provisioner) => provisioner.id)),
38
38
  [activeProvisionersQuery.data],
39
39
  );
40
40
 
@@ -0,0 +1,15 @@
1
+ import {expirationCommand} from './token-expiration-select.js';
2
+
3
+ describe('expirationCommand', () => {
4
+ test('maps "never" to a non-expiring command', () => {
5
+ const result = expirationCommand('never');
6
+
7
+ expect(result).toEqual({kind: 'never'});
8
+ });
9
+
10
+ test('maps a TTL option to an expires-after command in seconds', () => {
11
+ const result = expirationCommand('86400');
12
+
13
+ expect(result).toEqual({kind: 'expires-after', seconds: 86_400});
14
+ });
15
+ });