@meetreeve/module-team 0.0.0-stage → 0.1.0

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.
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # @meetreeve/module-team
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Reeve.Team module (DEV-9998, C2 of epic DEV-9983). Two halves:
4
+
5
+ * **`.` / `./client`**: a framework-agnostic, server-safe typed client,
6
+ `createTeamClient({ fetch })`, over the org-members routes. No React, no
7
+ Next.js.
8
+ * **`./config`**: `TeamMembersPanel`, the operator surface: members with a
9
+ role picker and remove, the invite dialog, pending invitations with
10
+ revoke/resend, and the seat count.
11
+
12
+ Versioned in lockstep with the Python `reeve-module-team` package. Both ship
13
+ the same MAJOR.MINOR.PATCH.
14
+
15
+ | Method | Route |
16
+ | --- | --- |
17
+ | `listMembers(orgId)` | `GET /api/org/members` |
18
+ | `updateMemberRole(orgId, memberId, role)` | `PUT /api/org/members/{id}/role` |
19
+ | `removeMember(orgId, memberId)` | `DELETE /api/org/members/{id}` |
20
+ | `inviteMember(orgId, email, role)` | `POST /api/org/invite` |
21
+ | `listInvitations(orgId)` | `GET /api/org/invitations` |
22
+ | `revokeInvitation(orgId, invitationId)` | `DELETE /api/org/invitations/{id}` |
23
+ | `resendInvitation(orgId, invitationId)` | `POST /api/org/invitations/{id}/resend` |
24
+
25
+ ## The transport is the host's
26
+
27
+ `createTeamClient` takes a JSON fetcher, `(path, init) => Promise<body>`,
28
+ that resolves the parsed body and rejects on a non-2xx. reeve-frontend's
29
+ `productFetch` already has that shape, and it owns auth, the 401 refresh and
30
+ timeouts. A spawned tenant passes its own.
31
+
32
+ ## The org is never ambient (DEV-8592)
33
+
34
+ Every client method takes the org it acts on and sends it as
35
+ `X-Reeve-Org-Id`. `TeamMembersPanel` requires `orgId` and keys its stateful
36
+ body on it. Switching orgs remounts the panel, so the previous org's
37
+ members, open dialogs and notices are dropped at once. A response for the
38
+ previous org that lands late is discarded.
39
+
40
+ ```tsx
41
+ import { createTeamClient } from "@meetreeve/module-team/client";
42
+ import { TeamMembersPanel } from "@meetreeve/module-team/config";
43
+
44
+ const client = createTeamClient({ fetch: (path, init) => productFetch(path, init) });
45
+
46
+ <TeamMembersPanel
47
+ client={client}
48
+ orgId={activeOrgId}
49
+ inviteRoles={["admin", "member"]} // default: admin, member, viewer
50
+ currentUserEmail={user?.email}
51
+ renderMemberActions={(m) => <BudgetButton member={m} />} // host-only extras
52
+ onNotice={(n) => toast[n.kind](n.message)} // optional; also shown inline
53
+ />;
54
+ ```
55
+
56
+ `inviteRoles` scopes both the invite dialog and the per-member role picker.
57
+ Owners get no role picker and no remove control. The backend still enforces
58
+ every rule.
59
+
60
+ The panel has no runtime dependencies beyond `react`/`react-dom`. It is
61
+ styled with Tailwind utility classes over shadcn tokens (`bg-card`,
62
+ `text-muted-foreground`, `border-border`, ...), so the host's Tailwind must
63
+ scan this package.
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ npm ci && npm test && npm run typecheck && npm run build && npm run check:esm
69
+ ```
70
+
71
+ reeve-frontend aliases `@meetreeve/module-team/{config,client}` to this
72
+ package's source until the first publish. Running `npm ci` here installs a
73
+ second React under `packages/module-team/node_modules`. Move that directory
74
+ aside before you run the root vitest suite.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Roles the org-members router knows. `billing_manager` is a legacy alias
3
+ * the backend still accepts; no surface offers it from the invite dialog.
4
+ * Rows may still carry a role this union doesn't name, so row fields are
5
+ * typed `string` and the panel renders unknown roles verbatim.
6
+ */
7
+ type TeamRole = "owner" | "admin" | "billing_manager" | "member" | "viewer";
8
+ /** A role the invite dialog and the role picker can hand out (never `owner`). */
9
+ type AssignableRole = Exclude<TeamRole, "owner" | "billing_manager">;
10
+ /** One live member of the org (`OrganizationMember` row). `GET /api/org/members`. */
11
+ interface TeamMember {
12
+ /** OrganizationMember.id — the id every member route takes. */
13
+ id: string;
14
+ user_id: string;
15
+ email: string | null;
16
+ role: string;
17
+ is_default: boolean;
18
+ /** ISO-8601. */
19
+ created_at: string;
20
+ }
21
+ /** One invitation record. `GET /api/org/invitations`. */
22
+ interface Invitation {
23
+ id: string;
24
+ email: string;
25
+ role: string;
26
+ invited_by_email: string | null;
27
+ /** pending | accepted | expired | revoked */
28
+ status: string;
29
+ /** ISO-8601. */
30
+ created_at: string;
31
+ /** ISO-8601. */
32
+ expires_at: string;
33
+ accepted_at: string | null;
34
+ }
35
+ /** `POST /api/org/invite`. */
36
+ interface InviteMemberResponse {
37
+ message: string;
38
+ email: string;
39
+ role: string;
40
+ invitation_id?: string;
41
+ }
42
+ /**
43
+ * The data-access boundary the panel is built against. Every method takes
44
+ * the org it acts on: there is no ambient "current org" anywhere in this
45
+ * package, so a request can never be issued for an org other than the one
46
+ * the caller named (DEV-8592). `createTeamClient` is the reference
47
+ * implementation; a host may supply its own.
48
+ */
49
+ interface TeamClient {
50
+ listMembers(orgId: string): Promise<TeamMember[]>;
51
+ updateMemberRole(orgId: string, memberId: string, role: string): Promise<void>;
52
+ removeMember(orgId: string, memberId: string): Promise<void>;
53
+ inviteMember(orgId: string, email: string, role: string): Promise<InviteMemberResponse>;
54
+ listInvitations(orgId: string): Promise<Invitation[]>;
55
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
56
+ resendInvitation(orgId: string, invitationId: string): Promise<void>;
57
+ }
58
+
59
+ /**
60
+ * Framework-agnostic Reeve.Team client (DEV-9998, C2 of epic DEV-9983).
61
+ *
62
+ * The server-safe half: no React, no Next.js. The only import is type-only,
63
+ * erased at build time.
64
+ *
65
+ * Contract: reeve-services' org-members router, ported route-for-route from
66
+ * reeve-frontend's `src/lib/api/team.ts`:
67
+ *
68
+ * GET /api/org/members -> TeamMember[]
69
+ * PUT /api/org/members/{id}/role {role} -> 2xx
70
+ * DELETE /api/org/members/{id} -> 2xx
71
+ * POST /api/org/invite {email, role} -> InviteMemberResponse
72
+ * GET /api/org/invitations -> Invitation[]
73
+ * DELETE /api/org/invitations/{id} -> 2xx
74
+ * POST /api/org/invitations/{id}/resend -> 2xx (resets the 7-day expiry)
75
+ *
76
+ * Transport is the HOST's: `createTeamClient({ fetch })` takes a JSON
77
+ * fetcher (reeve-frontend passes `productFetch`, which owns auth, 401
78
+ * refresh and timeouts). The org is never ambient. Every method takes the
79
+ * org it acts on and stamps it as `X-Reeve-Org-Id`, the header the backend's
80
+ * tenant resolver reads, so a request is keyed on the org its caller named
81
+ * rather than on whatever org a global pin held when it went out (DEV-8592).
82
+ */
83
+
84
+ /** The request header the backend tenant resolver reads the org assertion from. */
85
+ declare const ORG_HEADER = "X-Reeve-Org-Id";
86
+ /** What the client hands the host fetcher. `path` is relative to the API origin. */
87
+ type TeamRequestInit = {
88
+ method: "GET" | "POST" | "PUT" | "DELETE";
89
+ headers: Record<string, string>;
90
+ /** JSON-encoded, present only on requests that carry a body. */
91
+ body?: string;
92
+ };
93
+ /**
94
+ * A host-supplied JSON fetcher: resolve with the parsed response body (any
95
+ * value for an empty 2xx), reject on a non-2xx. reeve-frontend's
96
+ * `productFetch` has exactly this shape.
97
+ */
98
+ type TeamFetch = (path: string, init: TeamRequestInit) => Promise<unknown>;
99
+ type TeamClientOptions = {
100
+ fetch: TeamFetch;
101
+ };
102
+ declare function createTeamClient({ fetch }: TeamClientOptions): TeamClient;
103
+
104
+ export { type AssignableRole, type Invitation, type InviteMemberResponse, ORG_HEADER, type TeamClient, type TeamClientOptions, type TeamFetch, type TeamMember, type TeamRequestInit, type TeamRole, createTeamClient };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Roles the org-members router knows. `billing_manager` is a legacy alias
3
+ * the backend still accepts; no surface offers it from the invite dialog.
4
+ * Rows may still carry a role this union doesn't name, so row fields are
5
+ * typed `string` and the panel renders unknown roles verbatim.
6
+ */
7
+ type TeamRole = "owner" | "admin" | "billing_manager" | "member" | "viewer";
8
+ /** A role the invite dialog and the role picker can hand out (never `owner`). */
9
+ type AssignableRole = Exclude<TeamRole, "owner" | "billing_manager">;
10
+ /** One live member of the org (`OrganizationMember` row). `GET /api/org/members`. */
11
+ interface TeamMember {
12
+ /** OrganizationMember.id — the id every member route takes. */
13
+ id: string;
14
+ user_id: string;
15
+ email: string | null;
16
+ role: string;
17
+ is_default: boolean;
18
+ /** ISO-8601. */
19
+ created_at: string;
20
+ }
21
+ /** One invitation record. `GET /api/org/invitations`. */
22
+ interface Invitation {
23
+ id: string;
24
+ email: string;
25
+ role: string;
26
+ invited_by_email: string | null;
27
+ /** pending | accepted | expired | revoked */
28
+ status: string;
29
+ /** ISO-8601. */
30
+ created_at: string;
31
+ /** ISO-8601. */
32
+ expires_at: string;
33
+ accepted_at: string | null;
34
+ }
35
+ /** `POST /api/org/invite`. */
36
+ interface InviteMemberResponse {
37
+ message: string;
38
+ email: string;
39
+ role: string;
40
+ invitation_id?: string;
41
+ }
42
+ /**
43
+ * The data-access boundary the panel is built against. Every method takes
44
+ * the org it acts on: there is no ambient "current org" anywhere in this
45
+ * package, so a request can never be issued for an org other than the one
46
+ * the caller named (DEV-8592). `createTeamClient` is the reference
47
+ * implementation; a host may supply its own.
48
+ */
49
+ interface TeamClient {
50
+ listMembers(orgId: string): Promise<TeamMember[]>;
51
+ updateMemberRole(orgId: string, memberId: string, role: string): Promise<void>;
52
+ removeMember(orgId: string, memberId: string): Promise<void>;
53
+ inviteMember(orgId: string, email: string, role: string): Promise<InviteMemberResponse>;
54
+ listInvitations(orgId: string): Promise<Invitation[]>;
55
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
56
+ resendInvitation(orgId: string, invitationId: string): Promise<void>;
57
+ }
58
+
59
+ /**
60
+ * Framework-agnostic Reeve.Team client (DEV-9998, C2 of epic DEV-9983).
61
+ *
62
+ * The server-safe half: no React, no Next.js. The only import is type-only,
63
+ * erased at build time.
64
+ *
65
+ * Contract: reeve-services' org-members router, ported route-for-route from
66
+ * reeve-frontend's `src/lib/api/team.ts`:
67
+ *
68
+ * GET /api/org/members -> TeamMember[]
69
+ * PUT /api/org/members/{id}/role {role} -> 2xx
70
+ * DELETE /api/org/members/{id} -> 2xx
71
+ * POST /api/org/invite {email, role} -> InviteMemberResponse
72
+ * GET /api/org/invitations -> Invitation[]
73
+ * DELETE /api/org/invitations/{id} -> 2xx
74
+ * POST /api/org/invitations/{id}/resend -> 2xx (resets the 7-day expiry)
75
+ *
76
+ * Transport is the HOST's: `createTeamClient({ fetch })` takes a JSON
77
+ * fetcher (reeve-frontend passes `productFetch`, which owns auth, 401
78
+ * refresh and timeouts). The org is never ambient. Every method takes the
79
+ * org it acts on and stamps it as `X-Reeve-Org-Id`, the header the backend's
80
+ * tenant resolver reads, so a request is keyed on the org its caller named
81
+ * rather than on whatever org a global pin held when it went out (DEV-8592).
82
+ */
83
+
84
+ /** The request header the backend tenant resolver reads the org assertion from. */
85
+ declare const ORG_HEADER = "X-Reeve-Org-Id";
86
+ /** What the client hands the host fetcher. `path` is relative to the API origin. */
87
+ type TeamRequestInit = {
88
+ method: "GET" | "POST" | "PUT" | "DELETE";
89
+ headers: Record<string, string>;
90
+ /** JSON-encoded, present only on requests that carry a body. */
91
+ body?: string;
92
+ };
93
+ /**
94
+ * A host-supplied JSON fetcher: resolve with the parsed response body (any
95
+ * value for an empty 2xx), reject on a non-2xx. reeve-frontend's
96
+ * `productFetch` has exactly this shape.
97
+ */
98
+ type TeamFetch = (path: string, init: TeamRequestInit) => Promise<unknown>;
99
+ type TeamClientOptions = {
100
+ fetch: TeamFetch;
101
+ };
102
+ declare function createTeamClient({ fetch }: TeamClientOptions): TeamClient;
103
+
104
+ export { type AssignableRole, type Invitation, type InviteMemberResponse, ORG_HEADER, type TeamClient, type TeamClientOptions, type TeamFetch, type TeamMember, type TeamRequestInit, type TeamRole, createTeamClient };
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ ORG_HEADER: () => ORG_HEADER,
24
+ createTeamClient: () => createTeamClient
25
+ });
26
+ module.exports = __toCommonJS(client_exports);
27
+ var ORG_HEADER = "X-Reeve-Org-Id";
28
+ var ORG_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
29
+ function orgHeaders(orgId) {
30
+ if (typeof orgId !== "string" || !ORG_ID_SHAPE.test(orgId)) {
31
+ throw new Error(`orgId must be a plausible org id, got ${JSON.stringify(orgId)}`);
32
+ }
33
+ return { [ORG_HEADER]: orgId };
34
+ }
35
+ function segment(value, name) {
36
+ if (typeof value !== "string" || value === "") {
37
+ throw new Error(`${name} must be a non-empty string`);
38
+ }
39
+ return encodeURIComponent(value);
40
+ }
41
+ function hasStringId(row) {
42
+ return typeof row === "object" && row !== null && typeof row.id === "string" && row.id !== "";
43
+ }
44
+ function rows(data, what) {
45
+ if (!Array.isArray(data) || !data.every(hasStringId)) {
46
+ throw new Error(`team: malformed ${what} response`);
47
+ }
48
+ return data;
49
+ }
50
+ function createTeamClient({ fetch }) {
51
+ if (typeof fetch !== "function") {
52
+ throw new Error("createTeamClient: `fetch` must be a function");
53
+ }
54
+ const send = (orgId, method, path, body) => {
55
+ const headers = orgHeaders(orgId);
56
+ const init = { method, headers };
57
+ if (body !== void 0) {
58
+ headers["Content-Type"] = "application/json";
59
+ init.body = JSON.stringify(body);
60
+ }
61
+ return fetch(path, init);
62
+ };
63
+ return {
64
+ listMembers: (orgId) => send(orgId, "GET", "/api/org/members").then((d) => rows(d, "members")),
65
+ updateMemberRole: (orgId, memberId, role) => {
66
+ segment(role, "role");
67
+ return send(orgId, "PUT", `/api/org/members/${segment(memberId, "memberId")}/role`, {
68
+ role
69
+ }).then(() => void 0);
70
+ },
71
+ removeMember: (orgId, memberId) => send(orgId, "DELETE", `/api/org/members/${segment(memberId, "memberId")}`).then(
72
+ () => void 0
73
+ ),
74
+ inviteMember: (orgId, email, role) => {
75
+ if (typeof email !== "string" || email.trim() === "") {
76
+ throw new Error("email must be a non-empty string");
77
+ }
78
+ segment(role, "role");
79
+ return send(orgId, "POST", "/api/org/invite", { email: email.trim(), role }).then(
80
+ (d) => d
81
+ );
82
+ },
83
+ listInvitations: (orgId) => send(orgId, "GET", "/api/org/invitations").then((d) => rows(d, "invitations")),
84
+ revokeInvitation: (orgId, invitationId) => send(orgId, "DELETE", `/api/org/invitations/${segment(invitationId, "invitationId")}`).then(
85
+ () => void 0
86
+ ),
87
+ resendInvitation: (orgId, invitationId) => send(
88
+ orgId,
89
+ "POST",
90
+ `/api/org/invitations/${segment(invitationId, "invitationId")}/resend`
91
+ ).then(() => void 0)
92
+ };
93
+ }
94
+ // Annotate the CommonJS export names for ESM import in node:
95
+ 0 && (module.exports = {
96
+ ORG_HEADER,
97
+ createTeamClient
98
+ });
@@ -0,0 +1,72 @@
1
+ // src/client.ts
2
+ var ORG_HEADER = "X-Reeve-Org-Id";
3
+ var ORG_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
4
+ function orgHeaders(orgId) {
5
+ if (typeof orgId !== "string" || !ORG_ID_SHAPE.test(orgId)) {
6
+ throw new Error(`orgId must be a plausible org id, got ${JSON.stringify(orgId)}`);
7
+ }
8
+ return { [ORG_HEADER]: orgId };
9
+ }
10
+ function segment(value, name) {
11
+ if (typeof value !== "string" || value === "") {
12
+ throw new Error(`${name} must be a non-empty string`);
13
+ }
14
+ return encodeURIComponent(value);
15
+ }
16
+ function hasStringId(row) {
17
+ return typeof row === "object" && row !== null && typeof row.id === "string" && row.id !== "";
18
+ }
19
+ function rows(data, what) {
20
+ if (!Array.isArray(data) || !data.every(hasStringId)) {
21
+ throw new Error(`team: malformed ${what} response`);
22
+ }
23
+ return data;
24
+ }
25
+ function createTeamClient({ fetch }) {
26
+ if (typeof fetch !== "function") {
27
+ throw new Error("createTeamClient: `fetch` must be a function");
28
+ }
29
+ const send = (orgId, method, path, body) => {
30
+ const headers = orgHeaders(orgId);
31
+ const init = { method, headers };
32
+ if (body !== void 0) {
33
+ headers["Content-Type"] = "application/json";
34
+ init.body = JSON.stringify(body);
35
+ }
36
+ return fetch(path, init);
37
+ };
38
+ return {
39
+ listMembers: (orgId) => send(orgId, "GET", "/api/org/members").then((d) => rows(d, "members")),
40
+ updateMemberRole: (orgId, memberId, role) => {
41
+ segment(role, "role");
42
+ return send(orgId, "PUT", `/api/org/members/${segment(memberId, "memberId")}/role`, {
43
+ role
44
+ }).then(() => void 0);
45
+ },
46
+ removeMember: (orgId, memberId) => send(orgId, "DELETE", `/api/org/members/${segment(memberId, "memberId")}`).then(
47
+ () => void 0
48
+ ),
49
+ inviteMember: (orgId, email, role) => {
50
+ if (typeof email !== "string" || email.trim() === "") {
51
+ throw new Error("email must be a non-empty string");
52
+ }
53
+ segment(role, "role");
54
+ return send(orgId, "POST", "/api/org/invite", { email: email.trim(), role }).then(
55
+ (d) => d
56
+ );
57
+ },
58
+ listInvitations: (orgId) => send(orgId, "GET", "/api/org/invitations").then((d) => rows(d, "invitations")),
59
+ revokeInvitation: (orgId, invitationId) => send(orgId, "DELETE", `/api/org/invitations/${segment(invitationId, "invitationId")}`).then(
60
+ () => void 0
61
+ ),
62
+ resendInvitation: (orgId, invitationId) => send(
63
+ orgId,
64
+ "POST",
65
+ `/api/org/invitations/${segment(invitationId, "invitationId")}/resend`
66
+ ).then(() => void 0)
67
+ };
68
+ }
69
+ export {
70
+ ORG_HEADER,
71
+ createTeamClient
72
+ };
@@ -0,0 +1,100 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Roles the org-members router knows. `billing_manager` is a legacy alias
6
+ * the backend still accepts; no surface offers it from the invite dialog.
7
+ * Rows may still carry a role this union doesn't name, so row fields are
8
+ * typed `string` and the panel renders unknown roles verbatim.
9
+ */
10
+ type TeamRole = "owner" | "admin" | "billing_manager" | "member" | "viewer";
11
+ /** A role the invite dialog and the role picker can hand out (never `owner`). */
12
+ type AssignableRole = Exclude<TeamRole, "owner" | "billing_manager">;
13
+ /** One live member of the org (`OrganizationMember` row). `GET /api/org/members`. */
14
+ interface TeamMember {
15
+ /** OrganizationMember.id — the id every member route takes. */
16
+ id: string;
17
+ user_id: string;
18
+ email: string | null;
19
+ role: string;
20
+ is_default: boolean;
21
+ /** ISO-8601. */
22
+ created_at: string;
23
+ }
24
+ /** One invitation record. `GET /api/org/invitations`. */
25
+ interface Invitation {
26
+ id: string;
27
+ email: string;
28
+ role: string;
29
+ invited_by_email: string | null;
30
+ /** pending | accepted | expired | revoked */
31
+ status: string;
32
+ /** ISO-8601. */
33
+ created_at: string;
34
+ /** ISO-8601. */
35
+ expires_at: string;
36
+ accepted_at: string | null;
37
+ }
38
+ /** `POST /api/org/invite`. */
39
+ interface InviteMemberResponse {
40
+ message: string;
41
+ email: string;
42
+ role: string;
43
+ invitation_id?: string;
44
+ }
45
+ /**
46
+ * The data-access boundary the panel is built against. Every method takes
47
+ * the org it acts on: there is no ambient "current org" anywhere in this
48
+ * package, so a request can never be issued for an org other than the one
49
+ * the caller named (DEV-8592). `createTeamClient` is the reference
50
+ * implementation; a host may supply its own.
51
+ */
52
+ interface TeamClient {
53
+ listMembers(orgId: string): Promise<TeamMember[]>;
54
+ updateMemberRole(orgId: string, memberId: string, role: string): Promise<void>;
55
+ removeMember(orgId: string, memberId: string): Promise<void>;
56
+ inviteMember(orgId: string, email: string, role: string): Promise<InviteMemberResponse>;
57
+ listInvitations(orgId: string): Promise<Invitation[]>;
58
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
59
+ resendInvitation(orgId: string, invitationId: string): Promise<void>;
60
+ }
61
+
62
+ type TeamNotice = {
63
+ kind: "success" | "error";
64
+ message: string;
65
+ };
66
+ interface TeamMembersPanelProps {
67
+ /**
68
+ * Data access. `createTeamClient({ fetch })` from `@meetreeve/module-team/client`.
69
+ * Pass a stable instance (module scope or `useMemo`): a new client refetches.
70
+ */
71
+ client: TeamClient;
72
+ /**
73
+ * The org whose team this panel shows. Required: every request is issued
74
+ * for this org, and changing it resets the panel (DEV-8592).
75
+ */
76
+ orgId: string;
77
+ /**
78
+ * Roles the invite dialog AND the per-member role picker offer, in order.
79
+ * Default admin/member/viewer. The operator cockpit (DEV-2439) passes
80
+ * `["admin", "member"]`.
81
+ */
82
+ inviteRoles?: AssignableRole[];
83
+ /** The signed-in user's email: marks their row "you". */
84
+ currentUserEmail?: string | null;
85
+ /**
86
+ * Host-owned per-member controls (Studio's Budget / Capabilities dialogs).
87
+ * Rendered at the end of every member row, owners included; the host
88
+ * decides per member what to show.
89
+ */
90
+ renderMemberActions?: (member: TeamMember) => ReactNode;
91
+ /** Called with every success/error notice the panel shows inline. */
92
+ onNotice?: (notice: TeamNotice) => void;
93
+ }
94
+ /**
95
+ * Keyed on `orgId`: an org change remounts the body, which is what resets
96
+ * every piece of per-org state at once — no reset effect to forget.
97
+ */
98
+ declare function TeamMembersPanel(props: TeamMembersPanelProps): react.JSX.Element;
99
+
100
+ export { type AssignableRole, type Invitation, type InviteMemberResponse, type TeamClient, type TeamMember, TeamMembersPanel, type TeamMembersPanelProps, type TeamNotice, type TeamRole };
@@ -0,0 +1,100 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Roles the org-members router knows. `billing_manager` is a legacy alias
6
+ * the backend still accepts; no surface offers it from the invite dialog.
7
+ * Rows may still carry a role this union doesn't name, so row fields are
8
+ * typed `string` and the panel renders unknown roles verbatim.
9
+ */
10
+ type TeamRole = "owner" | "admin" | "billing_manager" | "member" | "viewer";
11
+ /** A role the invite dialog and the role picker can hand out (never `owner`). */
12
+ type AssignableRole = Exclude<TeamRole, "owner" | "billing_manager">;
13
+ /** One live member of the org (`OrganizationMember` row). `GET /api/org/members`. */
14
+ interface TeamMember {
15
+ /** OrganizationMember.id — the id every member route takes. */
16
+ id: string;
17
+ user_id: string;
18
+ email: string | null;
19
+ role: string;
20
+ is_default: boolean;
21
+ /** ISO-8601. */
22
+ created_at: string;
23
+ }
24
+ /** One invitation record. `GET /api/org/invitations`. */
25
+ interface Invitation {
26
+ id: string;
27
+ email: string;
28
+ role: string;
29
+ invited_by_email: string | null;
30
+ /** pending | accepted | expired | revoked */
31
+ status: string;
32
+ /** ISO-8601. */
33
+ created_at: string;
34
+ /** ISO-8601. */
35
+ expires_at: string;
36
+ accepted_at: string | null;
37
+ }
38
+ /** `POST /api/org/invite`. */
39
+ interface InviteMemberResponse {
40
+ message: string;
41
+ email: string;
42
+ role: string;
43
+ invitation_id?: string;
44
+ }
45
+ /**
46
+ * The data-access boundary the panel is built against. Every method takes
47
+ * the org it acts on: there is no ambient "current org" anywhere in this
48
+ * package, so a request can never be issued for an org other than the one
49
+ * the caller named (DEV-8592). `createTeamClient` is the reference
50
+ * implementation; a host may supply its own.
51
+ */
52
+ interface TeamClient {
53
+ listMembers(orgId: string): Promise<TeamMember[]>;
54
+ updateMemberRole(orgId: string, memberId: string, role: string): Promise<void>;
55
+ removeMember(orgId: string, memberId: string): Promise<void>;
56
+ inviteMember(orgId: string, email: string, role: string): Promise<InviteMemberResponse>;
57
+ listInvitations(orgId: string): Promise<Invitation[]>;
58
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
59
+ resendInvitation(orgId: string, invitationId: string): Promise<void>;
60
+ }
61
+
62
+ type TeamNotice = {
63
+ kind: "success" | "error";
64
+ message: string;
65
+ };
66
+ interface TeamMembersPanelProps {
67
+ /**
68
+ * Data access. `createTeamClient({ fetch })` from `@meetreeve/module-team/client`.
69
+ * Pass a stable instance (module scope or `useMemo`): a new client refetches.
70
+ */
71
+ client: TeamClient;
72
+ /**
73
+ * The org whose team this panel shows. Required: every request is issued
74
+ * for this org, and changing it resets the panel (DEV-8592).
75
+ */
76
+ orgId: string;
77
+ /**
78
+ * Roles the invite dialog AND the per-member role picker offer, in order.
79
+ * Default admin/member/viewer. The operator cockpit (DEV-2439) passes
80
+ * `["admin", "member"]`.
81
+ */
82
+ inviteRoles?: AssignableRole[];
83
+ /** The signed-in user's email: marks their row "you". */
84
+ currentUserEmail?: string | null;
85
+ /**
86
+ * Host-owned per-member controls (Studio's Budget / Capabilities dialogs).
87
+ * Rendered at the end of every member row, owners included; the host
88
+ * decides per member what to show.
89
+ */
90
+ renderMemberActions?: (member: TeamMember) => ReactNode;
91
+ /** Called with every success/error notice the panel shows inline. */
92
+ onNotice?: (notice: TeamNotice) => void;
93
+ }
94
+ /**
95
+ * Keyed on `orgId`: an org change remounts the body, which is what resets
96
+ * every piece of per-org state at once — no reset effect to forget.
97
+ */
98
+ declare function TeamMembersPanel(props: TeamMembersPanelProps): react.JSX.Element;
99
+
100
+ export { type AssignableRole, type Invitation, type InviteMemberResponse, type TeamClient, type TeamMember, TeamMembersPanel, type TeamMembersPanelProps, type TeamNotice, type TeamRole };