Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/organization-response-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@seamless-auth/react': patch
---

Correct the organization response types, checked against the API's declared response schemas.

- `addOrganizationMember` and `updateOrganizationMember` return `OrganizationMembershipResult` (`{ membership }`). They were typed as returning a members list, so reading `.members` gave `undefined`.
- `removeOrganizationMember` returns `MessageResult`, not a members list.
- `switchOrganization` returns `OrganizationSwitchResult` (`{ message, organizationId, organization }`), which describes what the endpoint actually sends.

`OrganizationMembershipResult` and `OrganizationSwitchResult` are new exports.
32 changes: 24 additions & 8 deletions src/client/createSeamlessAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ export interface OrganizationMembersResult {
total: number;
}

/** Response body for a single membership mutation. */
export interface OrganizationMembershipResult {
membership: OrganizationMembership;
}

/**
* Response body when the active organization changes. The server also returns
* session material here, which this SDK deliberately does not surface: sessions
* are carried by cookies, so adopters have no reason to handle raw tokens.
*/
export interface OrganizationSwitchResult {
message: string;
organizationId: string;
organization: Organization;
}

export interface OAuthProvider {
id: string;
name: string;
Expand Down Expand Up @@ -278,23 +294,23 @@ export interface SeamlessAuthClient {
) => Promise<SeamlessAuthResult<OrganizationResult>>;
switchOrganization: (
organizationId: string
) => Promise<SeamlessAuthResult<OrganizationResult>>;
) => Promise<SeamlessAuthResult<OrganizationSwitchResult>>;
listOrganizationMembers: (
organizationId: string
) => Promise<SeamlessAuthResult<OrganizationMembersResult>>;
addOrganizationMember: (
organizationId: string,
input: OrganizationMemberInput
) => Promise<SeamlessAuthResult<OrganizationMembersResult>>;
) => Promise<SeamlessAuthResult<OrganizationMembershipResult>>;
updateOrganizationMember: (
organizationId: string,
userId: string,
input: OrganizationMemberUpdateInput
) => Promise<SeamlessAuthResult<OrganizationMembersResult>>;
) => Promise<SeamlessAuthResult<OrganizationMembershipResult>>;
removeOrganizationMember: (
organizationId: string,
userId: string
) => Promise<SeamlessAuthResult<OrganizationMembersResult>>;
) => Promise<SeamlessAuthResult<MessageResult>>;
}

type StepUpPayload = Partial<StepUpStatus> & { message?: string };
Expand Down Expand Up @@ -814,7 +830,7 @@ export const createSeamlessAuthClient = (
),

switchOrganization: organizationId =>
requestResult<OrganizationResult>(
requestResult<OrganizationSwitchResult>(
fetchWithAuth(`/organizations/${encodeURIComponent(organizationId)}/switch`, {
method: 'POST',
}),
Expand All @@ -830,7 +846,7 @@ export const createSeamlessAuthClient = (
),

addOrganizationMember: (organizationId, input) =>
requestResult<OrganizationMembersResult>(
requestResult<OrganizationMembershipResult>(
fetchWithAuth(`/organizations/${encodeURIComponent(organizationId)}/members`, {
method: 'POST',
body: JSON.stringify(input),
Expand All @@ -839,7 +855,7 @@ export const createSeamlessAuthClient = (
),

updateOrganizationMember: (organizationId, userId, input) =>
requestResult<OrganizationMembersResult>(
requestResult<OrganizationMembershipResult>(
fetchWithAuth(
`/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(userId)}`,
{
Expand All @@ -851,7 +867,7 @@ export const createSeamlessAuthClient = (
),

removeOrganizationMember: (organizationId, userId) =>
requestResult<OrganizationMembersResult>(
requestResult<MessageResult>(
fetchWithAuth(
`/organizations/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(userId)}`,
{
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
OrganizationMemberInput,
OrganizationMemberUpdateInput,
OrganizationMembersResult,
OrganizationMembershipResult,
OrganizationResult,
OrganizationSwitchResult,
OrganizationsResult,
PasskeyLoginData,
PasskeyMetadata,
Expand Down Expand Up @@ -85,7 +87,9 @@ export type {
OrganizationMembership,
OrganizationMemberUpdateInput,
OrganizationMembersResult,
OrganizationMembershipResult,
OrganizationResult,
OrganizationSwitchResult,
OrganizationsResult,
PasskeyLoginData,
PasskeyMetadata,
Expand Down
25 changes: 25 additions & 0 deletions tests/createSeamlessAuthClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,31 @@ describe('createSeamlessAuthClient', () => {
);
});

it('surfaces the declared shapes for organization member mutations', async () => {
const membership = { userId: 'u1', roles: ['member'] };

mockFetchWithAuth
.mockResolvedValueOnce({ ok: true, json: async () => ({ membership }) })
.mockResolvedValueOnce({ ok: true, json: async () => ({ membership }) })
.mockResolvedValueOnce({ ok: true, json: async () => ({ message: 'Success' }) });

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
});

// The API returns a single membership envelope for add and update, and a
// plain message for remove, not a members list.
const added = await client.addOrganizationMember('org-1', { userId: 'u1' });
const updated = await client.updateOrganizationMember('org-1', 'u1', {
roles: ['admin'],
});
const removed = await client.removeOrganizationMember('org-1', 'u1');

expect(added.data?.membership).toEqual(membership);
expect(updated.data?.membership).toEqual(membership);
expect(removed.data?.message).toBe('Success');
});

it('uses OAuth login endpoints', async () => {
const providersResult = {
providers: [{ id: 'google', name: 'Google', scopes: ['openid', 'email'] }],
Expand Down
Loading