@tangle-network/agent-app 0.42.2 → 0.42.4

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 (49) hide show
  1. package/dist/InvitationsPanel-ZKAUYZYU.js +8 -0
  2. package/dist/InviteAcceptPage-P3QJC3C6.js +7 -0
  3. package/dist/{MembersPanel-4CGUQRPF.js → MembersPanel-FOVHWUQ3.js} +2 -2
  4. package/dist/MembersPanel-FOVHWUQ3.js.map +1 -0
  5. package/dist/{access-ChAHG4Tp.d.ts → access-BZ81Btt5.d.ts} +1 -1
  6. package/dist/chunk-F7D7YCUL.js +206 -0
  7. package/dist/chunk-F7D7YCUL.js.map +1 -0
  8. package/dist/{chunk-WOVCWPFF.js → chunk-FJUOBNIY.js} +1 -1
  9. package/dist/{chunk-3G334FVA.js → chunk-GNL3MG5J.js} +4 -3
  10. package/dist/chunk-GNL3MG5J.js.map +1 -0
  11. package/dist/chunk-SWUVTGMR.js +262 -0
  12. package/dist/chunk-SWUVTGMR.js.map +1 -0
  13. package/dist/{chunk-535V6BH6.js → chunk-VCPZ3HTN.js} +44 -4
  14. package/dist/chunk-VCPZ3HTN.js.map +1 -0
  15. package/dist/chunk-WEBBJBDH.js +58 -0
  16. package/dist/chunk-WEBBJBDH.js.map +1 -0
  17. package/dist/{chunk-MY75I367.js → chunk-WYBP3AKG.js} +18 -9
  18. package/dist/chunk-WYBP3AKG.js.map +1 -0
  19. package/dist/design-canvas-react/index.js +1 -1
  20. package/dist/design-canvas-react/lazy.js +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/invitations-CZ5G2tIn.d.ts +56 -0
  23. package/dist/invitations-schema-DaMO4hZE.d.ts +280 -0
  24. package/dist/{roles-CLtYKHHl.d.ts → roles-BC1n4t37.d.ts} +1 -1
  25. package/dist/sandbox/index.js +1 -1
  26. package/dist/teams/drizzle.d.ts +4 -3
  27. package/dist/teams/drizzle.js +31 -1
  28. package/dist/teams/drizzle.js.map +1 -1
  29. package/dist/teams/index.d.ts +2 -1
  30. package/dist/teams/index.js +16 -0
  31. package/dist/teams/invitations-api.d.ts +169 -0
  32. package/dist/teams/invitations-api.js +338 -0
  33. package/dist/teams/invitations-api.js.map +1 -0
  34. package/dist/teams/members-api.d.ts +2 -2
  35. package/dist/teams/members-api.js +5 -255
  36. package/dist/teams/members-api.js.map +1 -1
  37. package/dist/teams-react/index.d.ts +66 -7
  38. package/dist/teams-react/index.js +6 -2
  39. package/dist/teams-react/lazy.d.ts +6 -4
  40. package/dist/teams-react/lazy.js +6 -2
  41. package/dist/teams-react/lazy.js.map +1 -1
  42. package/package.json +8 -3
  43. package/dist/InviteAcceptPage-EUOGZO2X.js +0 -7
  44. package/dist/chunk-3G334FVA.js.map +0 -1
  45. package/dist/chunk-535V6BH6.js.map +0 -1
  46. package/dist/chunk-MY75I367.js.map +0 -1
  47. /package/dist/{InviteAcceptPage-EUOGZO2X.js.map → InvitationsPanel-ZKAUYZYU.js.map} +0 -0
  48. /package/dist/{MembersPanel-4CGUQRPF.js.map → InviteAcceptPage-P3QJC3C6.js.map} +0 -0
  49. /package/dist/{chunk-WOVCWPFF.js.map → chunk-FJUOBNIY.js.map} +0 -0
