@tangle-network/agent-app 0.28.0 → 0.29.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.
Files changed (39) hide show
  1. package/dist/{DesignCanvasEditor-TN7CWUJW.js → DesignCanvasEditor-IB2FMBBD.js} +3 -3
  2. package/dist/InviteAcceptPage-EUOGZO2X.js +7 -0
  3. package/dist/InviteAcceptPage-EUOGZO2X.js.map +1 -0
  4. package/dist/MembersPanel-4CGUQRPF.js +8 -0
  5. package/dist/MembersPanel-4CGUQRPF.js.map +1 -0
  6. package/dist/access-ChAHG4Tp.d.ts +539 -0
  7. package/dist/chunk-3G334FVA.js +162 -0
  8. package/dist/chunk-3G334FVA.js.map +1 -0
  9. package/dist/chunk-3SVAA3MA.js +41 -0
  10. package/dist/chunk-3SVAA3MA.js.map +1 -0
  11. package/dist/chunk-535V6BH6.js +128 -0
  12. package/dist/chunk-535V6BH6.js.map +1 -0
  13. package/dist/chunk-63CE7FEZ.js +63 -0
  14. package/dist/chunk-63CE7FEZ.js.map +1 -0
  15. package/dist/{chunk-7I37CO5D.js → chunk-N4ZFKQ5C.js} +4 -4
  16. package/dist/{chunk-J7JNV5BH.js → chunk-S5CAJX6O.js} +2 -2
  17. package/dist/design-canvas-react/index.js +3 -3
  18. package/dist/design-canvas-react/lazy.js +1 -1
  19. package/dist/index.js +7 -7
  20. package/dist/roles-CLtYKHHl.d.ts +49 -0
  21. package/dist/teams/drizzle.d.ts +42 -0
  22. package/dist/teams/drizzle.js +194 -0
  23. package/dist/teams/drizzle.js.map +1 -0
  24. package/dist/teams/index.d.ts +44 -0
  25. package/dist/teams/index.js +39 -0
  26. package/dist/teams/index.js.map +1 -0
  27. package/dist/teams/members-api.d.ts +152 -0
  28. package/dist/teams/members-api.js +261 -0
  29. package/dist/teams/members-api.js.map +1 -0
  30. package/dist/teams-react/index.d.ts +81 -0
  31. package/dist/teams-react/index.js +12 -0
  32. package/dist/teams-react/index.js.map +1 -0
  33. package/dist/teams-react/lazy.d.ts +9 -0
  34. package/dist/teams-react/lazy.js +13 -0
  35. package/dist/teams-react/lazy.js.map +1 -0
  36. package/package.json +26 -1
  37. /package/dist/{DesignCanvasEditor-TN7CWUJW.js.map → DesignCanvasEditor-IB2FMBBD.js.map} +0 -0
  38. /package/dist/{chunk-7I37CO5D.js.map → chunk-N4ZFKQ5C.js.map} +0 -0
  39. /package/dist/{chunk-J7JNV5BH.js.map → chunk-S5CAJX6O.js.map} +0 -0
