@shipfox/client-invitations 4.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 (61) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +47 -0
  3. package/dist/complete-acceptance.d.ts +1 -1
  4. package/dist/complete-acceptance.js +1 -1
  5. package/dist/complete-acceptance.js.map +1 -1
  6. package/dist/core/invitation-acceptance.d.ts +9 -0
  7. package/dist/core/invitation-acceptance.d.ts.map +1 -0
  8. package/dist/core/invitation-acceptance.js +3 -0
  9. package/dist/core/invitation-acceptance.js.map +1 -0
  10. package/dist/core/invitation-preview.d.ts +21 -0
  11. package/dist/core/invitation-preview.d.ts.map +1 -0
  12. package/dist/core/invitation-preview.js +5 -0
  13. package/dist/core/invitation-preview.js.map +1 -0
  14. package/dist/hooks/api/accept-invitation.d.ts +1 -8
  15. package/dist/hooks/api/accept-invitation.d.ts.map +1 -1
  16. package/dist/hooks/api/accept-invitation.js +7 -4
  17. package/dist/hooks/api/accept-invitation.js.map +1 -1
  18. package/dist/hooks/api/invitation-acceptance-mapper.d.ts +4 -0
  19. package/dist/hooks/api/invitation-acceptance-mapper.d.ts.map +1 -0
  20. package/dist/hooks/api/invitation-acceptance-mapper.js +12 -0
  21. package/dist/hooks/api/invitation-acceptance-mapper.js.map +1 -0
  22. package/dist/hooks/api/invitation-preview-mapper.d.ts +4 -0
  23. package/dist/hooks/api/invitation-preview-mapper.d.ts.map +1 -0
  24. package/dist/hooks/api/invitation-preview-mapper.js +32 -0
  25. package/dist/hooks/api/invitation-preview-mapper.js.map +1 -0
  26. package/dist/hooks/api/preview-invitation.d.ts +12 -7
  27. package/dist/hooks/api/preview-invitation.d.ts.map +1 -1
  28. package/dist/hooks/api/preview-invitation.js +12 -6
  29. package/dist/hooks/api/preview-invitation.js.map +1 -1
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/pages/invitation-accept-page.d.ts.map +1 -1
  35. package/dist/pages/invitation-accept-page.js +17 -19
  36. package/dist/pages/invitation-accept-page.js.map +1 -1
  37. package/dist/routes/accept.d.ts +2 -0
  38. package/dist/routes/accept.d.ts.map +1 -1
  39. package/dist/routes/accept.js +2 -0
  40. package/dist/routes/accept.js.map +1 -1
  41. package/dist/routes/inputs.d.ts +5 -0
  42. package/dist/routes/inputs.d.ts.map +1 -0
  43. package/dist/routes/inputs.js +8 -0
  44. package/dist/routes/inputs.js.map +1 -0
  45. package/dist/tsconfig.test.tsbuildinfo +1 -1
  46. package/package.json +10 -12
  47. package/src/complete-acceptance.ts +1 -1
  48. package/src/core/invitation-acceptance.ts +8 -0
  49. package/src/core/invitation-preview.ts +18 -0
  50. package/src/hooks/api/accept-invitation.ts +7 -8
  51. package/src/hooks/api/invitation-acceptance-mapper.test.ts +23 -0
  52. package/src/hooks/api/invitation-acceptance-mapper.ts +13 -0
  53. package/src/hooks/api/invitation-preview-mapper.test.ts +36 -0
  54. package/src/hooks/api/invitation-preview-mapper.ts +22 -0
  55. package/src/hooks/api/preview-invitation.ts +24 -9
  56. package/src/index.ts +3 -0
  57. package/src/pages/invitation-accept-page.tsx +24 -20
  58. package/src/routes/accept.tsx +5 -1
  59. package/src/routes/inputs.test.ts +11 -0
  60. package/src/routes/inputs.ts +10 -0
  61. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-invitations",
3
3
  "license": "MIT",
4
- "version": "4.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,10 +10,6 @@
10
10
  "type": "module",
11
11
  "main": "dist/index.js",
12
12
  "types": "dist/index.d.ts",