@@ -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":[]}
@@ -1,16 +1,24 @@
1
1
  // src/teams-react/components/InviteAcceptPage.tsx
2
2
  import { useState } from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
- function InviteAcceptPage({ details, onAccept, onNavigate }) {
4
+ function InviteAcceptPage({ details, onAccept, onNavigate, onResendVerification }) {
5
5
  const [accepting, setAccepting] = useState(false);
6
6
  const [acceptError, setAcceptError] = useState(null);
7
7
  const [accepted, setAccepted] = useState(false);
8
+ const [resendingVerification, setResendingVerification] = useState(false);
9
+ const [verificationSent, setVerificationSent] = useState(false);
8
10
  if (details.status === "invalid") {
9
- return /* @__PURE__ */ jsx(Shell, { title: "Invalid invite", body: "This invite link is invalid or has expired.", children: /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "sign-in" }), children: "Go to sign in" }) });
11
+ return /* @__PURE__ */ jsx(Shell, { title: "Invalid invite", body: "This invite link is invalid.", children: /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "sign-in" }), children: "Go to sign in" }) });
10
12
  }
11
13
  if (details.status === "already-accepted") {
12
14
  return /* @__PURE__ */ jsx(Shell, { title: "Already accepted", body: "This invite has already been accepted.", children: /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "open-app" }), children: "Open workspace" }) });
13
15
  }
16
+ if (details.status === "expired") {
17
+ return /* @__PURE__ */ jsx(Shell, { title: "Invite expired", body: "This invitation has expired. Ask the workspace admin to send a new one.", children: /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "sign-in" }), children: "Go to sign in" }) });
18
+ }
19
+ if (details.status === "revoked") {
20
+ return /* @__PURE__ */ jsx(Shell, { title: "Invite revoked", body: "This invitation has been revoked. Ask the workspace admin to send a new one.", children: /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "sign-in" }), children: "Go to sign in" }) });
21
+ }
14
22
  const workspaceName = details.workspaceName ?? "a workspace";
15
23
  const inviterPrefix = details.inviterName ? `${details.inviterName} invited you` : "You've been invited";
16
24
  const roleSuffix = details.role ? ` as ${details.role}` : "";
