@fieldwangai/agentflow 0.1.133 → 0.1.134

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.
@@ -2,6 +2,7 @@ import crypto from "crypto";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { getAgentflowDataRoot } from "./paths.mjs";
5
+ import { getTeamForUser } from "./teams.mjs";
5
6
 
6
7
  const REGISTRY_VERSION = 1;
7
8
 
@@ -52,11 +53,14 @@ function publicWorkflow(record, userId = "") {
52
53
  if (!record) return null;
53
54
  const actorId = normalizeUserId(userId);
54
55
  const members = record.members && typeof record.members === "object" ? record.members : {};
56
+ const access = prdWorkflowCollaborationAccess(record, actorId);
55
57
  return {
56
58
  id: record.id,
57
59
  tapdId: record.tapdId,
58
60
  ownerId: record.ownerId,
59
- role: actorId === record.ownerId ? "owner" : String(members[actorId] || ""),
61
+ role: access.role,
62
+ accessSource: access.source,
63
+ teamId: String(record.teamId || getTeamForUser(record.ownerId)?.id || ""),
60
64
  memberCount: Object.keys(members).length,
61
65
  members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
62
66
  shareActive: Boolean(record.shareToken),
@@ -69,11 +73,17 @@ function publicWorkflow(record, userId = "") {
69
73
  export function prdWorkflowCollaborationAccess(record, userId) {
70
74
  if (!record) return { allowed: true, role: "" };
71
75
  const actorId = normalizeUserId(userId);
72
- const role = actorId === record.ownerId ? "owner" : String(record.members?.[actorId] || "");
76
+ const directRole = actorId === record.ownerId ? "owner" : String(record.members?.[actorId] || "");
77
+ const actorTeam = getTeamForUser(actorId);
78
+ const recordTeamId = String(record.teamId || getTeamForUser(record.ownerId)?.id || "");
79
+ const teamRole = actorTeam?.status === "active" && actorTeam.id === recordTeamId ? "viewer" : "";
80
+ const role = directRole || teamRole;
73
81
  return {
74
82
  allowed: role === "owner" || role === "editor" || role === "viewer",
75
83
  writable: role === "owner" || role === "editor",
76
84
  role,
85
+ source: directRole ? "member" : teamRole ? "team" : "",
86
+ teamId: teamRole ? recordTeamId : "",
77
87
  };
78
88
  }
79
89
 
@@ -109,7 +119,17 @@ export function listPrdWorkflowCollaborationsForUser(userId) {
109
119
  const actorId = normalizeUserId(userId);
110
120
  if (!actorId) return [];
111
121
  return Object.values(readRegistry().workflows)
112
- .filter((record) => prdWorkflowCollaborationAccess(record, actorId).allowed)
122
+ .filter((record) => (
123
+ record?.ownerId === actorId || Boolean(record?.members?.[actorId])
124
+ ))
125
+ .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
126
+ }
127
+
128
+ export function listPrdWorkflowCollaborationsForTeam(teamId) {
129
+ const id = String(teamId || "").trim();
130
+ if (!id) return [];
131
+ return Object.values(readRegistry().workflows)
132
+ .filter((record) => String(record?.teamId || getTeamForUser(record?.ownerId)?.id || "") === id)
113
133
  .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
114
134
  }
115
135
 
@@ -129,6 +149,7 @@ export function ensurePrdWorkflowCollaboration({ tapdId, userId }) {
129
149
  id: workflowId,
130
150
  tapdId: normalizedTapdId,
131
151
  ownerId,
152
+ teamId: String(getTeamForUser(ownerId)?.id || ""),
132
153
  members: { [ownerId]: "owner" },
133
154
  createdAt: now,
134
155
  updatedAt: now,
@@ -0,0 +1,160 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { getAgentflowDataRoot } from "./paths.mjs";
5
+
6
+ const REGISTRY_VERSION = 1;
7
+
8
+ function registryPath() {
9
+ return path.join(getAgentflowDataRoot(), "organization", "teams.json");
10
+ }
11
+
12
+ function normalizeUserId(value) {
13
+ return String(value || "").trim().toLowerCase();
14
+ }
15
+
16
+ function normalizeTeamId(value) {
17
+ return String(value || "").trim();
18
+ }
19
+
20
+ function normalizeMembers(value) {
21
+ const members = Array.isArray(value) ? value : [];
22
+ return Array.from(new Set(members.map(normalizeUserId).filter(Boolean)));
23
+ }
24
+
25
+ function readRegistry() {
26
+ try {
27
+ const filePath = registryPath();
28
+ if (!fs.existsSync(filePath)) return { version: REGISTRY_VERSION, teams: {} };
29
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
30
+ return {
31
+ version: REGISTRY_VERSION,
32
+ teams: parsed?.teams && typeof parsed.teams === "object" && !Array.isArray(parsed.teams)
33
+ ? parsed.teams
34
+ : {},
35
+ };
36
+ } catch {
37
+ return { version: REGISTRY_VERSION, teams: {} };
38
+ }
39
+ }
40
+
41
+ function writeRegistry(registry) {
42
+ const filePath = registryPath();
43
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
44
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
45
+ fs.writeFileSync(tempPath, JSON.stringify({
46
+ version: REGISTRY_VERSION,
47
+ teams: registry?.teams || {},
48
+ }, null, 2) + "\n", "utf-8");
49
+ fs.renameSync(tempPath, filePath);
50
+ }
51
+
52
+ function publicTeam(record) {
53
+ if (!record) return null;
54
+ const members = normalizeMembers(record.members);
55
+ return {
56
+ id: String(record.id || ""),
57
+ name: String(record.name || ""),
58
+ description: String(record.description || ""),
59
+ status: record.status === "inactive" ? "inactive" : "active",
60
+ members,
61
+ memberCount: members.length,
62
+ createdAt: String(record.createdAt || ""),
63
+ updatedAt: String(record.updatedAt || ""),
64
+ };
65
+ }
66
+
67
+ export function listTeams({ includeInactive = true } = {}) {
68
+ return Object.values(readRegistry().teams)
69
+ .map(publicTeam)
70
+ .filter((team) => includeInactive || team.status === "active")
71
+ .sort((left, right) => left.name.localeCompare(right.name));
72
+ }
73
+
74
+ export function getTeamById(teamId) {
75
+ return publicTeam(readRegistry().teams[normalizeTeamId(teamId)] || null);
76
+ }
77
+
78
+ export function getTeamForUser(userId, { includeInactive = false } = {}) {
79
+ const id = normalizeUserId(userId);
80
+ if (!id) return null;
81
+ return listTeams({ includeInactive }).find((team) => team.members.includes(id)) || null;
82
+ }
83
+
84
+ export function createTeam({ name, description = "" } = {}) {
85
+ const normalizedName = String(name || "").trim();
86
+ if (!normalizedName) return { error: "团队名称不能为空", status: 400 };
87
+ const registry = readRegistry();
88
+ const duplicate = Object.values(registry.teams).some((team) => (
89
+ String(team?.name || "").trim().toLowerCase() === normalizedName.toLowerCase()
90
+ ));
91
+ if (duplicate) return { error: "已存在同名团队", status: 409 };
92
+ const now = new Date().toISOString();
93
+ const id = `team_${crypto.randomBytes(10).toString("hex")}`;
94
+ const record = {
95
+ id,
96
+ name: normalizedName,
97
+ description: String(description || "").trim(),
98
+ status: "active",
99
+ members: [],
100
+ createdAt: now,
101
+ updatedAt: now,
102
+ };
103
+ registry.teams[id] = record;
104
+ writeRegistry(registry);
105
+ return { team: publicTeam(record), created: true };
106
+ }
107
+
108
+ export function updateTeam(teamId, patch = {}) {
109
+ const id = normalizeTeamId(teamId);
110
+ const registry = readRegistry();
111
+ const record = registry.teams[id];
112
+ if (!record) return { error: "团队不存在", status: 404 };
113
+ if (Object.prototype.hasOwnProperty.call(patch, "name")) {
114
+ const name = String(patch.name || "").trim();
115
+ if (!name) return { error: "团队名称不能为空", status: 400 };
116
+ const duplicate = Object.values(registry.teams).some((team) => (
117
+ team?.id !== id && String(team?.name || "").trim().toLowerCase() === name.toLowerCase()
118
+ ));
119
+ if (duplicate) return { error: "已存在同名团队", status: 409 };
120
+ record.name = name;
121
+ }
122
+ if (Object.prototype.hasOwnProperty.call(patch, "description")) {
123
+ record.description = String(patch.description || "").trim();
124
+ }
125
+ if (Object.prototype.hasOwnProperty.call(patch, "status")) {
126
+ record.status = patch.status === "inactive" ? "inactive" : "active";
127
+ }
128
+ record.updatedAt = new Date().toISOString();
129
+ writeRegistry(registry);
130
+ return { team: publicTeam(record) };
131
+ }
132
+
133
+ export function setTeamMembers(teamId, members = []) {
134
+ const id = normalizeTeamId(teamId);
135
+ const registry = readRegistry();
136
+ const record = registry.teams[id];
137
+ if (!record) return { error: "团队不存在", status: 404 };
138
+ const normalized = normalizeMembers(members);
139
+ for (const team of Object.values(registry.teams)) {
140
+ if (!team || team.id === id) continue;
141
+ team.members = normalizeMembers(team.members).filter((memberId) => !normalized.includes(memberId));
142
+ }
143
+ record.members = normalized;
144
+ record.updatedAt = new Date().toISOString();
145
+ writeRegistry(registry);
146
+ return { team: publicTeam(record) };
147
+ }
148
+
149
+ export function deleteTeam(teamId) {
150
+ const id = normalizeTeamId(teamId);
151
+ const registry = readRegistry();
152
+ const record = registry.teams[id];
153
+ if (!record) return { error: "团队不存在", status: 404 };
154
+ if (normalizeMembers(record.members).length > 0) {
155
+ return { error: "请先移出团队中的全部成员", status: 409 };
156
+ }
157
+ delete registry.teams[id];
158
+ writeRegistry(registry);
159
+ return { deleted: true, teamId: id };
160
+ }
@@ -145,6 +145,8 @@ import {
145
145
  getWorkspaceCollaborationForProject,
146
146
  listWorkspaceCollaborationsForUser,
147
147
  removeWorkspaceCollaborationMember,
148
+ removeWorkspaceCollaborationTeamShare,
149
+ setWorkspaceCollaborationTeamShare,
148
150
  updateWorkspaceCollaborationFlow,
149
151
  workspaceCollaborationAccess,
150
152
  workspaceCollaborationSummary,
@@ -157,11 +159,21 @@ import {
157
159
  getPrdWorkflowCollaborationForUser,
158
160
  ensurePrdWorkflowShareLink,
159
161
  listPrdWorkflowCollaborationsForUser,
162
+ listPrdWorkflowCollaborationsForTeam,
160
163
  prdWorkflowCollaborationAccess,
161
164
  prdWorkflowCollaborationSummary,
162
165
  removePrdWorkflowCollaborationMember,
163
166
  revokePrdWorkflowShareLink,
164
167
  } from "./prd-workflow-collaboration.mjs";
168
+ import {
169
+ createTeam,
170
+ deleteTeam,
171
+ getTeamById,
172
+ getTeamForUser,
173
+ listTeams,
174
+ setTeamMembers,
175
+ updateTeam,
176
+ } from "./teams.mjs";
165
177
  import {
166
178
  legacyOverallToGlobalState,
167
179
  materializeWorkflowGlobalState,
@@ -3305,6 +3317,7 @@ function workspaceCollaborationSummaryWithUsers(record, userId) {
3305
3317
  const summary = workspaceCollaborationSummary(record, userId);
3306
3318
  if (!summary) return null;
3307
3319
  const users = readAuthUsers();
3320
+ const teams = new Map(listTeams().map((team) => [team.id, team]));
3308
3321
  return {
3309
3322
  ...summary,
3310
3323
  ownerUsername: String(users[summary.ownerId]?.username || summary.ownerId),
@@ -3312,6 +3325,23 @@ function workspaceCollaborationSummaryWithUsers(record, userId) {
3312
3325
  ...member,
3313
3326
  username: String(users[member.userId]?.username || member.userId),
3314
3327
  })),
3328
+ teamShares: (summary.teamShares || []).map((share) => ({
3329
+ ...share,
3330
+ teamName: String(teams.get(share.teamId)?.name || share.teamId),
3331
+ })),
3332
+ };
3333
+ }
3334
+
3335
+ function teamSummaryWithUsers(team) {
3336
+ if (!team) return null;
3337
+ const users = readAuthUsers();
3338
+ return {
3339
+ ...team,
3340
+ members: (team.members || []).map((userId) => ({
3341
+ userId,
3342
+ username: String(users[userId]?.username || userId),
3343
+ isAdmin: Boolean(users[userId]?.isAdmin),
3344
+ })),
3315
3345
  };
3316
3346
  }
3317
3347
 
@@ -3462,6 +3492,9 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3462
3492
  ownerId: String(collaboration.ownerId || ""),
3463
3493
  ownerUsername: String(collaboration.ownerUsername || collaboration.ownerId || ""),
3464
3494
  memberCount: Number(collaboration.memberCount || 0),
3495
+ accessSource: String(collaboration.accessSource || ""),
3496
+ teamId: String(collaboration.teamId || ""),
3497
+ teamName: String(getTeamById(collaboration.teamId)?.name || ""),
3465
3498
  shareActive: collaboration.shareActive === true,
3466
3499
  updatedAt: updatedAtTimestamp ? new Date(updatedAtTimestamp).toISOString() : String(record?.updatedAt || ""),
3467
3500
  };
@@ -12473,7 +12506,23 @@ export function startUiServer({
12473
12506
  return;
12474
12507
  }
12475
12508
  try {
12476
- const workflows = listPrdWorkflowCollaborationsForUser(userCtx.userId).map((record) => {
12509
+ const view = String(url.searchParams.get("view") || "personal").trim().toLowerCase();
12510
+ let team = null;
12511
+ let records;
12512
+ if (view === "team") {
12513
+ const requestedTeamId = String(url.searchParams.get("teamId") || "").trim();
12514
+ team = requestedTeamId && authUser?.isAdmin
12515
+ ? getTeamById(requestedTeamId)
12516
+ : getTeamForUser(userCtx.userId);
12517
+ if (!team || team.status !== "active") {
12518
+ json(res, 200, { ok: true, view: "team", team: null, workflows: [] });
12519
+ return;
12520
+ }
12521
+ records = listPrdWorkflowCollaborationsForTeam(team.id);
12522
+ } else {
12523
+ records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
12524
+ }
12525
+ const workflows = records.map((record) => {
12477
12526
  const stateRoot = path.resolve(getAgentflowUserDataRoot(record.ownerId));
12478
12527
  const tapdId = String(record.tapdId || "").trim();
12479
12528
  const project = prdWorkflowReadProjectState(stateRoot, tapdId);
@@ -12482,7 +12531,7 @@ export function startUiServer({
12482
12531
  const snapshot = project?.snapshot || latestClient || legacy?.snapshot || {};
12483
12532
  return prdWorkflowDashboardSummary(record, snapshot, userCtx);
12484
12533
  });
12485
- json(res, 200, { ok: true, workflows });
12534
+ json(res, 200, { ok: true, view: view === "team" ? "team" : "personal", team: teamSummaryWithUsers(team), workflows });
12486
12535
  } catch (error) {
12487
12536
  json(res, 500, { error: (error && error.message) || String(error) });
12488
12537
  }
@@ -14158,6 +14207,62 @@ export function startUiServer({
14158
14207
  return;
14159
14208
  }
14160
14209
 
14210
+ if (req.method === "GET" && url.pathname === "/api/teams/me") {
14211
+ const team = getTeamForUser(userCtx.userId);
14212
+ json(res, 200, { team: teamSummaryWithUsers(team) });
14213
+ return;
14214
+ }
14215
+
14216
+ if (url.pathname === "/api/admin/teams") {
14217
+ if (!authUser?.isAdmin) {
14218
+ json(res, 403, { error: "Admin permission required" });
14219
+ return;
14220
+ }
14221
+ if (req.method === "GET") {
14222
+ const assigned = new Set(listTeams().flatMap((team) => team.members || []));
14223
+ const users = Object.entries(readAuthUsers()).map(([userId, user]) => ({
14224
+ userId,
14225
+ username: String(user?.username || userId),
14226
+ isAdmin: Boolean(user?.isAdmin),
14227
+ assigned: assigned.has(userId),
14228
+ teamId: getTeamForUser(userId, { includeInactive: true })?.id || "",
14229
+ }));
14230
+ json(res, 200, { teams: listTeams().map(teamSummaryWithUsers), users });
14231
+ return;
14232
+ }
14233
+ let payload;
14234
+ try {
14235
+ payload = JSON.parse(await readBody(req));
14236
+ } catch {
14237
+ json(res, 400, { error: "Invalid JSON body" });
14238
+ return;
14239
+ }
14240
+ let result;
14241
+ if (req.method === "POST") {
14242
+ result = createTeam(payload || {});
14243
+ } else if (req.method === "PATCH") {
14244
+ result = updateTeam(payload?.teamId, payload || {});
14245
+ } else if (req.method === "PUT") {
14246
+ const users = readAuthUsers();
14247
+ const memberIds = Array.isArray(payload?.members) ? payload.members.map((value) => String(value || "").trim().toLowerCase()) : [];
14248
+ const unknown = memberIds.find((userId) => !users[userId]);
14249
+ result = unknown
14250
+ ? { error: `用户不存在:${unknown}`, status: 404 }
14251
+ : setTeamMembers(payload?.teamId, memberIds);
14252
+ } else if (req.method === "DELETE") {
14253
+ result = deleteTeam(payload?.teamId);
14254
+ } else {
14255
+ json(res, 405, { error: "Method not allowed" });
14256
+ return;
14257
+ }
14258
+ if (result?.error) {
14259
+ json(res, result.status || 400, { error: result.error });
14260
+ return;
14261
+ }
14262
+ json(res, 200, { ok: true, ...result, teams: listTeams().map(teamSummaryWithUsers) });
14263
+ return;
14264
+ }
14265
+
14161
14266
  if (url.pathname === "/api/feedback") {
14162
14267
  if (req.method === "POST") {
14163
14268
  let payload;
@@ -14345,6 +14450,8 @@ export function startUiServer({
14345
14450
  if (url.pathname === "/api/flows") {
14346
14451
  if (req.method === "GET") {
14347
14452
  try {
14453
+ const projectView = String(url.searchParams.get("view") || "all").trim().toLowerCase();
14454
+ const currentTeam = getTeamForUser(userCtx.userId);
14348
14455
  const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
14349
14456
  .filter((flow) => (
14350
14457
  !workspaceFlowCollaborationGuard(
@@ -14387,7 +14494,22 @@ export function startUiServer({
14387
14494
  });
14388
14495
  existingCollaborationIds.add(record.id);
14389
14496
  }
14390
- json(res, 200, flows);
14497
+ const visibleFlows = projectView === "team"
14498
+ ? flows.filter((flow) => (
14499
+ currentTeam
14500
+ && Array.isArray(flow.collaboration?.teamShares)
14501
+ && flow.collaboration.teamShares.some((share) => share.teamId === currentTeam.id)
14502
+ ))
14503
+ : projectView === "personal"
14504
+ ? flows.filter((flow) => (
14505
+ !flow.collaboration
14506
+ || flow.collaboration.ownerId === userCtx.userId
14507
+ || flow.collaboration.members?.some((member) => member.userId === userCtx.userId)
14508
+ || flow.source === "builtin"
14509
+ || flow.source === "admin"
14510
+ ))
14511
+ : flows;
14512
+ json(res, 200, visibleFlows);
14391
14513
  } catch (e) {
14392
14514
  json(res, 500, { error: (e && e.message) || String(e) });
14393
14515
  }
@@ -14714,6 +14836,74 @@ export function startUiServer({
14714
14836
  return;
14715
14837
  }
14716
14838
 
14839
+ if (url.pathname === "/api/workspace/collaboration/team-share" && (req.method === "POST" || req.method === "DELETE")) {
14840
+ try {
14841
+ const payload = JSON.parse(await readBody(req));
14842
+ const flowId = String(payload?.flowId || "").trim();
14843
+ const flowSource = String(payload?.flowSource || "user").trim();
14844
+ const archived = payload?.archived === true || payload?.flowArchived === true;
14845
+ if (!flowId || (flowSource !== "workspace" && flowSource !== "user")) {
14846
+ json(res, 400, { error: "当前 Project 不支持团队分享" });
14847
+ return;
14848
+ }
14849
+ const targetTeam = getTeamById(payload?.teamId);
14850
+ const actorTeam = getTeamForUser(userCtx.userId);
14851
+ if (!targetTeam || targetTeam.status !== "active") {
14852
+ json(res, 404, { error: "团队不存在或已停用" });
14853
+ return;
14854
+ }
14855
+ if (!authUser?.isAdmin && actorTeam?.id !== targetTeam.id) {
14856
+ json(res, 403, { error: "只能分享给自己所在的团队" });
14857
+ return;
14858
+ }
14859
+ const scoped = resolveWorkspaceScopeRoot(root, {
14860
+ flowId,
14861
+ flowSource,
14862
+ workspaceId: payload.workspaceId || "",
14863
+ archived,
14864
+ }, userCtx);
14865
+ if (scoped.error) {
14866
+ json(res, scoped.status || 400, { error: scoped.error });
14867
+ return;
14868
+ }
14869
+ const ensured = ensureWorkspaceCollaboration({
14870
+ flowId,
14871
+ flowSource,
14872
+ archived,
14873
+ userId: userCtx.userId,
14874
+ });
14875
+ if (ensured.error) {
14876
+ json(res, ensured.status || 400, { error: ensured.error });
14877
+ return;
14878
+ }
14879
+ const result = req.method === "POST"
14880
+ ? setWorkspaceCollaborationTeamShare({
14881
+ workspaceId: ensured.workspace.id,
14882
+ userId: userCtx.userId,
14883
+ teamId: targetTeam.id,
14884
+ role: payload?.role,
14885
+ })
14886
+ : removeWorkspaceCollaborationTeamShare({
14887
+ workspaceId: ensured.workspace.id,
14888
+ userId: userCtx.userId,
14889
+ teamId: targetTeam.id,
14890
+ });
14891
+ if (result.error) {
14892
+ json(res, result.status || 400, { error: result.error });
14893
+ return;
14894
+ }
14895
+ const record = getWorkspaceCollaborationForProject({ workspaceId: ensured.workspace.id });
14896
+ json(res, 200, {
14897
+ ok: true,
14898
+ workspace: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
14899
+ team: teamSummaryWithUsers(targetTeam),
14900
+ });
14901
+ } catch (e) {
14902
+ json(res, 400, { error: (e && e.message) || String(e) });
14903
+ }
14904
+ return;
14905
+ }
14906
+
14717
14907
  if (req.method === "DELETE" && url.pathname === "/api/workspace/collaboration/share") {
14718
14908
  try {
14719
14909
  const payload = JSON.parse(await readBody(req));
@@ -2,6 +2,7 @@ import crypto from "crypto";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { getAgentflowDataRoot } from "./paths.mjs";
5
+ import { getTeamForUser } from "./teams.mjs";
5
6
 
6
7
  const REGISTRY_VERSION = 1;
7
8
  const DEFAULT_INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -67,15 +68,19 @@ function publicWorkspace(record, userId = "") {
67
68
  if (!record) return null;
68
69
  const actorId = normalizeUserId(userId);
69
70
  const members = record.members && typeof record.members === "object" ? record.members : {};
71
+ const access = workspaceCollaborationAccess(record, actorId);
70
72
  return {
71
73
  id: record.id,
72
74
  flowId: record.flowId,
73
75
  flowSource: record.projectSource || record.flowSource || "workspace",
74
76
  archived: record.archived === true,
75
77
  ownerId: record.ownerId,
76
- role: actorId === record.ownerId ? "owner" : members[actorId] || "",
78
+ role: access.role,
79
+ accessSource: access.source,
80
+ teamId: access.teamId,
77
81
  memberCount: Object.keys(members).length,
78
82
  members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
83
+ teamShares: Object.entries(record.teamShares || {}).map(([teamId, role]) => ({ teamId, role })),
79
84
  createdAt: record.createdAt || "",
80
85
  updatedAt: record.updatedAt || "",
81
86
  };
@@ -98,12 +103,19 @@ export function getWorkspaceCollaborationById(workspaceId) {
98
103
  export function workspaceCollaborationAccess(record, userId) {
99
104
  if (!record) return { allowed: true, role: "" };
100
105
  const id = normalizeUserId(userId);
101
- const role = id === record.ownerId ? "owner" : String(record.members?.[id] || "");
106
+ const directRole = id === record.ownerId ? "owner" : String(record.members?.[id] || "");
107
+ const team = getTeamForUser(id);
108
+ const teamRole = team ? String(record.teamShares?.[team.id] || "") : "";
109
+ const role = directRole === "owner" || directRole === "editor" || teamRole === "editor"
110
+ ? directRole === "owner" ? "owner" : "editor"
111
+ : directRole === "viewer" || teamRole === "viewer" ? "viewer" : "";
102
112
  return {
103
113
  allowed: role === "owner" || role === "editor" || role === "viewer",
104
114
  writable: role === "owner" || role === "editor",
105
115
  runnable: role === "owner" || role === "editor",
106
116
  role,
117
+ source: directRole ? "member" : teamRole ? "team" : "",
118
+ teamId: teamRole ? team?.id || "" : "",
107
119
  };
108
120
  }
109
121
 
@@ -256,6 +268,44 @@ export function addWorkspaceCollaborationMember({
256
268
  return { workspace: publicWorkspace(record, actorId), memberUserId: targetId };
257
269
  }
258
270
 
271
+ export function setWorkspaceCollaborationTeamShare({
272
+ workspaceId,
273
+ userId,
274
+ teamId,
275
+ role = "viewer",
276
+ }) {
277
+ const registry = readRegistry();
278
+ const record = registry.workspaces[String(workspaceId || "").trim()];
279
+ if (!record) return { error: "Workspace collaboration not found", status: 404 };
280
+ const actorId = normalizeUserId(userId);
281
+ const targetTeamId = String(teamId || "").trim();
282
+ if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
283
+ return { error: "Only the workspace owner can share with a team", status: 403 };
284
+ }
285
+ if (!targetTeamId) return { error: "Missing teamId", status: 400 };
286
+ record.teamShares = record.teamShares && typeof record.teamShares === "object" ? record.teamShares : {};
287
+ record.teamShares[targetTeamId] = role === "editor" ? "editor" : "viewer";
288
+ record.updatedAt = new Date().toISOString();
289
+ writeRegistry(registry);
290
+ return { workspace: publicWorkspace(record, actorId), teamId: targetTeamId };
291
+ }
292
+
293
+ export function removeWorkspaceCollaborationTeamShare({ workspaceId, userId, teamId }) {
294
+ const registry = readRegistry();
295
+ const record = registry.workspaces[String(workspaceId || "").trim()];
296
+ if (!record) return { error: "Workspace collaboration not found", status: 404 };
297
+ const actorId = normalizeUserId(userId);
298
+ const targetTeamId = String(teamId || "").trim();
299
+ if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
300
+ return { error: "Only the workspace owner can revoke a team share", status: 403 };
301
+ }
302
+ const removed = Boolean(record.teamShares?.[targetTeamId]);
303
+ if (removed) delete record.teamShares[targetTeamId];
304
+ record.updatedAt = new Date().toISOString();
305
+ writeRegistry(registry);
306
+ return { workspace: publicWorkspace(record, actorId), teamId: targetTeamId, removed };
307
+ }
308
+
259
309
  export function removeWorkspaceCollaborationMember({
260
310
  workspaceId,
261
311
  userId,