@@ -0,0 +1,261 @@
1
+ import {
2
+ generateInviteToken
3
+ } from "../chunk-3SVAA3MA.js";
4
+ import {
5
+ canManageWorkspaceMemberRole,
6
+ hasWorkspaceRole,
7
+ isAssignableWorkspaceRole,
8
+ organizationRoleGrantsWorkspaceOwner
9
+ } from "../chunk-63CE7FEZ.js";
10
+
11
+ // src/teams/members-api.ts
12
+ import { and, eq, isNull } from "drizzle-orm";
13
+ var SeatLimitError = class extends Error {
14
+ status;
15
+ capability;
16
+ requiredPlan;
17
+ constructor(message, opts = {}) {
18
+ super(message);
19
+ this.name = "SeatLimitError";
20
+ this.status = opts.status ?? 402;
21
+ this.capability = opts.capability;
22
+ this.requiredPlan = opts.requiredPlan;
23
+ }
24
+ };
25
+ function createMembersApi(opts) {
26
+ const { db, tables, userTable, workspaceTable, access, enforceSeat, memberSyncSeam } = opts;
27
+ const { organizations, organizationMembers, workspaceMembers } = tables;
28
+ const users = userTable;
29
+ const workspaces = workspaceTable;
30
+ function fireSync(op) {
31
+ try {
32
+ Promise.resolve(op()).catch(() => {
33
+ });
34
+ } catch {
35
+ }
36
+ }
37
+ async function listMembers(input) {
38
+ const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id);
39
+ if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
40
+ const organizationId = accessRow.workspace.organizationId;
41
+ const [projectMembers, orgAdmins] = await Promise.all([
42
+ db.select({
43
+ id: workspaceMembers.id,
44
+ organizationMemberId: workspaceMembers.organizationMemberId,
45
+ userId: workspaceMembers.userId,
46
+ role: workspaceMembers.role,
47
+ invitedAt: workspaceMembers.invitedAt,
48
+ acceptedAt: workspaceMembers.acceptedAt,
49
+ inviteEmail: workspaceMembers.inviteEmail,
50
+ userName: users.name,
51
+ userEmail: users.email
52
+ }).from(workspaceMembers).leftJoin(users, eq(users.id, workspaceMembers.userId)).where(eq(workspaceMembers.workspaceId, input.workspaceId)),
53
+ db.select({
54
+ id: organizationMembers.id,
55
+ userId: organizationMembers.userId,
56
+ orgRole: organizationMembers.role,
57
+ userName: users.name,
58
+ userEmail: users.email,
59
+ createdAt: organizationMembers.createdAt
60
+ }).from(organizationMembers).innerJoin(users, eq(users.id, organizationMembers.userId)).where(eq(organizationMembers.organizationId, organizationId))
61
+ ]);
62
+ const explicitOrgMemberIds = new Set(
63
+ projectMembers.map((m) => m.organizationMemberId).filter(Boolean)
64
+ );
65
+ const members = [
66
+ ...orgAdmins.filter((m) => organizationRoleGrantsWorkspaceOwner(m.orgRole) && !explicitOrgMemberIds.has(m.id)).map((m) => ({
67
+ id: `org:${m.id}`,
68
+ userId: m.userId,
69
+ organizationMemberId: m.id,
70
+ role: "owner",
71
+ name: m.userName,
72
+ email: m.userEmail,
73
+ invitedAt: m.createdAt,
74
+ acceptedAt: m.createdAt,
75
+ inherited: true
76
+ })),
77
+ ...projectMembers.map((m) => ({
78
+ id: m.id,
79
+ userId: m.userId,
80
+ organizationMemberId: m.organizationMemberId,
81
+ role: m.role,
82
+ name: m.userName,
83
+ email: m.userEmail ?? m.inviteEmail,
84
+ invitedAt: m.invitedAt,
85
+ acceptedAt: m.acceptedAt,
86
+ inherited: false
87
+ }))
88
+ ];
89
+ return Response.json({ members, currentRole: accessRow.role, organizationId });
90
+ }
91
+ async function inviteMember(input) {
92
+ if (!input.email) return Response.json({ error: "Missing email" }, { status: 400 });
93
+ const requestedRole = input.role ?? "editor";
94
+ if (!isAssignableWorkspaceRole(requestedRole)) {
95
+ return Response.json({ error: "Invalid role. Must be viewer, editor, or admin." }, { status: 400 });
96
+ }
97
+ const assignRole = requestedRole;
98
+ const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, "admin");
99
+ if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
100
+ if (!hasWorkspaceRole(accessRow.role, assignRole)) {
101
+ return Response.json({ error: "Cannot assign a role higher than your own" }, { status: 403 });
102
+ }
103
+ const organizationId = accessRow.workspace.organizationId;
104
+ const normalizedEmail = input.email.toLowerCase().trim();
105
+ const invitee = await getUserByEmail(normalizedEmail);
106
+ const existingOrgMember = invitee ? await getOrganizationMember(organizationId, invitee.id) : null;
107
+ if (organizationRoleGrantsWorkspaceOwner(existingOrgMember?.role)) {
108
+ return Response.json({ error: "Organization admins already have project access" }, { status: 409 });
109
+ }
110
+ const existingInvite = await findExistingProjectInvite(input.workspaceId, normalizedEmail, invitee?.id);
111
+ if (existingInvite) {
112
+ return Response.json({ error: "This collaborator is already invited to this project" }, { status: 409 });
113
+ }
114
+ if (enforceSeat && !existingOrgMember && !await hasPendingOrgInvite(organizationId, normalizedEmail)) {
115
+ try {
116
+ await enforceSeat({ actorId: input.actor.id, organizationId });
117
+ } catch (err) {
118
+ if (err instanceof SeatLimitError) {
119
+ return Response.json(
120
+ { error: err.message, capability: err.capability, requiredPlan: err.requiredPlan },
121
+ { status: err.status }
122
+ );
123
+ }
124
+ throw err;
125
+ }
126
+ }
127
+ const token = generateInviteToken();
128
+ const [member] = await db.insert(workspaceMembers).values({
129
+ workspaceId: input.workspaceId,
130
+ organizationMemberId: existingOrgMember?.id ?? null,
131
+ userId: invitee?.id ?? null,
132
+ role: assignRole,
133
+ invitedBy: input.actor.id,
134
+ inviteEmail: normalizedEmail,
135
+ inviteToken: token
136
+ }).returning();
137
+ return Response.json({ member, inviteToken: token });
138
+ }
139
+ async function updateMemberRole(input) {
140
+ if (!input.memberId || !input.role) {
141
+ return Response.json({ error: "Missing memberId or role" }, { status: 400 });
142
+ }
143
+ if (input.memberId.startsWith("org:")) {
144
+ return Response.json({ error: "Organization owner/admin project access is managed at the organization level" }, { status: 403 });
145
+ }
146
+ if (!isAssignableWorkspaceRole(input.role)) {
147
+ return Response.json({ error: "Invalid role" }, { status: 400 });
148
+ }
149
+ const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, "admin");
150
+ if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
151
+ if (!hasWorkspaceRole(accessRow.role, input.role)) {
152
+ return Response.json({ error: "Cannot assign a role higher than your own" }, { status: 403 });
153
+ }
154
+ const [target] = await db.select({ id: workspaceMembers.id, role: workspaceMembers.role, userId: workspaceMembers.userId }).from(workspaceMembers).where(and(eq(workspaceMembers.id, input.memberId), eq(workspaceMembers.workspaceId, input.workspaceId))).limit(1);
155
+ if (!target) return Response.json({ error: "Member not found" }, { status: 404 });
156
+ if (target.userId === input.actor.id) return Response.json({ error: "Cannot change your own role" }, { status: 403 });
157
+ if (!canManageWorkspaceMemberRole(accessRow.role, target.role)) {
158
+ return Response.json({ error: "Cannot modify a member with equal or higher role" }, { status: 403 });
159
+ }
160
+ await db.update(workspaceMembers).set({ role: input.role }).where(eq(workspaceMembers.id, input.memberId));
161
+ if (target.userId && memberSyncSeam?.role) {
162
+ const userId = target.userId;
163
+ const nextRole = input.role;
164
+ fireSync(() => memberSyncSeam.role({ workspaceId: input.workspaceId, userId, role: nextRole }));
165
+ }
166
+ return Response.json({ success: true });
167
+ }
168
+ async function removeMember(input) {
169
+ if (!input.memberId) return Response.json({ error: "Missing memberId" }, { status: 400 });
170
+ if (input.memberId.startsWith("org:")) {
171
+ return Response.json({ error: "Organization owner/admin project access is managed at the organization level" }, { status: 403 });
172
+ }
173
+ const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, "admin");
174
+ if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
175
+ const [target] = await db.select({ id: workspaceMembers.id, role: workspaceMembers.role, userId: workspaceMembers.userId }).from(workspaceMembers).where(and(eq(workspaceMembers.id, input.memberId), eq(workspaceMembers.workspaceId, input.workspaceId))).limit(1);
176
+ if (!target) return Response.json({ error: "Member not found" }, { status: 404 });
177
+ if (target.userId === input.actor.id) return Response.json({ error: "Cannot remove yourself from this project" }, { status: 403 });
178
+ if (!canManageWorkspaceMemberRole(accessRow.role, target.role)) {
179
+ return Response.json({ error: "Cannot remove a member with equal or higher role" }, { status: 403 });
180
+ }
181
+ await db.delete(workspaceMembers).where(eq(workspaceMembers.id, input.memberId));
182
+ if (target.userId && memberSyncSeam?.remove) {
183
+ const userId = target.userId;
184
+ fireSync(() => memberSyncSeam.remove({ workspaceId: input.workspaceId, userId }));
185
+ }
186
+ return Response.json({ success: true });
187
+ }
188
+ async function acceptInvite(input) {
189
+ if (!input.token || typeof input.token !== "string") {
190
+ return Response.json({ error: "Missing invite token" }, { status: 400 });
191
+ }
192
+ const [invite] = await db.select().from(workspaceMembers).where(eq(workspaceMembers.inviteToken, input.token)).limit(1);
193
+ if (!invite) return Response.json({ error: "Invalid or expired invite" }, { status: 404 });
194
+ if (invite.acceptedAt) return Response.json({ error: "Invite already accepted" }, { status: 409 });
195
+ if (invite.inviteEmail && invite.inviteEmail.toLowerCase() !== input.actor.email?.toLowerCase()) {
196
+ return Response.json(
197
+ { error: `This invite was sent to ${invite.inviteEmail}. Please sign in with that email.` },
198
+ { status: 403 }
199
+ );
200
+ }
201
+ const [workspace] = await db.select({ id: workspaces.id, organizationId: workspaces.organizationId }).from(workspaces).where(eq(workspaces.id, invite.workspaceId)).limit(1);
202
+ if (!workspace) return Response.json({ error: "Project not found" }, { status: 404 });
203
+ const organizationId = workspace.organizationId;
204
+ const updated = await db.update(workspaceMembers).set({
205
+ userId: input.actor.id,
206
+ acceptedAt: /* @__PURE__ */ new Date(),
207
+ inviteToken: null
208
+ }).where(and(
209
+ eq(workspaceMembers.inviteToken, input.token),
210
+ isNull(workspaceMembers.acceptedAt)
211
+ )).returning({ id: workspaceMembers.id });
212
+ if (updated.length === 0) return Response.json({ error: "Invite already accepted" }, { status: 409 });
213
+ const [orgMember] = await db.insert(organizationMembers).values({
214
+ organizationId,
215
+ userId: input.actor.id,
216
+ role: "member"
217
+ }).onConflictDoUpdate({
218
+ target: [organizationMembers.organizationId, organizationMembers.userId],
219
+ set: { updatedAt: /* @__PURE__ */ new Date() }
220
+ }).returning();
221
+ await db.update(workspaceMembers).set({ organizationMemberId: invite.organizationMemberId ?? orgMember.id }).where(eq(workspaceMembers.id, updated[0].id));
222
+ if (memberSyncSeam?.add) {
223
+ const role = invite.role;
224
+ fireSync(() => memberSyncSeam.add({ workspaceId: invite.workspaceId, userId: input.actor.id, role }));
225
+ }
226
+ return Response.json({
227
+ success: true,
228
+ workspaceId: invite.workspaceId,
229
+ role: invite.role
230
+ });
231
+ }
232
+ async function getUserByEmail(email) {
233
+ const [user] = await db.select({ id: users.id, email: users.email }).from(users).where(eq(users.email, email)).limit(1);
234
+ return user ?? null;
235
+ }
236
+ async function getOrganizationMember(organizationId, userId) {
237
+ const [member] = await db.select().from(organizationMembers).where(and(eq(organizationMembers.organizationId, organizationId), eq(organizationMembers.userId, userId))).limit(1);
238
+ return member ?? null;
239
+ }
240
+ async function findExistingProjectInvite(workspaceId, email, userId) {
241
+ const byEmail = await db.select({ id: workspaceMembers.id }).from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.inviteEmail, email))).limit(1);
242
+ if (byEmail[0]) return byEmail[0];
243
+ if (!userId) return null;
244
+ const byUser = await db.select({ id: workspaceMembers.id }).from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, userId))).limit(1);
245
+ return byUser[0] ?? null;
246
+ }
247
+ async function hasPendingOrgInvite(organizationId, email) {
248
+ const [row] = await db.select({ id: workspaceMembers.id }).from(workspaceMembers).innerJoin(workspaces, eq(workspaces.id, workspaceMembers.workspaceId)).where(and(
249
+ eq(workspaces.organizationId, organizationId),
250
+ eq(workspaceMembers.inviteEmail, email),
251
+ isNull(workspaceMembers.acceptedAt)
252
+ )).limit(1);
253
+ return Boolean(row);
254
+ }
255
+ return { listMembers, inviteMember, updateMemberRole, removeMember, acceptInvite };
256
+ }
257
+ export {
258
+ SeatLimitError,
259
+ createMembersApi
260
+ };
261
+ //# sourceMappingURL=members-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/teams/members-api.ts"],"sourcesContent":["/**\n * Framework-neutral members API for the teams module: the invite / list /\n * update-role / remove / accept-invite logic, lifted out of any one app's route\n * file. Each app mounts these in its own route with its own auth — the handlers\n * take an already-authenticated `actor`, the parsed inputs, the `db` + `tables`\n * from `createTeamTables`, the product's user + workspace tables, and the\n * workspace-access API from `createWorkspaceAccess`. They return web-standard\n * `Response`s (available in Workers, Node 18+, Deno, browsers), so\n * \"framework-neutral\" is literal: no Remix/React-Router/Express import anywhere.\n *\n * Two OPTIONAL seams an app wires only if it needs them:\n * - `enforceSeat` — billing/seat-limit gate, called at invite time only when\n * the invite would consume a NEW billable seat. An app without seat billing\n * passes nothing and seats are never checked.\n * - `memberSyncSeam` — fire-and-forget propagation of membership changes to a\n * sandbox/external system (add on accept, role on update, remove on\n * delete). An app without sandbox sync passes nothing. Fail-soft by\n * contract: a thrown sync is caught and never blocks the DB mutation.\n *\n * Imports `drizzle-orm`, so this is a subpath, never re-exported from root.\n */\n\nimport { and, eq, isNull } from 'drizzle-orm'\nimport {\n type AssignableWorkspaceRole,\n type WorkspaceRole,\n canManageWorkspaceMemberRole,\n hasWorkspaceRole,\n isAssignableWorkspaceRole,\n organizationRoleGrantsWorkspaceOwner,\n} from './roles'\nimport { generateInviteToken } from './invite'\nimport type { TeamDatabase, WorkspaceAccessApi } from './drizzle/access'\nimport type { TeamParentTable, TeamTables } from './drizzle/schema'\n\n/** The authenticated caller — apps resolve this from their own session layer. */\nexport interface MembersApiActor {\n id: string\n email?: string | null\n}\n\n/**\n * The product's user table, narrowed to the columns the member queries read.\n * Adopters pass their real drizzle user table.\n */\nexport interface UserLookupTable {\n id: any\n name: any\n email: any\n}\n\n/**\n * The product's workspace table, narrowed to the columns the member queries\n * read. The handlers join it to resolve a workspace's organization (the same\n * table passed to createTeamTables as workspaceTable).\n */\nexport interface WorkspaceLookupTable {\n id: any\n organizationId: any\n}\n\n/**\n * Optional billing seat gate. The seam resolves the count input itself (it\n * owns the plan/seat model) and throws when over the limit; the handler turns a\n * thrown `SeatLimitError` into a 402. Called only when an invite would consume\n * a NEW seat (invitee is not already an org member and has no pending org\n * invite) — the same guard gtm uses, so the seam never fires on a no-op invite.\n */\nexport interface EnforceSeatSeam {\n (input: { actorId: string; organizationId: string }): Promise<void> | void\n}\n\n/** Thrown by an `enforceSeat` seam to deny an invite; serialized to a 402. */\nexport class SeatLimitError extends Error {\n readonly status: number\n readonly capability?: string\n readonly requiredPlan?: string\n constructor(message: string, opts: { status?: number; capability?: string; requiredPlan?: string } = {}) {\n super(message)\n this.name = 'SeatLimitError'\n this.status = opts.status ?? 402\n this.capability = opts.capability\n this.requiredPlan = opts.requiredPlan\n }\n}\n\n/**\n * Optional membership-change propagation to an external system (e.g. sandbox).\n * Every method is fire-and-forget and fail-soft: the handler awaits nothing and\n * swallows rejections, so an unavailable downstream never blocks the mutation.\n * Each fires only for members with a real `userId` (never email-only invites).\n */\nexport interface MemberSyncSeam {\n add?(input: { workspaceId: string; userId: string; role: WorkspaceRole }): Promise<void> | void\n role?(input: { workspaceId: string; userId: string; role: WorkspaceRole }): Promise<void> | void\n remove?(input: { workspaceId: string; userId: string }): Promise<void> | void\n}\n\nexport interface MembersApiOptions {\n db: TeamDatabase\n tables: TeamTables\n /** The product's user table (FK target passed to createTeamTables). */\n userTable: TeamParentTable & UserLookupTable\n /** The product's workspace table (FK target passed to createTeamTables). */\n workspaceTable: TeamParentTable & WorkspaceLookupTable\n /** Workspace-access API from createWorkspaceAccess — RBAC stays one source. */\n access: Pick<WorkspaceAccessApi, 'getWorkspaceAccess'>\n enforceSeat?: EnforceSeatSeam\n memberSyncSeam?: MemberSyncSeam\n}\n\nexport interface MemberListEntry {\n id: string\n userId: string | null\n organizationMemberId: string | null\n role: WorkspaceRole\n name: string | null\n email: string | null\n invitedAt: Date | number | null\n acceptedAt: Date | number | null\n inherited: boolean\n}\n\n/**\n * Build the members API bound to one product's db/tables/access. Returns five\n * handlers; an app maps its route methods onto them (GET→list, POST→invite,\n * PATCH→updateRole, DELETE→remove; accept on its own route).\n */\nexport function createMembersApi(opts: MembersApiOptions) {\n const { db, tables, userTable, workspaceTable, access, enforceSeat, memberSyncSeam } = opts\n const { organizations, organizationMembers, workspaceMembers } = tables\n const users = userTable\n const workspaces = workspaceTable\n\n function fireSync(op: () => Promise<void> | void) {\n try {\n Promise.resolve(op()).catch(() => {})\n } catch {\n // seam threw synchronously — swallow; sync is best-effort by contract.\n }\n }\n\n async function listMembers(input: { workspaceId: string; actor: MembersApiActor }): Promise<Response> {\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id)\n if (!accessRow) return Response.json({ error: 'Workspace not found' }, { status: 404 })\n\n const organizationId = (accessRow.workspace as { organizationId: string }).organizationId\n\n const [projectMembers, orgAdmins] = await Promise.all([\n db\n .select({\n id: workspaceMembers.id,\n organizationMemberId: workspaceMembers.organizationMemberId,\n userId: workspaceMembers.userId,\n role: workspaceMembers.role,\n invitedAt: workspaceMembers.invitedAt,\n acceptedAt: workspaceMembers.acceptedAt,\n inviteEmail: workspaceMembers.inviteEmail,\n userName: users.name,\n userEmail: users.email,\n })\n .from(workspaceMembers)\n .leftJoin(users, eq(users.id, workspaceMembers.userId))\n .where(eq(workspaceMembers.workspaceId, input.workspaceId)),\n db\n .select({\n id: organizationMembers.id,\n userId: organizationMembers.userId,\n orgRole: organizationMembers.role,\n userName: users.name,\n userEmail: users.email,\n createdAt: organizationMembers.createdAt,\n })\n .from(organizationMembers)\n .innerJoin(users, eq(users.id, organizationMembers.userId))\n .where(eq(organizationMembers.organizationId, organizationId)),\n ])\n\n const explicitOrgMemberIds = new Set(\n projectMembers.map((m: any) => m.organizationMemberId).filter(Boolean),\n )\n const members: MemberListEntry[] = [\n ...orgAdmins\n .filter((m: any) => organizationRoleGrantsWorkspaceOwner(m.orgRole) && !explicitOrgMemberIds.has(m.id))\n .map((m: any) => ({\n id: `org:${m.id}`,\n userId: m.userId,\n organizationMemberId: m.id,\n role: 'owner' as WorkspaceRole,\n name: m.userName,\n email: m.userEmail,\n invitedAt: m.createdAt,\n acceptedAt: m.createdAt,\n inherited: true,\n })),\n ...projectMembers.map((m: any) => ({\n id: m.id,\n userId: m.userId,\n organizationMemberId: m.organizationMemberId,\n role: m.role as WorkspaceRole,\n name: m.userName,\n email: m.userEmail ?? m.inviteEmail,\n invitedAt: m.invitedAt,\n acceptedAt: m.acceptedAt,\n inherited: false,\n })),\n ]\n\n return Response.json({ members, currentRole: accessRow.role, organizationId })\n }\n\n async function inviteMember(input: {\n workspaceId: string\n actor: MembersApiActor\n email?: string\n role?: string\n }): Promise<Response> {\n if (!input.email) return Response.json({ error: 'Missing email' }, { status: 400 })\n const requestedRole = input.role ?? 'editor'\n if (!isAssignableWorkspaceRole(requestedRole)) {\n return Response.json({ error: 'Invalid role. Must be viewer, editor, or admin.' }, { status: 400 })\n }\n const assignRole: AssignableWorkspaceRole = requestedRole\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, 'admin')\n if (!accessRow) return Response.json({ error: 'Workspace not found' }, { status: 404 })\n if (!hasWorkspaceRole(accessRow.role, assignRole)) {\n return Response.json({ error: 'Cannot assign a role higher than your own' }, { status: 403 })\n }\n\n const organizationId = (accessRow.workspace as { organizationId: string }).organizationId\n const normalizedEmail = input.email.toLowerCase().trim()\n const invitee = await getUserByEmail(normalizedEmail)\n const existingOrgMember = invitee ? await getOrganizationMember(organizationId, invitee.id) : null\n\n if (organizationRoleGrantsWorkspaceOwner(existingOrgMember?.role)) {\n return Response.json({ error: 'Organization admins already have project access' }, { status: 409 })\n }\n\n const existingInvite = await findExistingProjectInvite(input.workspaceId, normalizedEmail, invitee?.id)\n if (existingInvite) {\n return Response.json({ error: 'This collaborator is already invited to this project' }, { status: 409 })\n }\n\n if (enforceSeat && !existingOrgMember && !(await hasPendingOrgInvite(organizationId, normalizedEmail))) {\n try {\n await enforceSeat({ actorId: input.actor.id, organizationId })\n } catch (err) {\n if (err instanceof SeatLimitError) {\n return Response.json(\n { error: err.message, capability: err.capability, requiredPlan: err.requiredPlan },\n { status: err.status },\n )\n }\n throw err\n }\n }\n\n const token = generateInviteToken()\n const [member] = await db\n .insert(workspaceMembers)\n .values({\n workspaceId: input.workspaceId,\n organizationMemberId: existingOrgMember?.id ?? null,\n userId: invitee?.id ?? null,\n role: assignRole,\n invitedBy: input.actor.id,\n inviteEmail: normalizedEmail,\n inviteToken: token,\n })\n .returning()\n\n return Response.json({ member, inviteToken: token })\n }\n\n async function updateMemberRole(input: {\n workspaceId: string\n actor: MembersApiActor\n memberId?: string\n role?: string\n }): Promise<Response> {\n if (!input.memberId || !input.role) {\n return Response.json({ error: 'Missing memberId or role' }, { status: 400 })\n }\n if (input.memberId.startsWith('org:')) {\n return Response.json({ error: 'Organization owner/admin project access is managed at the organization level' }, { status: 403 })\n }\n if (!isAssignableWorkspaceRole(input.role)) {\n return Response.json({ error: 'Invalid role' }, { status: 400 })\n }\n\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, 'admin')\n if (!accessRow) return Response.json({ error: 'Workspace not found' }, { status: 404 })\n if (!hasWorkspaceRole(accessRow.role, input.role)) {\n return Response.json({ error: 'Cannot assign a role higher than your own' }, { status: 403 })\n }\n\n const [target] = await db\n .select({ id: workspaceMembers.id, role: workspaceMembers.role, userId: workspaceMembers.userId })\n .from(workspaceMembers)\n .where(and(eq(workspaceMembers.id, input.memberId), eq(workspaceMembers.workspaceId, input.workspaceId)))\n .limit(1)\n\n if (!target) return Response.json({ error: 'Member not found' }, { status: 404 })\n if (target.userId === input.actor.id) return Response.json({ error: 'Cannot change your own role' }, { status: 403 })\n if (!canManageWorkspaceMemberRole(accessRow.role, target.role as WorkspaceRole)) {\n return Response.json({ error: 'Cannot modify a member with equal or higher role' }, { status: 403 })\n }\n\n await db.update(workspaceMembers).set({ role: input.role }).where(eq(workspaceMembers.id, input.memberId))\n if (target.userId && memberSyncSeam?.role) {\n const userId = target.userId\n const nextRole = input.role as WorkspaceRole\n fireSync(() => memberSyncSeam.role!({ workspaceId: input.workspaceId, userId, role: nextRole }))\n }\n return Response.json({ success: true })\n }\n\n async function removeMember(input: {\n workspaceId: string\n actor: MembersApiActor\n memberId?: string\n }): Promise<Response> {\n if (!input.memberId) return Response.json({ error: 'Missing memberId' }, { status: 400 })\n if (input.memberId.startsWith('org:')) {\n return Response.json({ error: 'Organization owner/admin project access is managed at the organization level' }, { status: 403 })\n }\n\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, 'admin')\n if (!accessRow) return Response.json({ error: 'Workspace not found' }, { status: 404 })\n\n const [target] = await db\n .select({ id: workspaceMembers.id, role: workspaceMembers.role, userId: workspaceMembers.userId })\n .from(workspaceMembers)\n .where(and(eq(workspaceMembers.id, input.memberId), eq(workspaceMembers.workspaceId, input.workspaceId)))\n .limit(1)\n\n if (!target) return Response.json({ error: 'Member not found' }, { status: 404 })\n if (target.userId === input.actor.id) return Response.json({ error: 'Cannot remove yourself from this project' }, { status: 403 })\n if (!canManageWorkspaceMemberRole(accessRow.role, target.role as WorkspaceRole)) {\n return Response.json({ error: 'Cannot remove a member with equal or higher role' }, { status: 403 })\n }\n\n await db.delete(workspaceMembers).where(eq(workspaceMembers.id, input.memberId))\n if (target.userId && memberSyncSeam?.remove) {\n const userId = target.userId\n fireSync(() => memberSyncSeam.remove!({ workspaceId: input.workspaceId, userId }))\n }\n return Response.json({ success: true })\n }\n\n async function acceptInvite(input: { token?: string; actor: MembersApiActor }): Promise<Response> {\n if (!input.token || typeof input.token !== 'string') {\n return Response.json({ error: 'Missing invite token' }, { status: 400 })\n }\n\n const [invite] = await db\n .select()\n .from(workspaceMembers)\n .where(eq(workspaceMembers.inviteToken, input.token))\n .limit(1)\n\n if (!invite) return Response.json({ error: 'Invalid or expired invite' }, { status: 404 })\n if (invite.acceptedAt) return Response.json({ error: 'Invite already accepted' }, { status: 409 })\n\n if (invite.inviteEmail && invite.inviteEmail.toLowerCase() !== input.actor.email?.toLowerCase()) {\n return Response.json(\n { error: `This invite was sent to ${invite.inviteEmail}. Please sign in with that email.` },\n { status: 403 },\n )\n }\n\n const [workspace] = await db\n .select({ id: workspaces.id, organizationId: workspaces.organizationId })\n .from(workspaces)\n .where(eq(workspaces.id, invite.workspaceId))\n .limit(1)\n\n if (!workspace) return Response.json({ error: 'Project not found' }, { status: 404 })\n const organizationId = (workspace as { organizationId: string }).organizationId\n\n // Atomic accept: only succeeds if the token still exists and is unaccepted,\n // so two concurrent accepts can't both win.\n const updated = await db\n .update(workspaceMembers)\n .set({\n userId: input.actor.id,\n acceptedAt: new Date(),\n inviteToken: null,\n })\n .where(and(\n eq(workspaceMembers.inviteToken, input.token),\n isNull(workspaceMembers.acceptedAt),\n ))\n .returning({ id: workspaceMembers.id })\n\n if (updated.length === 0) return Response.json({ error: 'Invite already accepted' }, { status: 409 })\n\n const [orgMember] = await db\n .insert(organizationMembers)\n .values({\n organizationId,\n userId: input.actor.id,\n role: 'member',\n })\n .onConflictDoUpdate({\n target: [organizationMembers.organizationId, organizationMembers.userId],\n set: { updatedAt: new Date() },\n })\n .returning()\n\n await db\n .update(workspaceMembers)\n .set({ organizationMemberId: invite.organizationMemberId ?? orgMember!.id })\n .where(eq(workspaceMembers.id, updated[0]!.id))\n\n if (memberSyncSeam?.add) {\n const role = invite.role as WorkspaceRole\n fireSync(() => memberSyncSeam.add!({ workspaceId: invite.workspaceId, userId: input.actor.id, role }))\n }\n\n return Response.json({\n success: true,\n workspaceId: invite.workspaceId,\n role: invite.role,\n })\n }\n\n // ── internal query helpers ──\n\n async function getUserByEmail(email: string) {\n const [user] = await db\n .select({ id: users.id, email: users.email })\n .from(users)\n .where(eq(users.email, email))\n .limit(1)\n return user ?? null\n }\n\n async function getOrganizationMember(organizationId: string, userId: string) {\n const [member] = await db\n .select()\n .from(organizationMembers)\n .where(and(eq(organizationMembers.organizationId, organizationId), eq(organizationMembers.userId, userId)))\n .limit(1)\n return member ?? null\n }\n\n async function findExistingProjectInvite(workspaceId: string, email: string, userId?: string) {\n const byEmail = await db\n .select({ id: workspaceMembers.id })\n .from(workspaceMembers)\n .where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.inviteEmail, email)))\n .limit(1)\n if (byEmail[0]) return byEmail[0]\n if (!userId) return null\n const byUser = await db\n .select({ id: workspaceMembers.id })\n .from(workspaceMembers)\n .where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, userId)))\n .limit(1)\n return byUser[0] ?? null\n }\n\n async function hasPendingOrgInvite(organizationId: string, email: string): Promise<boolean> {\n const [row] = await db\n .select({ id: workspaceMembers.id })\n .from(workspaceMembers)\n .innerJoin(workspaces, eq(workspaces.id, workspaceMembers.workspaceId))\n .where(and(\n eq(workspaces.organizationId, organizationId),\n eq(workspaceMembers.inviteEmail, email),\n isNull(workspaceMembers.acceptedAt),\n ))\n .limit(1)\n return Boolean(row)\n }\n\n return { listMembers, inviteMember, updateMemberRole, removeMember, acceptInvite }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,81 @@
1
+ import { f as WorkspaceRole } from '../roles-CLtYKHHl.js';
2
+ import * as react from 'react';
3
+
4
+ /**
5
+ * Seams between the teams React surface and the host app. Everything here is
6
+ * interface-only and callback-driven: the components never import an app's
7
+ * router, fetch client, or toast system. The host passes data in and supplies
8
+ * the async callbacks (which it backs with `./teams/members-api` over fetch),
9
+ * so the same panel mounts in any app.
10
+ */
11
+
12
+ /** One member row as the panel renders it — the shape `members-api` returns. */
13
+ interface MemberView {
14
+ id: string;
15
+ userId: string | null;
16
+ role: WorkspaceRole;
17
+ name: string | null;
18
+ email: string | null;
19
+ /** null/undefined = pending invite (not yet accepted). */
20
+ acceptedAt?: Date | number | null;
21
+ /** true = inherited org owner/admin access (managed at the org level). */
22
+ inherited?: boolean;
23
+ }
24
+ interface MembersPanelProps {
25
+ members: MemberView[];
26
+ /** The viewer's effective role — gates which controls are interactive. */
27
+ currentRole: WorkspaceRole;
28
+ /** Invite a member; resolve with the shareable invite URL (or void). */
29
+ onInvite(input: {
30
+ email: string;
31
+ role: WorkspaceRole;
32
+ }): Promise<{
33
+ inviteUrl?: string;
34
+ } | void>;
35
+ /** Change a member's role. */
36
+ onChangeRole(input: {
37
+ memberId: string;
38
+ role: WorkspaceRole;
39
+ }): Promise<void>;
40
+ /** Remove a member. */
41
+ onRemove(input: {
42
+ memberId: string;
43
+ }): Promise<void>;
44
+ /** Optional toast/notice hook; defaults to a no-op (host owns its UX). */
45
+ onNotice?(notice: {
46
+ kind: 'success' | 'error';
47
+ message: string;
48
+ }): void;
49
+ }
50
+ type InviteAcceptStatus = 'pending' | 'invalid' | 'already-accepted';
51
+ interface InviteAcceptDetails {
52
+ status: InviteAcceptStatus;
53
+ /** Workspace the invite grants access to. */
54
+ workspaceName?: string | null;
55
+ /** Who sent it, if known. */
56
+ inviterName?: string | null;
57
+ /** Role the invite grants. */
58
+ role?: WorkspaceRole | null;
59
+ /** Email the invite was addressed to. */
60
+ inviteEmail?: string | null;
61
+ /** The signed-in user's email, or null if not signed in. */
62
+ currentUserEmail?: string | null;
63
+ }
64
+ interface InviteAcceptPageProps {
65
+ details: InviteAcceptDetails;
66
+ /** Accept the invite; resolve with the workspace to navigate to. */
67
+ onAccept(): Promise<{
68
+ workspaceId?: string;
69
+ } | void>;
70
+ /** Navigate (sign in / sign up / switch account / open app). */
71
+ onNavigate(target: {
72
+ kind: 'sign-in' | 'sign-up' | 'switch-account' | 'open-app';
73
+ workspaceId?: string;
74
+ }): void;
75
+ }
76
+
77
+ declare function MembersPanel({ members, currentRole, onInvite, onChangeRole, onRemove, onNotice, }: MembersPanelProps): react.JSX.Element;
78
+
79
+ declare function InviteAcceptPage({ details, onAccept, onNavigate }: InviteAcceptPageProps): react.JSX.Element;
80
+
81
+ export { type InviteAcceptDetails, InviteAcceptPage, type InviteAcceptPageProps, type InviteAcceptStatus, type MemberView, MembersPanel, type MembersPanelProps };
@@ -0,0 +1,12 @@
1
+ import {
2
+ MembersPanel
3
+ } from "../chunk-3G334FVA.js";
4
+ import {
5
+ InviteAcceptPage
6
+ } from "../chunk-535V6BH6.js";
7
+ import "../chunk-63CE7FEZ.js";
8
+ export {
9
+ InviteAcceptPage,
10
+ MembersPanel
11
+ };
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ import * as react from 'react';
2
+ import { InviteAcceptPage, MembersPanel } from './index.js';
3
+ export { InviteAcceptPageProps, MembersPanelProps } from './index.js';
4
+ import '../roles-CLtYKHHl.js';
5
+
6
+ declare const MembersPanelLazy: react.LazyExoticComponent<typeof MembersPanel>;
7
+ declare const InviteAcceptPageLazy: react.LazyExoticComponent<typeof InviteAcceptPage>;
8
+
9
+ export { InviteAcceptPageLazy, MembersPanelLazy };
@@ -0,0 +1,13 @@
1
+ // src/teams-react/lazy.tsx
2
+ import { lazy } from "react";
3
+ var MembersPanelLazy = lazy(
4
+ () => import("../MembersPanel-4CGUQRPF.js").then((m) => ({ default: m.MembersPanel }))
5
+ );
6
+ var InviteAcceptPageLazy = lazy(
7
+ () => import("../InviteAcceptPage-EUOGZO2X.js").then((m) => ({ default: m.InviteAcceptPage }))
8
+ );
9
+ export {
10
+ InviteAcceptPageLazy,
11
+ MembersPanelLazy
12
+ };
13
+ //# sourceMappingURL=lazy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/teams-react/lazy.tsx"],"sourcesContent":["/**\n * Code-split entries for the teams React surface. Products that don't need the\n * members UI on initial load import these `React.lazy` handles instead of the\n * components directly, so the panel/page chunks load on first render. Mount\n * inside a `<Suspense>` boundary; the product provides the fallback.\n */\n\nimport { lazy } from 'react'\nimport type { MembersPanelProps, InviteAcceptPageProps } from './contracts'\n\nexport type { MembersPanelProps, InviteAcceptPageProps }\n\nexport const MembersPanelLazy = lazy(\n () => import('./components/MembersPanel').then((m) => ({ default: m.MembersPanel })),\n)\n\nexport const InviteAcceptPageLazy = lazy(\n () => import('./components/InviteAcceptPage').then((m) => ({ default: m.InviteAcceptPage })),\n)\n"],"mappings":";AAOA,SAAS,YAAY;AAKd,IAAM,mBAAmB;AAAA,EAC9B,MAAM,OAAO,6BAA2B,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE;AACrF;AAEO,IAAM,uBAAuB;AAAA,EAClC,MAAM,OAAO,iCAA+B,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE;AAC7F;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "packageManager": "pnpm@10.33.4",
5
5
  "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