@@ -45,6 +53,8 @@ function InviteAcceptPage({ details, onAccept, onNavigate }) {
45
53
  const emailMismatch = Boolean(
46
54
  details.inviteEmail && details.inviteEmail.toLowerCase() !== details.currentUserEmail.toLowerCase()
47
55
  );
56
+ const needsVerification = Boolean(details.needsEmailVerification) && !emailMismatch;
57
+ const expiryLabel = details.expiresAt != null ? new Date(details.expiresAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : null;
48
58
  async function handleAccept() {
49
59
  setAccepting(true);
50
60
  setAcceptError(null);
@@ -60,6 +70,16 @@ function InviteAcceptPage({ details, onAccept, onNavigate }) {
60
70
  setAccepting(false);
61
71
  }
62
72
  }
73
+ async function handleResendVerification() {
74
+ if (!onResendVerification || resendingVerification) return;
75
+ setResendingVerification(true);
76
+ try {
77
+ await onResendVerification();
78
+ setVerificationSent(true);
79
+ } finally {
80
+ setResendingVerification(false);
81
+ }
82
+ }
63
83
  return /* @__PURE__ */ jsxs(Shell, { title: "Join workspace", children: [
64
84
  /* @__PURE__ */ jsxs("p", { className: "mb-4 text-sm text-[var(--text-secondary)]", children: [
65
85
  inviterPrefix,
@@ -72,6 +92,11 @@ function InviteAcceptPage({ details, onAccept, onNavigate }) {
72
92
  "Signed in as ",
73
93
  /* @__PURE__ */ jsx("span", { className: "font-medium text-[var(--text-primary)]", children: details.currentUserEmail })
74
94
  ] }),
95
+ expiryLabel && /* @__PURE__ */ jsxs("p", { className: "mb-4 text-xs text-[var(--text-muted)]", children: [
96
+ "This invitation expires on ",
97
+ expiryLabel,
98
+ "."
99
+ ] }),
75
100
  emailMismatch && /* @__PURE__ */ jsxs(
76
101
  "div",
77
102
  {
@@ -84,9 +109,24 @@ function InviteAcceptPage({ details, onAccept, onNavigate }) {
84
109
  ]
85
110
  }
86
111
  ),
112
+ needsVerification && /* @__PURE__ */ jsx(
113
+ "div",
114
+ {
115
+ role: "alert",
116
+ className: "mb-4 rounded-md border border-[var(--border-default)] px-4 py-2 text-sm text-[var(--text-warning)]",
117
+ children: verificationSent ? "Verification email sent \u2014 check your inbox, then refresh this page to accept." : "Verify your email address before you can accept this invitation."
118
+ }
119
+ ),
87
120
  acceptError && /* @__PURE__ */ jsx("p", { role: "alert", className: "mb-4 text-sm text-[var(--text-danger)]", children: acceptError }),
88
121
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
89
- emailMismatch ? /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "switch-account" }), children: "Switch account" }) : /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => void handleAccept(), disabled: accepting, children: accepting ? "Accepting\u2026" : "Accept invite" }),
122
+ emailMismatch ? /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "switch-account" }), children: "Switch account" }) : needsVerification ? onResendVerification ? /* @__PURE__ */ jsx(
123
+ PrimaryButton,
124
+ {
125
+ onClick: () => void handleResendVerification(),
126
+ disabled: resendingVerification || verificationSent,
127
+ children: verificationSent ? "Verification sent" : resendingVerification ? "Sending\u2026" : "Resend verification email"
128
+ }
129
+ ) : /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => onNavigate({ kind: "open-app" }), children: "Open app" }) : /* @__PURE__ */ jsx(PrimaryButton, { onClick: () => void handleAccept(), disabled: accepting, children: accepting ? "Accepting\u2026" : "Accept invite" }),
90
130
  /* @__PURE__ */ jsx(SecondaryButton, { onClick: () => onNavigate({ kind: "open-app" }), children: "Not now" })
91
131
  ] })
92
132
  ] });
