diff --git a/__tests__/workspaces.test.ts b/__tests__/workspaces.test.ts index ffe1549..db80e5a 100644 --- a/__tests__/workspaces.test.ts +++ b/__tests__/workspaces.test.ts @@ -3,23 +3,23 @@ import { fetchWorkspace, fetchManyWorkspaces, updateWorkspace, - updateInheritFlag, + updateWorkspaceInheritFlag, deleteWorkspace, - transferOwnership, - fetchMembers, - fetchMemberStanding, - updateMember, - removeMember, + transferWorkspaceOwnership, + fetchWorkspaceMembers, + fetchWorkspaceMemberStanding, + updateWorkspaceMember, + removeWorkspaceMember, leaveWorkspace, - removeFromSubtree, - createInvite, - fetchInvites, - revokeInvite, - resendInvite, - acceptInvite, - declineInvite, - fetchMyInvites, - getAuthority, + removeWorkspaceMemberFromSubtree, + createWorkspaceInvite, + fetchWorkspaceInvites, + revokeWorkspaceInvite, + resendWorkspaceInvite, + acceptWorkspaceInvite, + declineWorkspaceInvite, + fetchMyWorkspaceInvites, + fetchWorkspaceAuthority, } from "../src/modules/workspaces"; import { makeClient } from "./helpers/mockClient"; @@ -88,9 +88,9 @@ describe("node-sdk workspaces (lifecycle + ownership) — request shaping", () = }); }); - it("updateInheritFlag patches /workspaces/:id/inherit-flag with userId + flag", async () => { + it("updateWorkspaceInheritFlag patches /workspaces/:id/inherit-flag with userId + flag", async () => { const { client, projectInstance } = makeClient(); - await updateInheritFlag(client, { + await updateWorkspaceInheritFlag(client, { workspaceId: "w1", userId: "u1", inheritsFromParent: true, @@ -109,9 +109,9 @@ describe("node-sdk workspaces (lifecycle + ownership) — request shaping", () = }); }); - it("transferOwnership strips workspaceId into the path and posts the rest", async () => { + it("transferWorkspaceOwnership strips workspaceId into the path and posts the rest", async () => { const { client, projectInstance } = makeClient(); - await transferOwnership(client, { + await transferWorkspaceOwnership(client, { workspaceId: "w1", userId: "u1", newOwnerId: "u2", @@ -133,9 +133,9 @@ describe("node-sdk workspaces (lifecycle + ownership) — request shaping", () = }); describe("node-sdk workspaces (membership) — request shaping", () => { - it("fetchMembers strips workspaceId into the path and passes the rest as params", async () => { + it("fetchWorkspaceMembers strips workspaceId into the path and passes the rest as params", async () => { const { client, projectInstance } = makeClient(); - await fetchMembers(client, { + await fetchWorkspaceMembers(client, { workspaceId: "w1", include: "descendants", countOnly: true, @@ -145,9 +145,9 @@ describe("node-sdk workspaces (membership) — request shaping", () => { }); }); - it("fetchMemberStanding puts target in the path (no query params)", async () => { + it("fetchWorkspaceMemberStanding puts target in the path (no query params)", async () => { const { client, projectInstance } = makeClient(); - await fetchMemberStanding(client, { + await fetchWorkspaceMemberStanding(client, { workspaceId: "w1", targetUserId: "target1", }); @@ -156,9 +156,9 @@ describe("node-sdk workspaces (membership) — request shaping", () => { ); }); - it("updateMember puts target in the path, actor userId in the body", async () => { + it("updateWorkspaceMember puts target in the path, actor userId in the body", async () => { const { client, projectInstance } = makeClient(); - await updateMember(client, { + await updateWorkspaceMember(client, { workspaceId: "w1", targetUserId: "target1", userId: "actor1", @@ -172,9 +172,9 @@ describe("node-sdk workspaces (membership) — request shaping", () => { ); }); - it("removeMember puts target in the path and sends actor userId in the body (data)", async () => { + it("removeWorkspaceMember puts target in the path and sends actor userId in the body (data)", async () => { const { client, projectInstance } = makeClient(); - await removeMember(client, { + await removeWorkspaceMember(client, { workspaceId: "w1", targetUserId: "target1", userId: "actor1", @@ -194,9 +194,9 @@ describe("node-sdk workspaces (membership) — request shaping", () => { ); }); - it("removeFromSubtree puts target in the path and posts the acting userId", async () => { + it("removeWorkspaceMemberFromSubtree puts target in the path and posts the acting userId", async () => { const { client, projectInstance } = makeClient(); - await removeFromSubtree(client, { + await removeWorkspaceMemberFromSubtree(client, { workspaceId: "w1", targetUserId: "target1", userId: "actor1", @@ -209,9 +209,9 @@ describe("node-sdk workspaces (membership) — request shaping", () => { }); describe("node-sdk workspaces (invitations) — request shaping", () => { - it("createInvite strips workspaceId into the path and posts the rest (userId here is the TARGET)", async () => { + it("createWorkspaceInvite strips workspaceId into the path and posts the rest (userId here is the TARGET)", async () => { const { client, projectInstance } = makeClient(); - await createInvite(client, { + await createWorkspaceInvite(client, { workspaceId: "w1", userId: "invitee1", capabilities: ["invite"], @@ -224,9 +224,9 @@ describe("node-sdk workspaces (invitations) — request shaping", () => { ); }); - it("createInvite forwards an email-addressed invite body", async () => { + it("createWorkspaceInvite forwards an email-addressed invite body", async () => { const { client, projectInstance } = makeClient(); - await createInvite(client, { + await createWorkspaceInvite(client, { workspaceId: "w1", email: "New.Member@Example.com", rank: 1, @@ -237,51 +237,51 @@ describe("node-sdk workspaces (invitations) — request shaping", () => { ); }); - it("fetchInvites hits /workspaces/:id/invites", async () => { + it("fetchWorkspaceInvites hits /workspaces/:id/invites", async () => { const { client, projectInstance } = makeClient(); - await fetchInvites(client, { workspaceId: "w1" }); + await fetchWorkspaceInvites(client, { workspaceId: "w1" }); expect(projectInstance.get).toHaveBeenCalledWith("/workspaces/w1/invites"); }); - it("revokeInvite posts to the revoke route with an empty body", async () => { + it("revokeWorkspaceInvite posts to the revoke route with an empty body", async () => { const { client, projectInstance } = makeClient(); - await revokeInvite(client, { workspaceId: "w1", inviteId: "i1" }); + await revokeWorkspaceInvite(client, { workspaceId: "w1", inviteId: "i1" }); expect(projectInstance.post).toHaveBeenCalledWith( "/workspaces/w1/invites/i1/revoke", {} ); }); - it("resendInvite posts to the resend route with an empty body", async () => { + it("resendWorkspaceInvite posts to the resend route with an empty body", async () => { const { client, projectInstance } = makeClient(); - await resendInvite(client, { workspaceId: "w1", inviteId: "i1" }); + await resendWorkspaceInvite(client, { workspaceId: "w1", inviteId: "i1" }); expect(projectInstance.post).toHaveBeenCalledWith( "/workspaces/w1/invites/i1/resend", {} ); }); - it("acceptInvite posts the accepting userId to the non-:id-scoped accept route", async () => { + it("acceptWorkspaceInvite posts the accepting userId to the non-:id-scoped accept route", async () => { const { client, projectInstance } = makeClient(); - await acceptInvite(client, { inviteId: "i1", userId: "u1" }); + await acceptWorkspaceInvite(client, { inviteId: "i1", userId: "u1" }); expect(projectInstance.post).toHaveBeenCalledWith( "/workspace-invites/i1/accept", { userId: "u1" } ); }); - it("declineInvite posts the declining userId to the non-:id-scoped decline route", async () => { + it("declineWorkspaceInvite posts the declining userId to the non-:id-scoped decline route", async () => { const { client, projectInstance } = makeClient(); - await declineInvite(client, { inviteId: "i1", userId: "u1" }); + await declineWorkspaceInvite(client, { inviteId: "i1", userId: "u1" }); expect(projectInstance.post).toHaveBeenCalledWith( "/workspace-invites/i1/decline", { userId: "u1" } ); }); - it("fetchMyInvites hits /me/workspace-invites with the userId as a param", async () => { + it("fetchMyWorkspaceInvites hits /me/workspace-invites with the userId as a param", async () => { const { client, projectInstance } = makeClient(); - await fetchMyInvites(client, { userId: "u1" }); + await fetchMyWorkspaceInvites(client, { userId: "u1" }); expect(projectInstance.get).toHaveBeenCalledWith("/me/workspace-invites", { params: { userId: "u1" }, }); @@ -289,9 +289,9 @@ describe("node-sdk workspaces (invitations) — request shaping", () => { }); describe("node-sdk workspaces (authority) — request shaping", () => { - it("getAuthority hits the authority route with the target userId as a param", async () => { + it("fetchWorkspaceAuthority hits the authority route with the target userId as a param", async () => { const { client, projectInstance } = makeClient(); - await getAuthority(client, { workspaceId: "w1", userId: "u1" }); + await fetchWorkspaceAuthority(client, { workspaceId: "w1", userId: "u1" }); expect(projectInstance.get).toHaveBeenCalledWith( "/workspaces/w1/authority/me", { params: { userId: "u1" } } @@ -321,7 +321,7 @@ describe("node-sdk workspaces — response mapping", () => { ).resolves.toEqual(envelope); }); - it("fetchMembers returns response.data (roster or counts envelope)", async () => { + it("fetchWorkspaceMembers returns response.data (roster or counts envelope)", async () => { const { client, projectInstance } = makeClient(); const counts = { counts: { @@ -336,20 +336,20 @@ describe("node-sdk workspaces — response mapping", () => { }; projectInstance.get.mockResolvedValueOnce({ data: counts }); await expect( - fetchMembers(client, { workspaceId: "w1", countOnly: true }) + fetchWorkspaceMembers(client, { workspaceId: "w1", countOnly: true }) ).resolves.toEqual(counts); }); - it("acceptInvite returns response.data", async () => { + it("acceptWorkspaceInvite returns response.data", async () => { const { client, projectInstance } = makeClient(); const result = { success: true, workspaceId: "w1" }; projectInstance.post.mockResolvedValueOnce({ data: result }); await expect( - acceptInvite(client, { inviteId: "i1", userId: "u1" }) + acceptWorkspaceInvite(client, { inviteId: "i1", userId: "u1" }) ).resolves.toEqual(result); }); - it("getAuthority returns response.data", async () => { + it("fetchWorkspaceAuthority returns response.data", async () => { const { client, projectInstance } = makeClient(); const authority = { reasons: ["owner"], @@ -359,7 +359,7 @@ describe("node-sdk workspaces — response mapping", () => { }; projectInstance.get.mockResolvedValueOnce({ data: authority }); await expect( - getAuthority(client, { workspaceId: "w1", userId: "u1" }) + fetchWorkspaceAuthority(client, { workspaceId: "w1", userId: "u1" }) ).resolves.toEqual(authority); }); }); diff --git a/src/index.ts b/src/index.ts index 28230fa..d34fd17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,13 +155,16 @@ export type { WorkspaceCapability, WorkspaceInvitationStatus, WorkspaceAuthorityReason, + WorkspaceAuthorityReasonDetail, WorkspaceRosterReason, WorkspaceRosterEntry, WorkspaceRosterResponse, WorkspaceRosterCountsResponse, + WorkspaceStandingUser, WorkspaceMemberStanding, WorkspaceAuthority, - RemoveFromSubtreeResponse, + RemoveWorkspaceMemberFromSubtreeResponse, + SkippedWorkspace, } from "./interfaces/Workspace"; export type { Rule } from "./interfaces/Rule"; export type { Follow } from "./interfaces/Follow"; diff --git a/src/interfaces/Workspace.ts b/src/interfaces/Workspace.ts index abbe54c..cab7594 100644 --- a/src/interfaces/Workspace.ts +++ b/src/interfaces/Workspace.ts @@ -35,6 +35,28 @@ export type WorkspaceAuthorityReason = | "member" | "reach-holder"; +// One structured standing entry, as returned by the per-user standing read and +// the authority read. `viaWorkspaceId` names the ancestor responsible and is +// present on `ancestor-owner` / `reach-holder` only (`owner` / `member` are +// grants on the target workspace itself). A user may carry SEVERAL entries of +// the same type — one per granting ancestor. +// +// Modelled as a discriminated union so `type` narrows `viaWorkspaceId`: the two +// ancestor-derived reasons ALWAYS carry it and the two target-local reasons +// NEVER do — those are the only four combinations the server emits (see +// `resolveWorkspaceAuthority`). +export type WorkspaceAuthorityReasonDetail = + | { + // A grant on the target workspace itself — no ancestor is responsible. + type: "owner" | "member"; + viaWorkspaceId?: never; + } + | { + // A grant derived from an ancestor, which `viaWorkspaceId` names. + type: "ancestor-owner" | "reach-holder"; + viaWorkspaceId: string; + }; + export interface Workspace { id: string; name: string; @@ -95,6 +117,12 @@ export interface WorkspaceRosterReason { // `ancestor-owner`/`reach-holder` carry `viaWorkspaceId` (reach-holder also // `capabilities`); `descendant-member` carries `workspaceId` + `rank`/ // `capabilities`. `owner` carries none. + // + // The authority-bearing fields (`rank`, `capabilities`, `permissions`) are + // additionally OMITTED (absent, not null) on OTHER users' entries unless the + // caller operates people on the workspace (holds `invite`, `remove-member`, + // `edit-member-access` or `edit-member-profile`) or is an owner/ancestor-owner. + // The caller's OWN entry always carries them. rank?: number; capabilities?: string[]; permissions?: string[]; @@ -129,26 +157,75 @@ export interface WorkspaceRosterCountsResponse { } // The per-user standing read (`GET /workspaces/:id/members/:userId`). +/** + * The `user` carried by a standing read. Normally the full user record, but the + * server falls back to `{ id }` alone when the user row is gone (a deleted user + * with a lingering membership row is a reachable case), so every field except + * `id` may be absent. + */ +export type WorkspaceStandingUser = Pick & + Partial>; + export interface WorkspaceMemberStanding { - user: User; - reasons: WorkspaceAuthorityReason[]; - capabilities: string[]; - permissions: string[]; - rank: number | null; + user: WorkspaceStandingUser; + reasons: WorkspaceAuthorityReasonDetail[]; + // The authority-bearing fields are OMITTED (absent, not null) unless the + // caller operates people on the workspace (holds `invite`, `remove-member`, + // `edit-member-access` or `edit-member-profile`), is an owner/ancestor-owner, + // or is asking about THEMSELVES — a caller always sees their own access. + capabilities?: string[]; + permissions?: string[]; + rank?: number | null; title: string | null; metadata: Record; } // The authority-as-a-service read (`GET /workspaces/:id/authority/me`). export interface WorkspaceAuthority { - reasons: WorkspaceAuthorityReason[]; + reasons: WorkspaceAuthorityReasonDetail[]; capabilities: string[]; permissions: string[]; rank: number | null; } +/** + * One descendant the subtree sweep did NOT clear, where the target user still + * holds a direct membership. + * + * `id` / `name` are masked TOGETHER: an entry the acting user may see carries + * both, and one they may not carries `null` for both — the sweep reports THAT a + * membership survived without disclosing the existence or name of a sealed + * sub-workspace the actor has no authority over (the same sealing fence the + * descendant roster read applies). Modelled as a discriminated union so a + * `null` check on `id` narrows `name` too. + */ +export type SkippedWorkspace = + | { + // Visible: the actor has standing on this workspace. + id: string; + name: string; + /** Why it was skipped. `out-of-reach`: the actor's authority does not extend there. */ + reason: "out-of-reach"; + } + | { + // Sealed: the actor has no standing there, so its identity is withheld. + id: null; + name: null; + /** Why it was skipped. `out-of-reach`: the actor's authority does not extend there. */ + reason: "out-of-reach"; + }; + // Subtree-offboarding response (`POST /members/:userId/remove-from-subtree`). -export interface RemoveFromSubtreeResponse { +export interface RemoveWorkspaceMemberFromSubtreeResponse { removedCount: number; removed: { workspaceId: string; userId: string }[]; + /** + * How many descendant memberships the target RETAINED because the sweep could + * not reach them. Always `0` for an owner / ancestor-owner / privileged key. + * A non-zero value means the offboarding is PARTIAL — never conclude a user is + * fully removed from `removedCount` alone. + */ + skippedCount: number; + /** One entry per retained membership; `skippedCount === skipped.length`. */ + skipped: SkippedWorkspace[]; } diff --git a/src/modules/workspaces/acceptInvite.ts b/src/modules/workspaces/acceptWorkspaceInvite.ts similarity index 66% rename from src/modules/workspaces/acceptInvite.ts rename to src/modules/workspaces/acceptWorkspaceInvite.ts index 1191099..c90a217 100644 --- a/src/modules/workspaces/acceptInvite.ts +++ b/src/modules/workspaces/acceptWorkspaceInvite.ts @@ -1,13 +1,13 @@ import { SublayHttpClient } from "../../core/client"; -export interface AcceptInviteProps { +export interface AcceptWorkspaceInviteProps { inviteId: string; // The accepting user (must BE the invite's target; must have a verified // email). Required for the service key (act-as-user). userId: string; } -export interface AcceptInviteResponse { +export interface AcceptWorkspaceInviteResponse { success: boolean; workspaceId: string; } @@ -17,12 +17,12 @@ export interface AcceptInviteResponse { * Idempotent: if the user is already a member/owner, marks `accepted` with no * duplicate row. */ -export async function acceptInvite( +export async function acceptWorkspaceInvite( client: SublayHttpClient, - data: AcceptInviteProps -): Promise { + data: AcceptWorkspaceInviteProps +): Promise { const { inviteId, userId } = data; - const response = await client.projectInstance.post( + const response = await client.projectInstance.post( `/workspace-invites/${inviteId}/accept`, { userId } ); diff --git a/src/modules/workspaces/createInvite.ts b/src/modules/workspaces/createWorkspaceInvite.ts similarity index 85% rename from src/modules/workspaces/createInvite.ts rename to src/modules/workspaces/createWorkspaceInvite.ts index c455025..62bf6fd 100644 --- a/src/modules/workspaces/createInvite.ts +++ b/src/modules/workspaces/createWorkspaceInvite.ts @@ -1,7 +1,7 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceInvitation } from "../../interfaces/Workspace"; -export interface CreateInviteProps { +export interface CreateWorkspaceInviteProps { workspaceId: string; // Address the invitee by exactly one of: `email`, `userId`, or `username`. // (Here `userId` is the INVITE TARGET, not an acting user — the inviter is the @@ -15,9 +15,9 @@ export interface CreateInviteProps { title?: string | null; } -export async function createInvite( +export async function createWorkspaceInvite( client: SublayHttpClient, - data: CreateInviteProps + data: CreateWorkspaceInviteProps ): Promise { const { workspaceId, ...body } = data; const response = await client.projectInstance.post( diff --git a/src/modules/workspaces/declineInvite.ts b/src/modules/workspaces/declineInvite.ts deleted file mode 100644 index c745be4..0000000 --- a/src/modules/workspaces/declineInvite.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { SublayHttpClient } from "../../core/client"; - -export interface DeclineInviteProps { - inviteId: string; - // The declining user (must BE the invite's target). Not verification-gated. - // Required for the service key (act-as-user). - userId: string; -} - -export interface DeclineInviteResponse { - success: boolean; -} - -/** Decline an invitation — identity-matched (not verification-gated). */ -export async function declineInvite( - client: SublayHttpClient, - data: DeclineInviteProps -): Promise { - const { inviteId, userId } = data; - const response = await client.projectInstance.post( - `/workspace-invites/${inviteId}/decline`, - { userId } - ); - return response.data; -} diff --git a/src/modules/workspaces/declineWorkspaceInvite.ts b/src/modules/workspaces/declineWorkspaceInvite.ts new file mode 100644 index 0000000..36c04b9 --- /dev/null +++ b/src/modules/workspaces/declineWorkspaceInvite.ts @@ -0,0 +1,26 @@ +import { SublayHttpClient } from "../../core/client"; + +export interface DeclineWorkspaceInviteProps { + inviteId: string; + // The declining user (must BE the invite's target, and must be verified — + // a service key does NOT bypass the verified-email gate). + // Required for the service key (act-as-user). + userId: string; +} + +export interface DeclineWorkspaceInviteResponse { + success: boolean; +} + +/** Decline an invitation — identity-matched + verified-email-gated (same as accept). */ +export async function declineWorkspaceInvite( + client: SublayHttpClient, + data: DeclineWorkspaceInviteProps +): Promise { + const { inviteId, userId } = data; + const response = await client.projectInstance.post( + `/workspace-invites/${inviteId}/decline`, + { userId } + ); + return response.data; +} diff --git a/src/modules/workspaces/fetchMyInvites.ts b/src/modules/workspaces/fetchMyWorkspaceInvites.ts similarity index 61% rename from src/modules/workspaces/fetchMyInvites.ts rename to src/modules/workspaces/fetchMyWorkspaceInvites.ts index 6af40b7..e948b72 100644 --- a/src/modules/workspaces/fetchMyInvites.ts +++ b/src/modules/workspaces/fetchMyWorkspaceInvites.ts @@ -1,27 +1,27 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceInvitation } from "../../interfaces/Workspace"; -export interface FetchMyInvitesProps { +export interface FetchMyWorkspaceInvitesProps { // The user whose live pending invites to read (matched by `userId`). Required // for the service key (act-as-user). userId: string; } -export interface FetchMyInvitesResponse { +export interface FetchMyWorkspaceInvitesResponse { data: WorkspaceInvitation[]; } /** * The user's LIVE pending invites (`status='pending' AND expiresAt > now`), * matched by `userId`. Surfacing is NOT verification-gated (the verified check - * applies only at accept). + * applies when the user ACTS on an invite — accept or decline). */ -export async function fetchMyInvites( +export async function fetchMyWorkspaceInvites( client: SublayHttpClient, - data: FetchMyInvitesProps -): Promise { + data: FetchMyWorkspaceInvitesProps +): Promise { const { userId } = data; - const response = await client.projectInstance.get( + const response = await client.projectInstance.get( "/me/workspace-invites", { params: { userId } } ); diff --git a/src/modules/workspaces/getAuthority.ts b/src/modules/workspaces/fetchWorkspaceAuthority.ts similarity index 86% rename from src/modules/workspaces/getAuthority.ts rename to src/modules/workspaces/fetchWorkspaceAuthority.ts index 1a9dc29..5b75d35 100644 --- a/src/modules/workspaces/getAuthority.ts +++ b/src/modules/workspaces/fetchWorkspaceAuthority.ts @@ -1,7 +1,7 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceAuthority } from "../../interfaces/Workspace"; -export interface GetAuthorityProps { +export interface FetchWorkspaceAuthorityProps { workspaceId: string; // The user whose resolved standing to read. With a service key this is the // target user (the service-key `userId` variant); required here. @@ -14,9 +14,9 @@ export interface GetAuthorityProps { * workspace. A permission check is a one-line `.includes()` on the result; * Sublay never consumes the developer's opaque permissions. */ -export async function getAuthority( +export async function fetchWorkspaceAuthority( client: SublayHttpClient, - data: GetAuthorityProps + data: FetchWorkspaceAuthorityProps ): Promise { const { workspaceId, userId } = data; const response = await client.projectInstance.get( diff --git a/src/modules/workspaces/fetchInvites.ts b/src/modules/workspaces/fetchWorkspaceInvites.ts similarity index 59% rename from src/modules/workspaces/fetchInvites.ts rename to src/modules/workspaces/fetchWorkspaceInvites.ts index 58627ad..37f067c 100644 --- a/src/modules/workspaces/fetchInvites.ts +++ b/src/modules/workspaces/fetchWorkspaceInvites.ts @@ -1,11 +1,11 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceInvitation } from "../../interfaces/Workspace"; -export interface FetchInvitesProps { +export interface FetchWorkspaceInvitesProps { workspaceId: string; } -export interface FetchInvitesResponse { +export interface FetchWorkspaceInvitesResponse { data: WorkspaceInvitation[]; } @@ -13,12 +13,12 @@ export interface FetchInvitesResponse { * List a workspace's LIVE pending invites (`status='pending' AND expiresAt > * now`). Requires the `invite` capability (or owner). */ -export async function fetchInvites( +export async function fetchWorkspaceInvites( client: SublayHttpClient, - data: FetchInvitesProps -): Promise { + data: FetchWorkspaceInvitesProps +): Promise { const { workspaceId } = data; - const response = await client.projectInstance.get( + const response = await client.projectInstance.get( `/workspaces/${workspaceId}/invites` ); return response.data; diff --git a/src/modules/workspaces/fetchMemberStanding.ts b/src/modules/workspaces/fetchWorkspaceMemberStanding.ts similarity index 77% rename from src/modules/workspaces/fetchMemberStanding.ts rename to src/modules/workspaces/fetchWorkspaceMemberStanding.ts index e806382..3f34771 100644 --- a/src/modules/workspaces/fetchMemberStanding.ts +++ b/src/modules/workspaces/fetchWorkspaceMemberStanding.ts @@ -1,15 +1,15 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceMemberStanding } from "../../interfaces/Workspace"; -export interface FetchMemberStandingProps { +export interface FetchWorkspaceMemberStandingProps { workspaceId: string; // The target user whose standing to read (path param). targetUserId: string; } -export async function fetchMemberStanding( +export async function fetchWorkspaceMemberStanding( client: SublayHttpClient, - data: FetchMemberStandingProps + data: FetchWorkspaceMemberStandingProps ): Promise { const { workspaceId, targetUserId } = data; const response = diff --git a/src/modules/workspaces/fetchMembers.ts b/src/modules/workspaces/fetchWorkspaceMembers.ts similarity index 88% rename from src/modules/workspaces/fetchMembers.ts rename to src/modules/workspaces/fetchWorkspaceMembers.ts index a98c387..31df66d 100644 --- a/src/modules/workspaces/fetchMembers.ts +++ b/src/modules/workspaces/fetchWorkspaceMembers.ts @@ -4,7 +4,7 @@ import { WorkspaceRosterCountsResponse, } from "../../interfaces/Workspace"; -export interface FetchMembersProps { +export interface FetchWorkspaceMembersProps { workspaceId: string; // Comma-separated add-on buckets: `ancestorOwners`, `reachHolders`, // `descendants`. Default returns owner + direct members only. @@ -18,9 +18,9 @@ export interface FetchMembersProps { * array. Always returned in full (never paginated). With `countOnly=true` the * shape is `WorkspaceRosterCountsResponse` instead. */ -export async function fetchMembers( +export async function fetchWorkspaceMembers( client: SublayHttpClient, - data: FetchMembersProps + data: FetchWorkspaceMembersProps ): Promise { const { workspaceId, ...params } = data; const response = await client.projectInstance.get< diff --git a/src/modules/workspaces/index.ts b/src/modules/workspaces/index.ts index efa531e..24992d4 100644 --- a/src/modules/workspaces/index.ts +++ b/src/modules/workspaces/index.ts @@ -3,26 +3,26 @@ export { createWorkspace } from "./createWorkspace"; export { fetchWorkspace } from "./fetchWorkspace"; export { fetchManyWorkspaces } from "./fetchManyWorkspaces"; export { updateWorkspace } from "./updateWorkspace"; -export { updateInheritFlag } from "./updateInheritFlag"; +export { updateWorkspaceInheritFlag } from "./updateWorkspaceInheritFlag"; export { deleteWorkspace } from "./deleteWorkspace"; -export { transferOwnership } from "./transferOwnership"; +export { transferWorkspaceOwnership } from "./transferWorkspaceOwnership"; // Membership -export { fetchMembers } from "./fetchMembers"; -export { fetchMemberStanding } from "./fetchMemberStanding"; -export { updateMember } from "./updateMember"; -export { removeMember } from "./removeMember"; +export { fetchWorkspaceMembers } from "./fetchWorkspaceMembers"; +export { fetchWorkspaceMemberStanding } from "./fetchWorkspaceMemberStanding"; +export { updateWorkspaceMember } from "./updateWorkspaceMember"; +export { removeWorkspaceMember } from "./removeWorkspaceMember"; export { leaveWorkspace } from "./leaveWorkspace"; -export { removeFromSubtree } from "./removeFromSubtree"; +export { removeWorkspaceMemberFromSubtree } from "./removeWorkspaceMemberFromSubtree"; // Invitations -export { createInvite } from "./createInvite"; -export { fetchInvites } from "./fetchInvites"; -export { revokeInvite } from "./revokeInvite"; -export { resendInvite } from "./resendInvite"; -export { acceptInvite } from "./acceptInvite"; -export { declineInvite } from "./declineInvite"; -export { fetchMyInvites } from "./fetchMyInvites"; +export { createWorkspaceInvite } from "./createWorkspaceInvite"; +export { fetchWorkspaceInvites } from "./fetchWorkspaceInvites"; +export { revokeWorkspaceInvite } from "./revokeWorkspaceInvite"; +export { resendWorkspaceInvite } from "./resendWorkspaceInvite"; +export { acceptWorkspaceInvite } from "./acceptWorkspaceInvite"; +export { declineWorkspaceInvite } from "./declineWorkspaceInvite"; +export { fetchMyWorkspaceInvites } from "./fetchMyWorkspaceInvites"; // Authority-as-a-service -export { getAuthority } from "./getAuthority"; +export { fetchWorkspaceAuthority } from "./fetchWorkspaceAuthority"; diff --git a/src/modules/workspaces/removeMember.ts b/src/modules/workspaces/removeWorkspaceMember.ts similarity index 83% rename from src/modules/workspaces/removeMember.ts rename to src/modules/workspaces/removeWorkspaceMember.ts index f2c0d21..938171e 100644 --- a/src/modules/workspaces/removeMember.ts +++ b/src/modules/workspaces/removeWorkspaceMember.ts @@ -1,6 +1,6 @@ import { SublayHttpClient } from "../../core/client"; -export interface RemoveMemberProps { +export interface RemoveWorkspaceMemberProps { workspaceId: string; // The member to remove (path param). targetUserId: string; @@ -9,9 +9,9 @@ export interface RemoveMemberProps { userId: string; } -export async function removeMember( +export async function removeWorkspaceMember( client: SublayHttpClient, - data: RemoveMemberProps + data: RemoveWorkspaceMemberProps ): Promise { const { workspaceId, targetUserId, userId } = data; // The controller resolves the acting user from the request body; send it there diff --git a/src/modules/workspaces/removeFromSubtree.ts b/src/modules/workspaces/removeWorkspaceMemberFromSubtree.ts similarity index 55% rename from src/modules/workspaces/removeFromSubtree.ts rename to src/modules/workspaces/removeWorkspaceMemberFromSubtree.ts index 67ff4e4..7b9deba 100644 --- a/src/modules/workspaces/removeFromSubtree.ts +++ b/src/modules/workspaces/removeWorkspaceMemberFromSubtree.ts @@ -1,7 +1,7 @@ import { SublayHttpClient } from "../../core/client"; -import { RemoveFromSubtreeResponse } from "../../interfaces/Workspace"; +import { RemoveWorkspaceMemberFromSubtreeResponse } from "../../interfaces/Workspace"; -export interface RemoveFromSubtreeProps { +export interface RemoveWorkspaceMemberFromSubtreeProps { workspaceId: string; // The user to offboard from this node and every descendant (path param). targetUserId: string; @@ -14,14 +14,18 @@ export interface RemoveFromSubtreeProps { * Removes the target user's direct memberships on this workspace and every * descendant. Blocks (409 `workspace/owns-descendants`) with a report if the * user OWNS any descendant workspace — transfer or delete those first. + * + * The response also carries `skippedCount` / `skipped` (memberships the sweep + * could NOT reach). A service key reaches the whole subtree, so these are `0` / + * `[]` here — but assert on them rather than reading `removedCount` alone. */ -export async function removeFromSubtree( +export async function removeWorkspaceMemberFromSubtree( client: SublayHttpClient, - data: RemoveFromSubtreeProps -): Promise { + data: RemoveWorkspaceMemberFromSubtreeProps +): Promise { const { workspaceId, targetUserId, userId } = data; const response = - await client.projectInstance.post( + await client.projectInstance.post( `/workspaces/${workspaceId}/members/${targetUserId}/remove-from-subtree`, { userId } ); diff --git a/src/modules/workspaces/resendInvite.ts b/src/modules/workspaces/resendWorkspaceInvite.ts similarity index 84% rename from src/modules/workspaces/resendInvite.ts rename to src/modules/workspaces/resendWorkspaceInvite.ts index 8b75c7c..3442360 100644 --- a/src/modules/workspaces/resendInvite.ts +++ b/src/modules/workspaces/resendWorkspaceInvite.ts @@ -1,7 +1,7 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceInvitation } from "../../interfaces/Workspace"; -export interface ResendInviteProps { +export interface ResendWorkspaceInviteProps { workspaceId: string; inviteId: string; } @@ -11,9 +11,9 @@ export interface ResendInviteProps { * past `expiresAt` — resets a 14-day expiry and resends the email). A terminal * invite (accepted/declined/revoked) → 409. Requires `invite` (or owner). */ -export async function resendInvite( +export async function resendWorkspaceInvite( client: SublayHttpClient, - data: ResendInviteProps + data: ResendWorkspaceInviteProps ): Promise { const { workspaceId, inviteId } = data; const response = await client.projectInstance.post( diff --git a/src/modules/workspaces/revokeInvite.ts b/src/modules/workspaces/revokeWorkspaceInvite.ts similarity index 55% rename from src/modules/workspaces/revokeInvite.ts rename to src/modules/workspaces/revokeWorkspaceInvite.ts index 99da166..4641648 100644 --- a/src/modules/workspaces/revokeInvite.ts +++ b/src/modules/workspaces/revokeWorkspaceInvite.ts @@ -1,21 +1,21 @@ import { SublayHttpClient } from "../../core/client"; -export interface RevokeInviteProps { +export interface RevokeWorkspaceInviteProps { workspaceId: string; inviteId: string; } -export interface RevokeInviteResponse { +export interface RevokeWorkspaceInviteResponse { success: boolean; } /** Revoke a pending invitation. Requires the `invite` capability (or owner). */ -export async function revokeInvite( +export async function revokeWorkspaceInvite( client: SublayHttpClient, - data: RevokeInviteProps -): Promise { + data: RevokeWorkspaceInviteProps +): Promise { const { workspaceId, inviteId } = data; - const response = await client.projectInstance.post( + const response = await client.projectInstance.post( `/workspaces/${workspaceId}/invites/${inviteId}/revoke`, {} ); diff --git a/src/modules/workspaces/transferOwnership.ts b/src/modules/workspaces/transferWorkspaceOwnership.ts similarity index 87% rename from src/modules/workspaces/transferOwnership.ts rename to src/modules/workspaces/transferWorkspaceOwnership.ts index d459a38..1829d84 100644 --- a/src/modules/workspaces/transferOwnership.ts +++ b/src/modules/workspaces/transferWorkspaceOwnership.ts @@ -1,7 +1,7 @@ import { SublayHttpClient } from "../../core/client"; import { Workspace } from "../../interfaces/Workspace"; -export interface TransferOwnershipProps { +export interface TransferWorkspaceOwnershipProps { workspaceId: string; // The acting user (own owner or an ancestor owner). Required for the service // key (act-as-user). @@ -16,9 +16,9 @@ export interface TransferOwnershipProps { previousOwnerCapabilities?: string[]; } -export async function transferOwnership( +export async function transferWorkspaceOwnership( client: SublayHttpClient, - data: TransferOwnershipProps + data: TransferWorkspaceOwnershipProps ): Promise { const { workspaceId, ...body } = data; const response = await client.projectInstance.post( diff --git a/src/modules/workspaces/updateInheritFlag.ts b/src/modules/workspaces/updateWorkspaceInheritFlag.ts similarity index 79% rename from src/modules/workspaces/updateInheritFlag.ts rename to src/modules/workspaces/updateWorkspaceInheritFlag.ts index d1810db..8bf1231 100644 --- a/src/modules/workspaces/updateInheritFlag.ts +++ b/src/modules/workspaces/updateWorkspaceInheritFlag.ts @@ -1,16 +1,16 @@ import { SublayHttpClient } from "../../core/client"; import { Workspace } from "../../interfaces/Workspace"; -export interface UpdateInheritFlagProps { +export interface UpdateWorkspaceInheritFlagProps { workspaceId: string; // Owner-only flip; the acting user (must be own owner or an ancestor owner). userId: string; inheritsFromParent: boolean; } -export async function updateInheritFlag( +export async function updateWorkspaceInheritFlag( client: SublayHttpClient, - data: UpdateInheritFlagProps + data: UpdateWorkspaceInheritFlagProps ): Promise { const { workspaceId, userId, inheritsFromParent } = data; const response = await client.projectInstance.patch( diff --git a/src/modules/workspaces/updateMember.ts b/src/modules/workspaces/updateWorkspaceMember.ts similarity index 89% rename from src/modules/workspaces/updateMember.ts rename to src/modules/workspaces/updateWorkspaceMember.ts index 4096ddd..aac58c2 100644 --- a/src/modules/workspaces/updateMember.ts +++ b/src/modules/workspaces/updateWorkspaceMember.ts @@ -1,7 +1,7 @@ import { SublayHttpClient } from "../../core/client"; import { WorkspaceMember } from "../../interfaces/Workspace"; -export interface UpdateMemberProps { +export interface UpdateWorkspaceMemberProps { workspaceId: string; // The target member's user id (path param). targetUserId: string; @@ -18,9 +18,9 @@ export interface UpdateMemberProps { metadata?: Record; } -export async function updateMember( +export async function updateWorkspaceMember( client: SublayHttpClient, - data: UpdateMemberProps + data: UpdateWorkspaceMemberProps ): Promise { const { workspaceId, targetUserId, ...body } = data; const response = await client.projectInstance.patch(