13
- "imports": {
14
- "#*": "./dist/*",
15
- "#test/*": "./test/*"
16
- },
17
13
  "exports": {
18
14
  ".": {
19
15
  "types": "./dist/index.d.ts",
@@ -31,13 +27,12 @@
31
27
  "dependencies": {
32
28
  "@swc/helpers": "^0.5.17",
33
29
  "@tanstack/react-query": "^5.101.0",
34
- "@shipfox/api-workspaces-dto": "6.0.0",
35
- "@shipfox/client-api": "4.0.0",
36
- "@shipfox/api-auth-dto": "7.1.0",
37
- "@shipfox/client-shell": "4.0.0",
38
- "@shipfox/client-auth": "4.0.0",
39
- "@shipfox/client-ui": "4.0.0",
40
- "@shipfox/react-ui": "0.3.5"
30
+ "@shipfox/api-auth-dto": "9.0.1",
31
+ "@shipfox/api-workspaces-dto": "9.0.1",
32
+ "@shipfox/client-api": "6.0.1",
33
+ "@shipfox/client-ui": "6.0.1",
34
+ "@shipfox/react-ui": "0.3.6",
35
+ "@shipfox/client-shell": "6.0.1"
41
36
  },
42
37
  "peerDependencies": {
43
38
  "@tanstack/react-query": "^5.101.0",
@@ -45,6 +40,9 @@
45
40
  "react": "^19.0.0",
46
41
  "react-dom": "^19.0.0"
47
42
  },
43
+ "imports": {
44
+ "#*": "./dist/*"
45
+ },
48
46
  "scripts": {
49
47
  "build": "shipfox-swc",
50
48
  "check": "shipfox-biome-check",
@@ -8,7 +8,7 @@ import type {NavigateOptions} from '@tanstack/react-router';
8
8
  * routes the user into the workspace home.
9
9
  *
10
10
  * `refreshAuth` is passed in so this helper stays a plain function (no hook).
11
- * Call sites construct it via `useRefreshAuth()` from `@shipfox/client-auth`.
11
+ * Call sites construct it via `useRefreshAuth()` from `@shipfox/client-shell/runtime`.
12
12
  */
13
13
  export async function completeInvitationAcceptance(params: {
14
14
  workspaceId: string;
@@ -0,0 +1,8 @@
1
+ export interface InvitationAcceptance {
2
+ membership: {
3
+ id: string;
4
+ userId: string;
5
+ workspaceId: string;
6
+ };
7
+ alreadyMember: boolean;
8
+ }
@@ -0,0 +1,18 @@
1
+ export type InvitationPreview =
2
+ | {
3
+ status: 'pending';
4
+ workspaceId: string;
5
+ workspaceName: string;
6
+ email: string;
7
+ invitedByDisplay?: string;
8
+ expiresAt: string;
9
+ }
10
+ | {status: 'expired'; workspaceName: string; expiresAt: string}
11
+ | {status: 'already_used'; workspaceName: string}
12
+ | {status: 'invalid'};
13
+
14
+ export function pendingInvitation(
15
+ preview: InvitationPreview | undefined,
16
+ ): Extract<InvitationPreview, {status: 'pending'}> | undefined {
17
+ return preview?.status === 'pending' ? preview : undefined;
18
+ }
@@ -1,15 +1,14 @@
1
- import type {
2
- AcceptInvitationBodyDto,
3
- AcceptInvitationResponseDto,
4
- } from '@shipfox/api-workspaces-dto';
5
- import {apiRequest} from '@shipfox/client-api';
1
+ import {acceptInvitationResponseSchema} from '@shipfox/api-workspaces-dto';
2
+ import {checkedApiRequest} from '@shipfox/client-api';
6
3
  import {useMutation} from '@tanstack/react-query';
4
+ import {toInvitationAcceptance} from './invitation-acceptance-mapper.js';
7
5
 
8
- async function acceptInvitation(body: AcceptInvitationBodyDto) {
9
- return await apiRequest<AcceptInvitationResponseDto>('/invitations/accept', {
6
+ async function acceptInvitation(command: {token: string}) {
7
+ const response = await checkedApiRequest(acceptInvitationResponseSchema, '/invitations/accept', {
10
8
  method: 'POST',
11
- body,
9
+ body: command,
12
10
  });
11
+ return toInvitationAcceptance(response);
13
12
  }
14
13
 
15
14
  export function useAcceptInvitation() {
@@ -0,0 +1,23 @@
1
+ import {toInvitationAcceptance} from './invitation-acceptance-mapper.js';
2
+
3
+ describe('toInvitationAcceptance', () => {
4
+ test('maps the acceptance response into the invitation domain', () => {
5
+ const result = toInvitationAcceptance({
6
+ membership: {
7
+ id: '11111111-1111-4111-8111-111111111111',
8
+ user_id: '22222222-2222-4222-8222-222222222222',
9
+ workspace_id: '33333333-3333-4333-8333-333333333333',
10
+ },
11
+ already_member: true,
12
+ });
13
+
14
+ expect(result).toEqual({
15
+ membership: {
16
+ id: '11111111-1111-4111-8111-111111111111',
17
+ userId: '22222222-2222-4222-8222-222222222222',
18
+ workspaceId: '33333333-3333-4333-8333-333333333333',
19
+ },
20
+ alreadyMember: true,
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,13 @@
1
+ import type {AcceptInvitationResponseDto} from '@shipfox/api-workspaces-dto';
2
+ import type {InvitationAcceptance} from '#core/invitation-acceptance.js';
3
+
4
+ export function toInvitationAcceptance(dto: AcceptInvitationResponseDto): InvitationAcceptance {
5
+ return {
6
+ membership: {
7
+ id: dto.membership.id,
8
+ userId: dto.membership.user_id,
9
+ workspaceId: dto.membership.workspace_id,
10
+ },
11
+ alreadyMember: dto.already_member,
12
+ };
13
+ }
@@ -0,0 +1,36 @@
1
+ import {describe, expect, it} from '@shipfox/vitest/vi';
2
+ import {toInvitationPreview} from './invitation-preview-mapper.js';
3
+
4
+ describe('toInvitationPreview', () => {
5
+ it.each([
6
+ [
7
+ {
8
+ status: 'pending' as const,
9
+ workspace_id: '11111111-1111-4111-8111-111111111111',
10
+ workspace_name: 'Acme',
11
+ email: 'member@example.com',
12
+ invited_by_display: 'Owner',
13
+ expires_at: '2026-08-01T00:00:00.000Z',
14
+ },
15
+ {
16
+ status: 'pending',
17
+ workspaceId: '11111111-1111-4111-8111-111111111111',
18
+ workspaceName: 'Acme',
19
+ email: 'member@example.com',
20
+ invitedByDisplay: 'Owner',
21
+ expiresAt: '2026-08-01T00:00:00.000Z',
22
+ },
23
+ ],
24
+ [
25
+ {status: 'expired' as const, workspace_name: 'Acme', expires_at: '2026-08-01T00:00:00.000Z'},
26
+ {status: 'expired', workspaceName: 'Acme', expiresAt: '2026-08-01T00:00:00.000Z'},
27
+ ],
28
+ [
29
+ {status: 'already_used' as const, workspace_name: 'Acme'},
30
+ {status: 'already_used', workspaceName: 'Acme'},
31
+ ],
32
+ [{status: 'invalid' as const}, {status: 'invalid'}],
33
+ ])('maps %s previews into the invitation domain', (dto, expected) => {
34
+ expect(toInvitationPreview(dto)).toEqual(expected);
35
+ });
36
+ });
@@ -0,0 +1,22 @@
1
+ import type {PreviewInvitationResponseDto} from '@shipfox/api-workspaces-dto';
2
+ import type {InvitationPreview} from '#core/invitation-preview.js';
3
+
4
+ export function toInvitationPreview(dto: PreviewInvitationResponseDto): InvitationPreview {
5
+ switch (dto.status) {
6
+ case 'pending':
7
+ return {
8
+ status: dto.status,
9
+ workspaceId: dto.workspace_id,
10
+ workspaceName: dto.workspace_name,
11
+ email: dto.email,
12
+ ...(dto.invited_by_display ? {invitedByDisplay: dto.invited_by_display} : {}),
13
+ expiresAt: dto.expires_at,
14
+ };
15
+ case 'expired':
16
+ return {status: dto.status, workspaceName: dto.workspace_name, expiresAt: dto.expires_at};
17
+ case 'already_used':
18
+ return {status: dto.status, workspaceName: dto.workspace_name};
19
+ case 'invalid':
20
+ return {status: dto.status};
21
+ }
22
+ }
@@ -1,23 +1,38 @@
1
- import type {PreviewInvitationResponseDto} from '@shipfox/api-workspaces-dto';
2
- import {apiRequest} from '@shipfox/client-api';
3
- import {useQuery} from '@tanstack/react-query';
1
+ import {previewInvitationResponseSchema} from '@shipfox/api-workspaces-dto';
2
+ import {checkedApiRequest} from '@shipfox/client-api';
3
+ import {type FetchQueryOptions, queryOptions, useQuery} from '@tanstack/react-query';
4
+ import type {InvitationPreview} from '#core/invitation-preview.js';
5
+ import {toInvitationPreview} from './invitation-preview-mapper.js';
4
6
 
5
7
  export const previewInvitationQueryKey = (token: string) =>
6
8
  ['invitations', 'preview', token] as const;
7
9
 
8
- async function previewInvitation(token: string): Promise<PreviewInvitationResponseDto> {
10
+ type PreviewInvitationQueryOptions = FetchQueryOptions<
11
+ InvitationPreview,
12
+ Error,
13
+ InvitationPreview,
14
+ ReturnType<typeof previewInvitationQueryKey>
15
+ >;
16
+
17
+ async function previewInvitation(token: string): Promise<InvitationPreview> {
9
18
  const params = new URLSearchParams({token});
10
- return await apiRequest<PreviewInvitationResponseDto>(
19
+ const response = await checkedApiRequest(
20
+ previewInvitationResponseSchema,
11
21
  `/invitations/preview?${params.toString()}`,
12
22
  );
23
+ return toInvitationPreview(response);
13
24
  }
14
25
 
15
- export function usePreviewInvitation(token: string | undefined) {
16
- return useQuery({
17
- queryKey: previewInvitationQueryKey(token ?? ''),
18
- queryFn: () => previewInvitation(token as string),
26
+ export function previewInvitationQueryOptions(token: string): PreviewInvitationQueryOptions {
27
+ return queryOptions({
28
+ queryKey: previewInvitationQueryKey(token),
29
+ queryFn: () => previewInvitation(token),
19
30
  enabled: Boolean(token),
20
31
  retry: false,
21
32
  staleTime: 30_000,
22
33
  });
23
34
  }
35
+
36
+ export function usePreviewInvitation(token: string | undefined) {
37
+ return useQuery(previewInvitationQueryOptions(token ?? ''));
38
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
+ export type {InvitationAcceptance} from '#core/invitation-acceptance.js';
2
+ export {type InvitationPreview, pendingInvitation} from '#core/invitation-preview.js';
1
3
  export {completeInvitationAcceptance} from './complete-acceptance.js';
2
4
  export {useAcceptInvitation} from './hooks/api/accept-invitation.js';
3
5
  export {
4
6
  previewInvitationQueryKey,
7
+ previewInvitationQueryOptions,
5
8
  usePreviewInvitation,
6
9
  } from './hooks/api/preview-invitation.js';
7
10
  export {InvitationAcceptPage} from './pages/invitation-accept-page.js';
@@ -1,4 +1,9 @@
1
- import {AuthShell, useAuthState, useRefreshAuth} from '@shipfox/client-auth';
1
+ import {
2
+ AuthShell,
3
+ useAuthState,
4
+ useRefreshAuth,
5
+ useRouteSearch,
6
+ } from '@shipfox/client-shell/runtime';
2
7
  import {createSingleFlight} from '@shipfox/client-ui';
3
8
  import {Button} from '@shipfox/react-ui/button';
4
9
  import {Callout} from '@shipfox/react-ui/callout';
@@ -6,20 +11,19 @@ import {ShipfoxLoader} from '@shipfox/react-ui/loader';
6
11
  import {toast} from '@shipfox/react-ui/toast';
7
12
  import {Text} from '@shipfox/react-ui/typography';
8
13
  import {formatDate} from '@shipfox/react-ui/utils';
9
- import {Link, useNavigate, useSearch} from '@tanstack/react-router';
14
+ import {Link, useNavigate} from '@tanstack/react-router';
10
15
  import {useCallback, useEffect, useRef} from 'react';
11
16
  import {completeInvitationAcceptance} from '#complete-acceptance.js';
12
17
  import {useAcceptInvitation} from '#hooks/api/accept-invitation.js';
13
18
  import {usePreviewInvitation} from '#hooks/api/preview-invitation.js';
19
+ import {validateInvitationAcceptSearch} from '../routes/inputs.js';
14
20
 
15
21
  const invitationAccepts = createSingleFlight<string, void>();
16
22
  const toastedTerminals = new Set<string>();
17
23
  const TOASTED_TERMINALS_MAX = 32;
18
24
 
19
25
  export function InvitationAcceptPage() {
20
- const search = useSearch({strict: false}) as {token?: unknown};
21
- const token =
22
- typeof search.token === 'string' && search.token.length > 0 ? search.token : undefined;
26
+ const {token} = useRouteSearch(validateInvitationAcceptSearch);
23
27
  const navigate = useNavigate();
24
28
  const auth = useAuthState();
25
29
  const refreshAuth = useRefreshAuth();
@@ -53,7 +57,7 @@ export function InvitationAcceptPage() {
53
57
  const runAccept = useCallback(
54
58
  async (params: {token: string; workspaceName: string}) => {
55
59
  const result = await accept.mutateAsync({token: params.token});
56
- await completeAccept(result.membership.workspace_id, params.workspaceName);
60
+ await completeAccept(result.membership.workspaceId, params.workspaceName);
57
61
  },
58
62
  [accept, completeAccept],
59
63
  );
@@ -74,7 +78,7 @@ export function InvitationAcceptPage() {
74
78
 
75
79
  hasKickedAccept.current = true;
76
80
  invitationAccepts
77
- .run(token, async () => await runAccept({token, workspaceName: data.workspace_name}))
81
+ .run(token, async () => await runAccept({token, workspaceName: data.workspaceName}))
78
82
  .catch(() => {
79
83
  // The mutation surfaces its error via accept.error; the UI re-renders
80
84
  // into the auth-match-error state below.
@@ -137,7 +141,7 @@ export function InvitationAcceptPage() {
137
141
  return (
138
142
  <AuthShell title="Invitation expired" description="The link is no longer accepting joins.">
139
143
  <Callout role="alert" type="error">
140
- This invitation expired on {formatDate(data.expires_at)}. Ask your administrator to send a
144
+ This invitation expired on {formatDate(data.expiresAt)}. Ask your administrator to send a
141
145
  new one.
142
146
  </Callout>
143
147
  <GoHomeButton navigate={navigate} />
@@ -150,12 +154,12 @@ export function InvitationAcceptPage() {
150
154
  toast.info(`This invitation has already been accepted.`),
151
155
  );
152
156
  return (
153
- <AuthShell title="Invitation already used" description={data.workspace_name}>
157
+ <AuthShell title="Invitation already used" description={data.workspaceName}>
154
158
  <Callout role="alert" type="info">
155
159
  This invitation has already been accepted.{' '}
156
160
  {auth.isAuthenticated
157
- ? `Open your dashboard to access ${data.workspace_name} if your account is a member.`
158
- : `Log in to access ${data.workspace_name}.`}
161
+ ? `Open your dashboard to access ${data.workspaceName} if your account is a member.`
162
+ : `Log in to access ${data.workspaceName}.`}
159
163
  </Callout>
160
164
  {auth.isAuthenticated ? null : (
161
165
  <Button asChild className="w-full">
@@ -168,8 +172,8 @@ export function InvitationAcceptPage() {
168
172
  }
169
173
 
170
174
  const inviterLine =
171
- data.invited_by_display != null
172
- ? `Invited by ${data.invited_by_display} to join as ${data.email}.`
175
+ data.invitedByDisplay != null
176
+ ? `Invited by ${data.invitedByDisplay} to join as ${data.email}.`
173
177
  : `Invited to join as ${data.email}.`;
174
178
 
175
179
  if (!auth.isAuthenticated) {
@@ -177,7 +181,7 @@ export function InvitationAcceptPage() {
177
181
  const signupHref = `/auth/signup?redirect=${encodeURIComponent(redirect)}`;
178
182
  const loginHref = `/auth/login?redirect=${encodeURIComponent(redirect)}`;
179
183
  return (
180
- <AuthShell title={data.workspace_name} description={inviterLine}>
184
+ <AuthShell title={data.workspaceName} description={inviterLine}>
181
185
  <div className="flex flex-col gap-16">
182
186
  <Button asChild className="w-full">
183
187
  <Link to={signupHref}>Create account</Link>
@@ -199,7 +203,7 @@ export function InvitationAcceptPage() {
199
203
  const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;
200
204
  const logoutHref = `/auth/logout?redirect=${encodeURIComponent(redirect)}`;
201
205
  return (
202
- <AuthShell title={data.workspace_name} description={inviterLine}>
206
+ <AuthShell title={data.workspaceName} description={inviterLine}>
203
207
  <Callout role="alert" type="warning">
204
208
  You're signed in as {auth.user?.email}, but this invitation is for {data.email}.
205
209
  </Callout>
@@ -223,15 +227,15 @@ export function InvitationAcceptPage() {
223
227
  // result. Show either the pending state or the error state.
224
228
  if (accept.isError) {
225
229
  return (
226
- <AuthShell title={data.workspace_name} description={inviterLine}>
230
+ <AuthShell title={data.workspaceName} description={inviterLine}>
227
231
  <Callout role="alert" type="error">
228
- We couldn't add you to {data.workspace_name}. Try again in a moment.
232
+ We couldn't add you to {data.workspaceName}. Try again in a moment.
229
233
  </Callout>
230
234
  <Button
231
235
  className="w-full"
232
236
  isLoading={accept.isPending}
233
237
  onClick={() => {
234
- runAccept({token, workspaceName: data.workspace_name}).catch(() => {
238
+ runAccept({token, workspaceName: data.workspaceName}).catch(() => {
235
239
  // The mutation state drives the rendered error; avoid an
236
240
  // unhandled rejection from the click handler.
237
241
  });
@@ -244,11 +248,11 @@ export function InvitationAcceptPage() {
244
248
  }
245
249
 
246
250
  return (
247
- <AuthShell title={data.workspace_name} description={inviterLine}>
251
+ <AuthShell title={data.workspaceName} description={inviterLine}>
248
252
  <div className="flex flex-col items-center gap-12" aria-live="polite">
249
253
  <ShipfoxLoader />
250
254
  <Text size="sm" className="text-foreground-neutral-subtle">
251
- Adding you to {data.workspace_name}…
255
+ Adding you to {data.workspaceName}…
252
256
  </Text>
253
257
  </div>
254
258
  </AuthShell>
@@ -1,4 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {InvitationAcceptPage} from '#pages/invitation-accept-page.js';
3
+ import {validateInvitationAcceptSearch} from './inputs.js';
3
4
 
4
- export default defineRoute({component: InvitationAcceptPage});
5
+ export default defineRoute({
6
+ validateSearch: validateInvitationAcceptSearch,
7
+ component: InvitationAcceptPage,
8
+ });
@@ -0,0 +1,11 @@
1
+ import {validateInvitationAcceptSearch} from './inputs.js';
2
+
3
+ describe('invitation route inputs', () => {
4
+ it('accepts a token and drops missing or malformed values', () => {
5
+ expect(validateInvitationAcceptSearch({token: 'invite-token'})).toEqual({
6
+ token: 'invite-token',
7
+ });
8
+ expect(validateInvitationAcceptSearch({})).toEqual({});
9
+ expect(validateInvitationAcceptSearch({token: ['invite-token']})).toEqual({});
10
+ });
11
+ });
@@ -0,0 +1,10 @@
1
+ export interface InvitationAcceptSearch {
2
+ token?: string;
3
+ }
4
+
5
+ export function validateInvitationAcceptSearch(
6
+ input: Record<string, unknown>,
7
+ ): InvitationAcceptSearch {
8
+ const token = typeof input.token === 'string' && input.token.length > 0 ? input.token : undefined;
9
+ return token ? {token} : {};
10
+ }