@@ -125,4 +165,4 @@ function SecondaryButton({ children, onClick }) {
125
165
  export {
126
166
  InviteAcceptPage
127
167
  };
128
- //# sourceMappingURL=chunk-535V6BH6.js.map
168
+ //# sourceMappingURL=chunk-VCPZ3HTN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/teams-react/components/InviteAcceptPage.tsx"],"sourcesContent":["/**\n * Invite-accept surface for `/invite/:token`. Renders the invite state (valid /\n * invalid / already-accepted), handles the signed-out path (sign in or create\n * an account with the invited email), the email-mismatch path (switch account),\n * and the accept action. Callback-driven: the host supplies the resolved\n * `details` and `onAccept` / `onNavigate`, so this imports no app router or\n * fetch client. Styled with the shipped Tangle Quiet tokens (`var(--*)`).\n */\n\nimport { useState } from 'react'\nimport type { InviteAcceptPageProps } from '../contracts'\n\nexport function InviteAcceptPage({ details, onAccept, onNavigate, onResendVerification }: InviteAcceptPageProps) {\n const [accepting, setAccepting] = useState(false)\n const [acceptError, setAcceptError] = useState<string | null>(null)\n const [accepted, setAccepted] = useState(false)\n const [resendingVerification, setResendingVerification] = useState(false)\n const [verificationSent, setVerificationSent] = useState(false)\n\n if (details.status === 'invalid') {\n return (\n <Shell title=\"Invalid invite\" body=\"This invite link is invalid.\">\n <PrimaryButton onClick={() => onNavigate({ kind: 'sign-in' })}>Go to sign in</PrimaryButton>\n </Shell>\n )\n }\n\n if (details.status === 'already-accepted') {\n return (\n <Shell title=\"Already accepted\" body=\"This invite has already been accepted.\">\n <PrimaryButton onClick={() => onNavigate({ kind: 'open-app' })}>Open workspace</PrimaryButton>\n </Shell>\n )\n }\n\n if (details.status === 'expired') {\n return (\n <Shell title=\"Invite expired\" body=\"This invitation has expired. Ask the workspace admin to send a new one.\">\n <PrimaryButton onClick={() => onNavigate({ kind: 'sign-in' })}>Go to sign in</PrimaryButton>\n </Shell>\n )\n }\n\n if (details.status === 'revoked') {\n return (\n <Shell title=\"Invite revoked\" body=\"This invitation has been revoked. Ask the workspace admin to send a new one.\">\n <PrimaryButton onClick={() => onNavigate({ kind: 'sign-in' })}>Go to sign in</PrimaryButton>\n </Shell>\n )\n }\n\n const workspaceName = details.workspaceName ?? 'a workspace'\n const inviterPrefix = details.inviterName ? `${details.inviterName} invited you` : \"You've been invited\"\n const roleSuffix = details.role ? ` as ${details.role}` : ''\n\n if (!details.currentUserEmail) {\n return (\n <Shell title=\"You've been invited\">\n <p className=\"mb-4 text-sm text-[var(--text-secondary)]\">\n {inviterPrefix} to join <span className=\"font-medium text-[var(--text-primary)]\">{workspaceName}</span>{roleSuffix}.\n </p>\n {details.inviteEmail && (\n <p className=\"mb-6 text-sm text-[var(--text-secondary)]\">\n Sign in or create an account with{' '}\n <span className=\"font-medium text-[var(--text-primary)]\">{details.inviteEmail}</span> to accept.\n </p>\n )}\n <div className=\"flex gap-2\">\n <PrimaryButton onClick={() => onNavigate({ kind: 'sign-in' })}>Sign in</PrimaryButton>\n <SecondaryButton onClick={() => onNavigate({ kind: 'sign-up' })}>Create account</SecondaryButton>\n </div>\n </Shell>\n )\n }\n\n if (accepted) {\n return (\n <Shell title=\"Welcome!\">\n <p className=\"text-sm text-[var(--text-secondary)]\">\n You've joined <span className=\"font-medium text-[var(--text-primary)]\">{workspaceName}</span>.\n </p>\n </Shell>\n )\n }\n\n const emailMismatch = Boolean(\n details.inviteEmail &&\n details.inviteEmail.toLowerCase() !== details.currentUserEmail.toLowerCase(),\n )\n const needsVerification = Boolean(details.needsEmailVerification) && !emailMismatch\n const expiryLabel = details.expiresAt != null\n ? new Date(details.expiresAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })\n : null\n\n async function handleAccept() {\n setAccepting(true)\n setAcceptError(null)\n try {\n const result = await onAccept()\n setAccepted(true)\n if (result && 'workspaceId' in result && result.workspaceId) {\n onNavigate({ kind: 'open-app', workspaceId: result.workspaceId })\n }\n } catch (err) {\n setAcceptError(err instanceof Error ? err.message : 'Failed to accept invite')\n } finally {\n setAccepting(false)\n }\n }\n\n async function handleResendVerification() {\n if (!onResendVerification || resendingVerification) return\n setResendingVerification(true)\n try {\n await onResendVerification()\n setVerificationSent(true)\n } finally {\n setResendingVerification(false)\n }\n }\n\n return (\n <Shell title=\"Join workspace\">\n <p className=\"mb-4 text-sm text-[var(--text-secondary)]\">\n {inviterPrefix} to join <span className=\"font-medium text-[var(--text-primary)]\">{workspaceName}</span>{roleSuffix}.\n </p>\n <p className=\"mb-4 text-sm text-[var(--text-secondary)]\">\n Signed in as <span className=\"font-medium text-[var(--text-primary)]\">{details.currentUserEmail}</span>\n </p>\n {expiryLabel && (\n <p className=\"mb-4 text-xs text-[var(--text-muted)]\">This invitation expires on {expiryLabel}.</p>\n )}\n {emailMismatch && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-[var(--border-default)] px-4 py-2 text-sm text-[var(--text-warning)]\"\n >\n This invite was sent to <span className=\"font-medium\">{details.inviteEmail}</span>. Switch to that account to accept it.\n </div>\n )}\n {needsVerification && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-[var(--border-default)] px-4 py-2 text-sm text-[var(--text-warning)]\"\n >\n {verificationSent\n ? 'Verification email sent — check your inbox, then refresh this page to accept.'\n : 'Verify your email address before you can accept this invitation.'}\n </div>\n )}\n {acceptError && (\n <p role=\"alert\" className=\"mb-4 text-sm text-[var(--text-danger)]\">{acceptError}</p>\n )}\n <div className=\"flex gap-2\">\n {emailMismatch ? (\n <PrimaryButton onClick={() => onNavigate({ kind: 'switch-account' })}>Switch account</PrimaryButton>\n ) : needsVerification ? (\n onResendVerification ? (\n <PrimaryButton\n onClick={() => void handleResendVerification()}\n disabled={resendingVerification || verificationSent}\n >\n {verificationSent ? 'Verification sent' : resendingVerification ? 'Sending…' : 'Resend verification email'}\n </PrimaryButton>\n ) : (\n <PrimaryButton onClick={() => onNavigate({ kind: 'open-app' })}>Open app</PrimaryButton>\n )\n ) : (\n <PrimaryButton onClick={() => void handleAccept()} disabled={accepting}>\n {accepting ? 'Accepting…' : 'Accept invite'}\n </PrimaryButton>\n )}\n <SecondaryButton onClick={() => onNavigate({ kind: 'open-app' })}>Not now</SecondaryButton>\n </div>\n </Shell>\n )\n}\n\nfunction Shell({ title, body, children }: { title: string; body?: string; children: React.ReactNode }) {\n return (\n <div className=\"mx-auto flex w-full max-w-sm flex-col\">\n <h1 className=\"mb-1 text-xl font-semibold tracking-tight text-[var(--text-primary)]\">{title}</h1>\n {body && <p className=\"mb-6 text-sm text-[var(--text-secondary)]\">{body}</p>}\n {children}\n </div>\n )\n}\n\nfunction PrimaryButton({ children, onClick, disabled }: { children: React.ReactNode; onClick(): void; disabled?: boolean }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className=\"flex-1 rounded bg-[var(--brand-primary)] px-4 py-2 text-sm text-white disabled:opacity-50\"\n >\n {children}\n </button>\n )\n}\n\nfunction SecondaryButton({ children, onClick }: { children: React.ReactNode; onClick(): void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"flex-1 rounded border border-[var(--border-default)] px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {children}\n </button>\n )\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,58 @@
1
+ // src/teams/invitations.ts
2
+ var INVITATION_EXPIRY_DAYS = 7;
3
+ var INVITATION_PERMISSIONS = ["admin", "editor", "viewer"];
4
+ var TOKEN_BYTE_LENGTH = 32;
5
+ function normalizeInvitationEmail(email) {
6
+ return email.trim().toLowerCase();
7
+ }
8
+ function parseInvitationPermission(value) {
9
+ return INVITATION_PERMISSIONS.includes(value) ? value : null;
10
+ }
11
+ function getInvitationExpiresAt(now = /* @__PURE__ */ new Date()) {
12
+ return new Date(now.getTime() + INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1e3);
13
+ }
14
+ function generateInvitationToken() {
15
+ const bytes = new Uint8Array(TOKEN_BYTE_LENGTH);
16
+ globalThis.crypto.getRandomValues(bytes);
17
+ const token = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
18
+ return `inv_${btoa(token).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "")}`;
19
+ }
20
+ function inviteUrlForToken(origin, token) {
21
+ return `${origin.replace(/\/+$/, "")}/invite/${encodeURIComponent(token)}`;
22
+ }
23
+ function renderInvitationEmail(input, brand) {
24
+ const role = input.permission.toLowerCase();
25
+ const expiry = input.expiresAt.toLocaleDateString("en-US", {
26
+ month: "short",
27
+ day: "numeric",
28
+ year: "numeric"
29
+ });
30
+ return {
31
+ from: brand.fromAddress,
32
+ subject: `${input.inviterEmail} invited you to ${input.workspaceName}`,
33
+ html: [
34
+ `<p>${escapeHtml(input.inviterEmail)} invited you to join <strong>${escapeHtml(input.workspaceName)}</strong> as ${escapeHtml(role)}.</p>`,
35
+ `<p><a href="${input.inviteUrl}">Accept the invitation</a></p>`,
36
+ `<p>This invitation expires on ${expiry}.</p>`
37
+ ].join(""),
38
+ text: [
39
+ `${input.inviterEmail} invited you to join ${input.workspaceName} as ${role}.`,
40
+ `Accept the invitation: ${input.inviteUrl}`,
41
+ `This invitation expires on ${expiry}.`
42
+ ].join("\n\n")
43
+ };
44
+ }
45
+ function escapeHtml(value) {
46
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
47
+ }
48
+
49
+ export {
50
+ INVITATION_EXPIRY_DAYS,
51
+ normalizeInvitationEmail,
52
+ parseInvitationPermission,
53
+ getInvitationExpiresAt,
54
+ generateInvitationToken,
55
+ inviteUrlForToken,
56
+ renderInvitationEmail
57
+ };
58
+ //# sourceMappingURL=chunk-WEBBJBDH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/teams/invitations.ts"],"sourcesContent":["/**\n * Pure invitation helpers + email-template renderer for the teams capability.\n * Zero dependencies: no drizzle, no env, no react, no network. The lifecycle API\n * (`./invitations-api`) and an app's own mail transport build on these; this leaf\n * imports nothing back, so a consumer can pull just the token/expiry math or the\n * template renderer without dragging in drizzle or a mail client.\n *\n * `renderInvitationEmail` is deliberately transport-free: it returns the\n * `{ from, subject, html, text }` an app hands to its own Resend/SES/etc. The\n * secret (API key) and the network call stay in the app's seam — agent-app ships\n * only the deterministic template.\n */\n\nimport type { AssignableWorkspaceRole } from './roles'\n\n/** The role an invitation grants — the assignable workspace ladder (never owner). */\nexport type InvitationPermission = AssignableWorkspaceRole\n\nexport type InvitationStatus = 'pending' | 'accepted' | 'expired' | 'revoked'\nexport type InvitationEmailStatus = 'not_sent' | 'sent' | 'failed'\n\nexport const INVITATION_EXPIRY_DAYS = 7\n\nconst INVITATION_PERMISSIONS = ['admin', 'editor', 'viewer'] as const\nconst TOKEN_BYTE_LENGTH = 32\n\nexport function normalizeInvitationEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nexport function parseInvitationPermission(value: string | undefined): InvitationPermission | null {\n return INVITATION_PERMISSIONS.includes(value as InvitationPermission) ? (value as InvitationPermission) : null\n}\n\nexport function getInvitationExpiresAt(now: Date = new Date()): Date {\n return new Date(now.getTime() + INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000)\n}\n\n/**\n * A cryptographically-random, URL-safe invitation token. `inv_`-prefixed so a\n * token is self-identifying and never collides with the workspaceMember invite\n * tokens minted by `members-api` (`generateInviteToken`).\n */\nexport function generateInvitationToken(): string {\n const bytes = new Uint8Array(TOKEN_BYTE_LENGTH)\n globalThis.crypto.getRandomValues(bytes)\n const token = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')\n return `inv_${btoa(token).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '')}`\n}\n\nexport function inviteUrlForToken(origin: string, token: string): string {\n return `${origin.replace(/\\/+$/, '')}/invite/${encodeURIComponent(token)}`\n}\n\n// ── email template (pure; no transport) ──\n\nexport interface RenderInvitationEmailInput {\n to: string\n workspaceName: string\n inviterEmail: string\n permission: string\n inviteUrl: string\n expiresAt: Date\n}\n\nexport interface InvitationEmailBrand {\n /** RFC-5322 From header, e.g. `GTM Agent <noreply@gtm.tangle.tools>`. */\n fromAddress: string\n}\n\nexport interface RenderedInvitationEmail {\n from: string\n subject: string\n html: string\n text: string\n}\n\n/**\n * Render the invitation email body — pure, deterministic, transport-free. The\n * caller passes the result to its own mail client; this never reads a secret or\n * touches the network.\n */\nexport function renderInvitationEmail(\n input: RenderInvitationEmailInput,\n brand: InvitationEmailBrand,\n): RenderedInvitationEmail {\n const role = input.permission.toLowerCase()\n const expiry = input.expiresAt.toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n })\n return {\n from: brand.fromAddress,\n subject: `${input.inviterEmail} invited you to ${input.workspaceName}`,\n html: [\n `<p>${escapeHtml(input.inviterEmail)} invited you to join <strong>${escapeHtml(input.workspaceName)}</strong> as ${escapeHtml(role)}.</p>`,\n `<p><a href=\"${input.inviteUrl}\">Accept the invitation</a></p>`,\n `<p>This invitation expires on ${expiry}.</p>`,\n ].join(''),\n text: [\n `${input.inviterEmail} invited you to join ${input.workspaceName} as ${role}.`,\n `Accept the invitation: ${input.inviteUrl}`,\n `This invitation expires on ${expiry}.`,\n ].join('\\n\\n'),\n }\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n"],"mappings":";AAqBO,IAAM,yBAAyB;AAEtC,IAAM,yBAAyB,CAAC,SAAS,UAAU,QAAQ;AAC3D,IAAM,oBAAoB;AAEnB,SAAS,yBAAyB,OAAuB;AAC9D,SAAO,MAAM,KAAK,EAAE,YAAY;AAClC;AAEO,SAAS,0BAA0B,OAAwD;AAChG,SAAO,uBAAuB,SAAS,KAA6B,IAAK,QAAiC;AAC5G;AAEO,SAAS,uBAAuB,MAAY,oBAAI,KAAK,GAAS;AACnE,SAAO,IAAI,KAAK,IAAI,QAAQ,IAAI,yBAAyB,KAAK,KAAK,KAAK,GAAI;AAC9E;AAOO,SAAS,0BAAkC;AAChD,QAAM,QAAQ,IAAI,WAAW,iBAAiB;AAC9C,aAAW,OAAO,gBAAgB,KAAK;AACvC,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,SAAS,OAAO,aAAa,IAAI,CAAC,EAAE,KAAK,EAAE;AAC5E,SAAO,OAAO,KAAK,KAAK,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,QAAQ,EAAE,CAAC;AACvF;AAEO,SAAS,kBAAkB,QAAgB,OAAuB;AACvE,SAAO,GAAG,OAAO,QAAQ,QAAQ,EAAE,CAAC,WAAW,mBAAmB,KAAK,CAAC;AAC1E;AA8BO,SAAS,sBACd,OACA,OACyB;AACzB,QAAM,OAAO,MAAM,WAAW,YAAY;AAC1C,QAAM,SAAS,MAAM,UAAU,mBAAmB,SAAS;AAAA,IACzD,OAAO;AAAA,IACP,KAAK;AAAA,IACL,MAAM;AAAA,EACR,CAAC;AACD,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,SAAS,GAAG,MAAM,YAAY,mBAAmB,MAAM,aAAa;AAAA,IACpE,MAAM;AAAA,MACJ,MAAM,WAAW,MAAM,YAAY,CAAC,gCAAgC,WAAW,MAAM,aAAa,CAAC,gBAAgB,WAAW,IAAI,CAAC;AAAA,MACnI,eAAe,MAAM,SAAS;AAAA,MAC9B,iCAAiC,MAAM;AAAA,IACzC,EAAE,KAAK,EAAE;AAAA,IACT,MAAM;AAAA,MACJ,GAAG,MAAM,YAAY,wBAAwB,MAAM,aAAa,OAAO,IAAI;AAAA,MAC3E,0BAA0B,MAAM,SAAS;AAAA,MACzC,8BAA8B,MAAM;AAAA,IACtC,EAAE,KAAK,MAAM;AAAA,EACf;AACF;AAEA,SAAS,WAAW,OAAuB;AACzC,SAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;","names":[]}
@@ -674,8 +674,23 @@ async function writeProfileFilesToBox(box, files, options = {}) {
674
674
  const execTimeoutMs = options.execTimeoutMs ?? PROFILE_WRITE_EXEC_TIMEOUT_MS;
675
675
  const paceMs = options.paceMs ?? PROFILE_WRITE_PACE_MS;
676
676
  const maxRetries = options.maxRetries ?? PROFILE_WRITE_MAX_RETRIES;
677
+ const fileApiAvailable = typeof box.fs?.writeMany === "function";
678
+ const viaFileApi = [];
679
+ const viaExec = [];
680
+ for (const mount of files) {
681
+ if (mount.resource.kind !== "inline") continue;
682
+ const fileApiPath = fileApiAvailable ? fileApiTarget(mount) : null;
683
+ if (fileApiPath !== null) viaFileApi.push({ path: fileApiPath, content: mount.resource.content ?? "" });
684
+ else viaExec.push(mount);
685
+ }
686
+ if (viaFileApi.length > 0) {
687
+ try {
688
+ await box.fs.writeMany(viaFileApi, { paceMs, maxRetries });
689
+ } catch (err) {
690
+ return fail(new Error("writeProfileFilesToBox: file-API batch write failed", { cause: err }));
691
+ }
692
+ }
677
693
  let execStarted = false;
678
- const fileApiAvailable = typeof box.fs?.write === "function";
679
694
  const paceAndRetry = async (run, path) => {
680
695
  for (let attempt = 0; ; attempt++) {
681
696
  if (execStarted && paceMs > 0) await sleep(paceMs);
@@ -693,16 +708,10 @@ async function writeProfileFilesToBox(box, files, options = {}) {
693
708
  }
694
709
  }
695
710
  };
696
- for (const mount of files) {
711
+ for (const mount of viaExec) {
697
712
  if (mount.resource.kind !== "inline") continue;
698
713
  const content = mount.resource.content ?? "";
699
714
  const path = mount.path;
700
- const fileApiPath = fileApiAvailable ? fileApiTarget(mount) : null;
701
- if (fileApiPath !== null) {
702
- const res2 = await paceAndRetry(() => box.fs.write(fileApiPath, content), path);
703
- if (!res2.succeeded) return res2;
704
- continue;
705
- }
706
715
  const b64 = Buffer.from(content, "utf8").toString("base64");
707
716
  const b64Chunks = [];
708
717
  for (let i = 0; i < b64.length; i += PROFILE_WRITE_B64_CHUNK_CHARS) {
@@ -1405,4 +1414,4 @@ export {
1405
1414
  isTerminalPromptEvent,
1406
1415
  detectInteractiveQuestion
1407
1416
  };
1408
- //# sourceMappingURL=chunk-MY75I367.js.map
1417
+ //# sourceMappingURL=chunk-WYBP3AKG.js.map