6
6
  "keywords": [
@@ -228,6 +228,31 @@
228
228
  "import": "./dist/design-canvas-react/lazy.js",
229
229
  "default": "./dist/design-canvas-react/lazy.js"
230
230
  },
231
+ "./teams": {
232
+ "types": "./dist/teams/index.d.ts",
233
+ "import": "./dist/teams/index.js",
234
+ "default": "./dist/teams/index.js"
235
+ },
236
+ "./teams/drizzle": {
237
+ "types": "./dist/teams/drizzle.d.ts",
238
+ "import": "./dist/teams/drizzle.js",
239
+ "default": "./dist/teams/drizzle.js"
240
+ },
241
+ "./teams/members-api": {
242
+ "types": "./dist/teams/members-api.d.ts",
243
+ "import": "./dist/teams/members-api.js",
244
+ "default": "./dist/teams/members-api.js"
245
+ },
246
+ "./teams-react": {
247
+ "types": "./dist/teams-react/index.d.ts",
248
+ "import": "./dist/teams-react/index.js",
249
+ "default": "./dist/teams-react/index.js"
250
+ },
251
+ "./teams-react/lazy": {
252
+ "types": "./dist/teams-react/lazy.d.ts",
253
+ "import": "./dist/teams-react/lazy.js",
254
+ "default": "./dist/teams-react/lazy.js"
255
+ },
231
256
  "./theme": {
232
257
  "types": "./dist/theme/index.d.ts",
233
258
  "import": "./dist/theme/index.js",