@burdenoff/be-sdk 2026.529.3 → 2026.601.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.d.ts +18 -10
- package/dist/auth/index.js +1 -1
- package/dist/chunk-DVAZQEOL.js +1007 -0
- package/dist/chunk-DVAZQEOL.js.map +1 -0
- package/dist/{chunk-7NUQXGA7.js → chunk-KKCC3Z4I.js} +108 -30
- package/dist/chunk-KKCC3Z4I.js.map +1 -0
- package/dist/i18n/index.d.ts +3 -2
- package/dist/i18n-DrdPTann.d.ts +33 -0
- package/dist/{index-BV3G48cH.d.ts → index-D4l1u8VR.d.ts} +2 -1
- package/dist/index-kssw5ziY.d.ts +250 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +8 -226
- package/dist/sharing/index.d.ts +1 -0
- package/dist/sharing/index.js +5 -0
- package/dist/sharing/index.js.map +1 -0
- package/dist/{i18n-DA6IPnYy.d.ts → types-C8TbSdEj.d.ts} +1 -31
- package/dist/types-sKXodL2N.d.ts +227 -0
- package/package.json +3 -3
- package/dist/chunk-7NUQXGA7.js.map +0 -1
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import { logger } from './chunk-CBZNP6RF.js';
|
|
2
|
+
import { __export, __name } from './chunk-XE4OXN2W.js';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import { GraphQLError } from 'graphql';
|
|
5
|
+
|
|
6
|
+
// src/sharing/index.ts
|
|
7
|
+
var sharing_exports = {};
|
|
8
|
+
__export(sharing_exports, {
|
|
9
|
+
RESOURCE_CAPABILITY_MAP: () => RESOURCE_CAPABILITY_MAP,
|
|
10
|
+
RESOURCE_TYPE_LABELS: () => RESOURCE_TYPE_LABELS,
|
|
11
|
+
ROLE_CAPABILITIES: () => ROLE_CAPABILITIES,
|
|
12
|
+
ROLE_ORDER: () => ROLE_ORDER,
|
|
13
|
+
SHARE_EMAIL_TEMPLATE_SLUG: () => SHARE_EMAIL_TEMPLATE_SLUG,
|
|
14
|
+
SHARE_GRANT_PARTIAL_UNIQUE_SQL: () => SHARE_GRANT_PARTIAL_UNIQUE_SQL,
|
|
15
|
+
SHARE_GRANT_PRISMA: () => SHARE_GRANT_PRISMA,
|
|
16
|
+
SHARE_GRANT_SDL: () => SHARE_GRANT_SDL,
|
|
17
|
+
ShareRole: () => ShareRole,
|
|
18
|
+
ShareService: () => ShareService,
|
|
19
|
+
ShareStatus: () => ShareStatus,
|
|
20
|
+
ShareVisibility: () => ShareVisibility,
|
|
21
|
+
buildPerEntityShareSdl: () => buildPerEntityShareSdl,
|
|
22
|
+
buildShareEmailVariables: () => buildShareEmailVariables,
|
|
23
|
+
capabilitiesFor: () => capabilitiesFor,
|
|
24
|
+
legacyToRole: () => legacyToRole,
|
|
25
|
+
makeSendShareEmail: () => makeSendShareEmail,
|
|
26
|
+
maxRole: () => maxRole,
|
|
27
|
+
normalizeStatus: () => normalizeStatus,
|
|
28
|
+
resourceTypeLabel: () => resourceTypeLabel,
|
|
29
|
+
roleAtLeast: () => roleAtLeast,
|
|
30
|
+
roleLabel: () => roleLabel,
|
|
31
|
+
roleToLegacy: () => roleToLegacy
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// src/sharing/types.ts
|
|
35
|
+
var ShareVisibility = /* @__PURE__ */ (function(ShareVisibility2) {
|
|
36
|
+
ShareVisibility2["PRIVATE"] = "PRIVATE";
|
|
37
|
+
ShareVisibility2["SHARED_WITH_USERS"] = "SHARED_WITH_USERS";
|
|
38
|
+
ShareVisibility2["SHARED_WITH_VIBE"] = "SHARED_WITH_VIBE";
|
|
39
|
+
ShareVisibility2["PUBLIC_LINK"] = "PUBLIC_LINK";
|
|
40
|
+
return ShareVisibility2;
|
|
41
|
+
})({});
|
|
42
|
+
var ShareRole = /* @__PURE__ */ (function(ShareRole2) {
|
|
43
|
+
ShareRole2["VIEWER"] = "VIEWER";
|
|
44
|
+
ShareRole2["COMMENTER"] = "COMMENTER";
|
|
45
|
+
ShareRole2["EDITOR"] = "EDITOR";
|
|
46
|
+
ShareRole2["MANAGER"] = "MANAGER";
|
|
47
|
+
return ShareRole2;
|
|
48
|
+
})({});
|
|
49
|
+
var ShareStatus = /* @__PURE__ */ (function(ShareStatus2) {
|
|
50
|
+
ShareStatus2["PENDING"] = "PENDING";
|
|
51
|
+
ShareStatus2["ACCEPTED"] = "ACCEPTED";
|
|
52
|
+
ShareStatus2["REVOKED"] = "REVOKED";
|
|
53
|
+
ShareStatus2["EXPIRED"] = "EXPIRED";
|
|
54
|
+
return ShareStatus2;
|
|
55
|
+
})({});
|
|
56
|
+
var ROLE_ORDER = {
|
|
57
|
+
["VIEWER"]: 0,
|
|
58
|
+
["COMMENTER"]: 1,
|
|
59
|
+
["EDITOR"]: 2,
|
|
60
|
+
["MANAGER"]: 3
|
|
61
|
+
};
|
|
62
|
+
function roleAtLeast(role, required) {
|
|
63
|
+
return ROLE_ORDER[role] >= ROLE_ORDER[required];
|
|
64
|
+
}
|
|
65
|
+
__name(roleAtLeast, "roleAtLeast");
|
|
66
|
+
function maxRole(a, b) {
|
|
67
|
+
return ROLE_ORDER[a] >= ROLE_ORDER[b] ? a : b;
|
|
68
|
+
}
|
|
69
|
+
__name(maxRole, "maxRole");
|
|
70
|
+
var ROLE_CAPABILITIES = {
|
|
71
|
+
["VIEWER"]: {
|
|
72
|
+
canView: true,
|
|
73
|
+
canComment: false,
|
|
74
|
+
canEdit: false,
|
|
75
|
+
canManage: false,
|
|
76
|
+
canExecute: false
|
|
77
|
+
},
|
|
78
|
+
["COMMENTER"]: {
|
|
79
|
+
canView: true,
|
|
80
|
+
canComment: true,
|
|
81
|
+
canEdit: false,
|
|
82
|
+
canManage: false,
|
|
83
|
+
canExecute: false
|
|
84
|
+
},
|
|
85
|
+
["EDITOR"]: {
|
|
86
|
+
canView: true,
|
|
87
|
+
canComment: true,
|
|
88
|
+
canEdit: true,
|
|
89
|
+
canManage: false,
|
|
90
|
+
canExecute: true
|
|
91
|
+
},
|
|
92
|
+
["MANAGER"]: {
|
|
93
|
+
canView: true,
|
|
94
|
+
canComment: true,
|
|
95
|
+
canEdit: true,
|
|
96
|
+
canManage: true,
|
|
97
|
+
canExecute: true
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function capabilitiesFor(role) {
|
|
101
|
+
return ROLE_CAPABILITIES[role];
|
|
102
|
+
}
|
|
103
|
+
__name(capabilitiesFor, "capabilitiesFor");
|
|
104
|
+
var RESOURCE_CAPABILITY_MAP = {
|
|
105
|
+
session: {
|
|
106
|
+
["VIEWER"]: "VIEW",
|
|
107
|
+
["COMMENTER"]: "VIEW",
|
|
108
|
+
["EDITOR"]: "INTERACT",
|
|
109
|
+
["MANAGER"]: "INTERACT"
|
|
110
|
+
},
|
|
111
|
+
vibedeck: {
|
|
112
|
+
["VIEWER"]: "VIEW",
|
|
113
|
+
["COMMENTER"]: "VIEW",
|
|
114
|
+
["EDITOR"]: "EXECUTE",
|
|
115
|
+
["MANAGER"]: "EXECUTE"
|
|
116
|
+
},
|
|
117
|
+
file: {
|
|
118
|
+
["VIEWER"]: "VIEW",
|
|
119
|
+
["COMMENTER"]: "COMMENT",
|
|
120
|
+
["EDITOR"]: "EDIT",
|
|
121
|
+
["MANAGER"]: "EDIT"
|
|
122
|
+
},
|
|
123
|
+
folder: {
|
|
124
|
+
["VIEWER"]: "VIEW",
|
|
125
|
+
["COMMENTER"]: "COMMENT",
|
|
126
|
+
["EDITOR"]: "EDIT",
|
|
127
|
+
["MANAGER"]: "EDIT"
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function roleToLegacy(resourceType, role) {
|
|
131
|
+
return RESOURCE_CAPABILITY_MAP[resourceType]?.[role] ?? role;
|
|
132
|
+
}
|
|
133
|
+
__name(roleToLegacy, "roleToLegacy");
|
|
134
|
+
function legacyToRole(resourceType, legacy) {
|
|
135
|
+
const table = RESOURCE_CAPABILITY_MAP[resourceType];
|
|
136
|
+
if (table) {
|
|
137
|
+
for (const role of [
|
|
138
|
+
"VIEWER",
|
|
139
|
+
"COMMENTER",
|
|
140
|
+
"EDITOR",
|
|
141
|
+
"MANAGER"
|
|
142
|
+
]) {
|
|
143
|
+
if (table[role] === legacy) return role;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
switch (legacy.toUpperCase()) {
|
|
147
|
+
case "MANAGER":
|
|
148
|
+
case "OWNER":
|
|
149
|
+
case "ADMIN":
|
|
150
|
+
return "MANAGER";
|
|
151
|
+
case "EDITOR":
|
|
152
|
+
case "EDIT":
|
|
153
|
+
case "INTERACT":
|
|
154
|
+
case "EXECUTE":
|
|
155
|
+
case "WRITE":
|
|
156
|
+
return "EDITOR";
|
|
157
|
+
case "COMMENTER":
|
|
158
|
+
case "COMMENT":
|
|
159
|
+
return "COMMENTER";
|
|
160
|
+
default:
|
|
161
|
+
return "VIEWER";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
__name(legacyToRole, "legacyToRole");
|
|
165
|
+
function normalizeStatus(status) {
|
|
166
|
+
const upper = status.toUpperCase();
|
|
167
|
+
if (upper === "ACTIVE") return "ACCEPTED";
|
|
168
|
+
if (upper in ShareStatus) return upper;
|
|
169
|
+
return "PENDING";
|
|
170
|
+
}
|
|
171
|
+
__name(normalizeStatus, "normalizeStatus");
|
|
172
|
+
var ACTIVE_STATUSES = [
|
|
173
|
+
ShareStatus.ACCEPTED,
|
|
174
|
+
"ACTIVE"
|
|
175
|
+
];
|
|
176
|
+
var DENY = {
|
|
177
|
+
allowed: false,
|
|
178
|
+
effectiveRole: null,
|
|
179
|
+
via: "none"
|
|
180
|
+
};
|
|
181
|
+
function notFound(message = "Not found") {
|
|
182
|
+
return new GraphQLError(message, {
|
|
183
|
+
extensions: {
|
|
184
|
+
code: "NOT_FOUND"
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
__name(notFound, "notFound");
|
|
189
|
+
function forbidden(message = "Forbidden") {
|
|
190
|
+
return new GraphQLError(message, {
|
|
191
|
+
extensions: {
|
|
192
|
+
code: "FORBIDDEN"
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
__name(forbidden, "forbidden");
|
|
197
|
+
function badRequest(message) {
|
|
198
|
+
return new GraphQLError(message, {
|
|
199
|
+
extensions: {
|
|
200
|
+
code: "BAD_REQUEST"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
__name(badRequest, "badRequest");
|
|
205
|
+
function generateToken() {
|
|
206
|
+
return randomBytes(32).toString("hex");
|
|
207
|
+
}
|
|
208
|
+
__name(generateToken, "generateToken");
|
|
209
|
+
function emailsMatch(a, b) {
|
|
210
|
+
return !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
211
|
+
}
|
|
212
|
+
__name(emailsMatch, "emailsMatch");
|
|
213
|
+
function pickHighestRole(candidates) {
|
|
214
|
+
let best = null;
|
|
215
|
+
for (const c of candidates) {
|
|
216
|
+
if (!best || maxRole(best.role, c.role) === c.role && c.role !== best.role) best = c;
|
|
217
|
+
}
|
|
218
|
+
return best;
|
|
219
|
+
}
|
|
220
|
+
__name(pickHighestRole, "pickHighestRole");
|
|
221
|
+
var ShareService = class {
|
|
222
|
+
static {
|
|
223
|
+
__name(this, "ShareService");
|
|
224
|
+
}
|
|
225
|
+
prisma;
|
|
226
|
+
resourceType;
|
|
227
|
+
workspaceId;
|
|
228
|
+
deps;
|
|
229
|
+
sharedWithVibeRole;
|
|
230
|
+
requireWorkspaceMember;
|
|
231
|
+
constructor(config) {
|
|
232
|
+
this.prisma = config.prisma;
|
|
233
|
+
this.resourceType = config.resourceType;
|
|
234
|
+
this.workspaceId = config.workspaceId;
|
|
235
|
+
this.deps = config.deps;
|
|
236
|
+
this.sharedWithVibeRole = config.options?.sharedWithVibeRole ?? ShareRole.VIEWER;
|
|
237
|
+
this.requireWorkspaceMember = config.options?.requireWorkspaceMember ?? true;
|
|
238
|
+
}
|
|
239
|
+
// -------------------------------------------------------------------------
|
|
240
|
+
// Access decisions
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Resolve the EFFECTIVE access of `actorId` on `resourceId`.
|
|
244
|
+
*
|
|
245
|
+
* Default-DENY. Considers (highest wins): owner ⇒ MANAGER, an active
|
|
246
|
+
* PUBLIC_LINK grant ⇒ its role, SHARED_WITH_VIBE + hasVibeAccess ⇒ baseline,
|
|
247
|
+
* an ACCEPTED (never PENDING) non-expired SHARED_WITH_USERS grant ⇒ its role.
|
|
248
|
+
* Fails CLOSED if a dependency throws.
|
|
249
|
+
*/
|
|
250
|
+
async resolveAccess(actorId, resourceId) {
|
|
251
|
+
try {
|
|
252
|
+
const meta = await this.deps.loadResourceMeta(resourceId);
|
|
253
|
+
if (!meta) return DENY;
|
|
254
|
+
if (actorId && meta.ownerId === actorId) {
|
|
255
|
+
return {
|
|
256
|
+
allowed: true,
|
|
257
|
+
effectiveRole: ShareRole.MANAGER,
|
|
258
|
+
via: "owner"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const candidates = [];
|
|
262
|
+
if (meta.visibility === ShareVisibility.SHARED_WITH_VIBE && meta.vibeId && actorId) {
|
|
263
|
+
if (await this.deps.hasVibeAccess(actorId, meta.vibeId, this.workspaceId)) {
|
|
264
|
+
candidates.push({
|
|
265
|
+
role: this.sharedWithVibeRole,
|
|
266
|
+
via: "vibe"
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const grant of await this.queryActiveAccessGrants(actorId, resourceId)) {
|
|
271
|
+
const isPublic = grant.visibility === ShareVisibility.PUBLIC_LINK;
|
|
272
|
+
const isMyUserGrant = grant.visibility === ShareVisibility.SHARED_WITH_USERS && grant.granteeUserId === actorId;
|
|
273
|
+
if (isPublic) candidates.push({
|
|
274
|
+
role: grant.role,
|
|
275
|
+
via: "public_link"
|
|
276
|
+
});
|
|
277
|
+
else if (isMyUserGrant) candidates.push({
|
|
278
|
+
role: grant.role,
|
|
279
|
+
via: "user_grant"
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const best = pickHighestRole(candidates);
|
|
283
|
+
if (!best) return DENY;
|
|
284
|
+
return {
|
|
285
|
+
allowed: true,
|
|
286
|
+
effectiveRole: best.role,
|
|
287
|
+
via: best.via
|
|
288
|
+
};
|
|
289
|
+
} catch (err) {
|
|
290
|
+
logger.warn("[ShareService] resolveAccess failed; denying", {
|
|
291
|
+
resourceType: this.resourceType,
|
|
292
|
+
resourceId,
|
|
293
|
+
err
|
|
294
|
+
});
|
|
295
|
+
return DENY;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Active (non-expired) PUBLIC_LINK grants + this actor's SHARED_WITH_USERS grants. */
|
|
299
|
+
async queryActiveAccessGrants(actorId, resourceId) {
|
|
300
|
+
return this.prisma.findMany({
|
|
301
|
+
where: {
|
|
302
|
+
resourceType: this.resourceType,
|
|
303
|
+
resourceId,
|
|
304
|
+
deletedAt: null,
|
|
305
|
+
status: {
|
|
306
|
+
in: [
|
|
307
|
+
...ACTIVE_STATUSES
|
|
308
|
+
]
|
|
309
|
+
},
|
|
310
|
+
OR: [
|
|
311
|
+
{
|
|
312
|
+
visibility: ShareVisibility.PUBLIC_LINK
|
|
313
|
+
},
|
|
314
|
+
...actorId ? [
|
|
315
|
+
{
|
|
316
|
+
granteeUserId: actorId,
|
|
317
|
+
visibility: ShareVisibility.SHARED_WITH_USERS
|
|
318
|
+
}
|
|
319
|
+
] : []
|
|
320
|
+
],
|
|
321
|
+
AND: [
|
|
322
|
+
{
|
|
323
|
+
OR: [
|
|
324
|
+
{
|
|
325
|
+
expiresAt: null
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
expiresAt: {
|
|
329
|
+
gt: /* @__PURE__ */ new Date()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/** True if `actorId` may act on `resourceId` at >= `requiredRole`. */
|
|
339
|
+
async canAccess(actorId, resourceId, requiredRole) {
|
|
340
|
+
const result = await this.resolveAccess(actorId, resourceId);
|
|
341
|
+
return result.allowed && result.effectiveRole !== null && roleAtLeast(result.effectiveRole, requiredRole);
|
|
342
|
+
}
|
|
343
|
+
/** Assert >= requiredRole; throws NOT_FOUND (read) / FORBIDDEN (mutate) otherwise. */
|
|
344
|
+
async assertAccess(actorId, resourceId, requiredRole, mode = "read") {
|
|
345
|
+
if (await this.canAccess(actorId, resourceId, requiredRole)) return;
|
|
346
|
+
throw mode === "read" ? notFound() : forbidden("You do not have permission to perform this action");
|
|
347
|
+
}
|
|
348
|
+
// -------------------------------------------------------------------------
|
|
349
|
+
// Share creation
|
|
350
|
+
// -------------------------------------------------------------------------
|
|
351
|
+
/** Invite a specific person by email (PENDING + token + email). */
|
|
352
|
+
async createUserShare(actor, input) {
|
|
353
|
+
const meta = await this.deps.loadResourceMeta(input.resourceId);
|
|
354
|
+
if (!meta) throw notFound("Resource not found");
|
|
355
|
+
await this.assertAccess(actor.id, input.resourceId, ShareRole.MANAGER, "mutate");
|
|
356
|
+
const email = input.granteeEmail.trim().toLowerCase();
|
|
357
|
+
if (!email) throw badRequest("A recipient email is required");
|
|
358
|
+
const granteeUserId = await this.deps.resolveUserByEmail(email);
|
|
359
|
+
if (granteeUserId && granteeUserId === actor.id) {
|
|
360
|
+
throw badRequest("You cannot share a resource with yourself");
|
|
361
|
+
}
|
|
362
|
+
if (granteeUserId && this.requireWorkspaceMember) {
|
|
363
|
+
const isMember = await this.deps.isWorkspaceMember(granteeUserId, this.workspaceId);
|
|
364
|
+
if (isMember === false) {
|
|
365
|
+
throw forbidden("You can only share with members of this workspace");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const existing = await this.prisma.findFirst({
|
|
369
|
+
where: {
|
|
370
|
+
resourceType: this.resourceType,
|
|
371
|
+
resourceId: input.resourceId,
|
|
372
|
+
deletedAt: null,
|
|
373
|
+
status: {
|
|
374
|
+
in: [
|
|
375
|
+
ShareStatus.PENDING,
|
|
376
|
+
ShareStatus.ACCEPTED
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
OR: [
|
|
380
|
+
...granteeUserId ? [
|
|
381
|
+
{
|
|
382
|
+
granteeUserId
|
|
383
|
+
}
|
|
384
|
+
] : [],
|
|
385
|
+
{
|
|
386
|
+
granteeEmail: email
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
let grant;
|
|
392
|
+
if (existing) {
|
|
393
|
+
grant = await this.prisma.update({
|
|
394
|
+
where: {
|
|
395
|
+
id: existing.id
|
|
396
|
+
},
|
|
397
|
+
data: {
|
|
398
|
+
role: input.role,
|
|
399
|
+
message: input.message ?? null,
|
|
400
|
+
expiresAt: input.expiresAt ?? null,
|
|
401
|
+
...granteeUserId && !existing.granteeUserId ? {
|
|
402
|
+
granteeUserId
|
|
403
|
+
} : {}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
} else {
|
|
407
|
+
grant = await this.prisma.create({
|
|
408
|
+
data: {
|
|
409
|
+
workspaceId: this.workspaceId,
|
|
410
|
+
resourceType: this.resourceType,
|
|
411
|
+
resourceId: input.resourceId,
|
|
412
|
+
granteeUserId: granteeUserId ?? null,
|
|
413
|
+
granteeEmail: email,
|
|
414
|
+
role: input.role,
|
|
415
|
+
visibility: ShareVisibility.SHARED_WITH_USERS,
|
|
416
|
+
status: ShareStatus.PENDING,
|
|
417
|
+
token: generateToken(),
|
|
418
|
+
expiresAt: input.expiresAt ?? null,
|
|
419
|
+
message: input.message ?? null,
|
|
420
|
+
createdBy: actor.id,
|
|
421
|
+
onBehalfOf: actor.id
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
await this.fireShareEmail(actor, grant, meta.name);
|
|
426
|
+
return grant;
|
|
427
|
+
}
|
|
428
|
+
/** Create an immediately-active public link (anyone with the URL). */
|
|
429
|
+
async createLinkShare(actor, input) {
|
|
430
|
+
const meta = await this.deps.loadResourceMeta(input.resourceId);
|
|
431
|
+
if (!meta) throw notFound("Resource not found");
|
|
432
|
+
await this.assertAccess(actor.id, input.resourceId, ShareRole.MANAGER, "mutate");
|
|
433
|
+
return this.prisma.create({
|
|
434
|
+
data: {
|
|
435
|
+
workspaceId: this.workspaceId,
|
|
436
|
+
resourceType: this.resourceType,
|
|
437
|
+
resourceId: input.resourceId,
|
|
438
|
+
granteeUserId: null,
|
|
439
|
+
granteeEmail: null,
|
|
440
|
+
role: input.role,
|
|
441
|
+
visibility: ShareVisibility.PUBLIC_LINK,
|
|
442
|
+
status: ShareStatus.ACCEPTED,
|
|
443
|
+
token: generateToken(),
|
|
444
|
+
expiresAt: input.expiresAt ?? null,
|
|
445
|
+
message: null,
|
|
446
|
+
createdBy: actor.id,
|
|
447
|
+
onBehalfOf: actor.id
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// -------------------------------------------------------------------------
|
|
452
|
+
// Accept / decline
|
|
453
|
+
// -------------------------------------------------------------------------
|
|
454
|
+
/** Explicitly accept a SHARED_WITH_USERS invite. Idempotent. */
|
|
455
|
+
async acceptShare(token, actor) {
|
|
456
|
+
const grant = await this.findActiveByToken(token);
|
|
457
|
+
if (!grant) throw notFound("This share link is invalid or has expired");
|
|
458
|
+
if (grant.visibility === ShareVisibility.PUBLIC_LINK) {
|
|
459
|
+
return grant;
|
|
460
|
+
}
|
|
461
|
+
const idMatches = !!grant.granteeUserId && grant.granteeUserId === actor.id;
|
|
462
|
+
const emailMatches = emailsMatch(grant.granteeEmail, actor.email);
|
|
463
|
+
if (!idMatches && !emailMatches) {
|
|
464
|
+
throw forbidden("This invitation was sent to a different account");
|
|
465
|
+
}
|
|
466
|
+
if (grant.status === ShareStatus.ACCEPTED) {
|
|
467
|
+
if (!grant.granteeUserId) {
|
|
468
|
+
return this.prisma.update({
|
|
469
|
+
where: {
|
|
470
|
+
id: grant.id
|
|
471
|
+
},
|
|
472
|
+
data: {
|
|
473
|
+
granteeUserId: actor.id
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return grant;
|
|
478
|
+
}
|
|
479
|
+
const now = /* @__PURE__ */ new Date();
|
|
480
|
+
const res = await this.prisma.updateMany({
|
|
481
|
+
where: {
|
|
482
|
+
id: grant.id,
|
|
483
|
+
status: ShareStatus.PENDING
|
|
484
|
+
},
|
|
485
|
+
data: {
|
|
486
|
+
status: ShareStatus.ACCEPTED,
|
|
487
|
+
acceptedAt: now,
|
|
488
|
+
acceptedBy: actor.id,
|
|
489
|
+
granteeUserId: actor.id
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
if (res.count === 0) {
|
|
493
|
+
const fresh = await this.prisma.findFirst({
|
|
494
|
+
where: {
|
|
495
|
+
id: grant.id
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
if (fresh) return fresh;
|
|
499
|
+
}
|
|
500
|
+
const updated = await this.prisma.findFirst({
|
|
501
|
+
where: {
|
|
502
|
+
id: grant.id
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
if (!updated) throw notFound();
|
|
506
|
+
return updated;
|
|
507
|
+
}
|
|
508
|
+
/** Decline an invite (marks it REVOKED). */
|
|
509
|
+
async declineShare(token, actor) {
|
|
510
|
+
const grant = await this.findActiveByToken(token);
|
|
511
|
+
if (!grant) return false;
|
|
512
|
+
const idMatches = !!grant.granteeUserId && grant.granteeUserId === actor.id;
|
|
513
|
+
const emailMatches = emailsMatch(grant.granteeEmail, actor.email);
|
|
514
|
+
if (!idMatches && !emailMatches) throw forbidden("This invitation was sent to a different account");
|
|
515
|
+
await this.prisma.update({
|
|
516
|
+
where: {
|
|
517
|
+
id: grant.id
|
|
518
|
+
},
|
|
519
|
+
data: {
|
|
520
|
+
status: ShareStatus.REVOKED,
|
|
521
|
+
revokedAt: /* @__PURE__ */ new Date(),
|
|
522
|
+
revokedBy: actor.id
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
/** Public preview of a grant by token (safe fields only — never the resource body). */
|
|
528
|
+
async shareGrantByToken(token) {
|
|
529
|
+
return this.findActiveByToken(token);
|
|
530
|
+
}
|
|
531
|
+
// -------------------------------------------------------------------------
|
|
532
|
+
// Management
|
|
533
|
+
// -------------------------------------------------------------------------
|
|
534
|
+
/** Revoke a grant or link. Owner/MANAGER only. */
|
|
535
|
+
async revoke(grantId, actor) {
|
|
536
|
+
const grant = await this.prisma.findFirst({
|
|
537
|
+
where: {
|
|
538
|
+
id: grantId,
|
|
539
|
+
resourceType: this.resourceType
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
if (!grant || grant.deletedAt) throw notFound();
|
|
543
|
+
await this.assertAccess(actor.id, grant.resourceId, ShareRole.MANAGER, "mutate");
|
|
544
|
+
return this.prisma.update({
|
|
545
|
+
where: {
|
|
546
|
+
id: grantId
|
|
547
|
+
},
|
|
548
|
+
data: {
|
|
549
|
+
status: ShareStatus.REVOKED,
|
|
550
|
+
revokedAt: /* @__PURE__ */ new Date(),
|
|
551
|
+
revokedBy: actor.id
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/** Change the role on a grant. Owner/MANAGER only. */
|
|
556
|
+
async updateRole(grantId, role, actor) {
|
|
557
|
+
const grant = await this.prisma.findFirst({
|
|
558
|
+
where: {
|
|
559
|
+
id: grantId,
|
|
560
|
+
resourceType: this.resourceType
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
if (!grant || grant.deletedAt) throw notFound();
|
|
564
|
+
await this.assertAccess(actor.id, grant.resourceId, ShareRole.MANAGER, "mutate");
|
|
565
|
+
return this.prisma.update({
|
|
566
|
+
where: {
|
|
567
|
+
id: grantId
|
|
568
|
+
},
|
|
569
|
+
data: {
|
|
570
|
+
role
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// -------------------------------------------------------------------------
|
|
575
|
+
// Reads
|
|
576
|
+
// -------------------------------------------------------------------------
|
|
577
|
+
/** All active grants on a resource (caller must be owner/MANAGER — gate in resolver). */
|
|
578
|
+
async resourceShares(resourceId) {
|
|
579
|
+
return this.prisma.findMany({
|
|
580
|
+
where: {
|
|
581
|
+
resourceType: this.resourceType,
|
|
582
|
+
resourceId,
|
|
583
|
+
deletedAt: null,
|
|
584
|
+
status: {
|
|
585
|
+
in: [
|
|
586
|
+
ShareStatus.PENDING,
|
|
587
|
+
ShareStatus.ACCEPTED
|
|
588
|
+
]
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
orderBy: {
|
|
592
|
+
createdAt: "desc"
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
/** Grants where the actor is the (accepted-or-pending) grantee. */
|
|
597
|
+
async sharesSharedWithMe(actorId, opts) {
|
|
598
|
+
return this.prisma.findMany({
|
|
599
|
+
where: {
|
|
600
|
+
...opts?.resourceType ? {
|
|
601
|
+
resourceType: opts.resourceType
|
|
602
|
+
} : {},
|
|
603
|
+
granteeUserId: actorId,
|
|
604
|
+
deletedAt: null,
|
|
605
|
+
status: ShareStatus.ACCEPTED,
|
|
606
|
+
OR: [
|
|
607
|
+
{
|
|
608
|
+
expiresAt: null
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
expiresAt: {
|
|
612
|
+
gt: /* @__PURE__ */ new Date()
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
},
|
|
617
|
+
orderBy: {
|
|
618
|
+
acceptedAt: "desc"
|
|
619
|
+
},
|
|
620
|
+
take: opts?.take,
|
|
621
|
+
skip: opts?.skip
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Resource ids the actor can see via an ACCEPTED user grant — for narrowing
|
|
626
|
+
* list resolvers (`WHERE id IN (...)`).
|
|
627
|
+
*/
|
|
628
|
+
async listAccessibleResourceIds(actorId) {
|
|
629
|
+
const grants = await this.prisma.findMany({
|
|
630
|
+
where: {
|
|
631
|
+
resourceType: this.resourceType,
|
|
632
|
+
granteeUserId: actorId,
|
|
633
|
+
deletedAt: null,
|
|
634
|
+
status: ShareStatus.ACCEPTED,
|
|
635
|
+
visibility: ShareVisibility.SHARED_WITH_USERS,
|
|
636
|
+
OR: [
|
|
637
|
+
{
|
|
638
|
+
expiresAt: null
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
expiresAt: {
|
|
642
|
+
gt: /* @__PURE__ */ new Date()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
]
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
return Array.from(new Set(grants.map((g) => g.resourceId)));
|
|
649
|
+
}
|
|
650
|
+
/** Resource ids with an active public link — for list narrowing. */
|
|
651
|
+
async listPublicResourceIds() {
|
|
652
|
+
const grants = await this.prisma.findMany({
|
|
653
|
+
where: {
|
|
654
|
+
resourceType: this.resourceType,
|
|
655
|
+
deletedAt: null,
|
|
656
|
+
status: {
|
|
657
|
+
in: [
|
|
658
|
+
...ACTIVE_STATUSES
|
|
659
|
+
]
|
|
660
|
+
},
|
|
661
|
+
visibility: ShareVisibility.PUBLIC_LINK,
|
|
662
|
+
OR: [
|
|
663
|
+
{
|
|
664
|
+
expiresAt: null
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
expiresAt: {
|
|
668
|
+
gt: /* @__PURE__ */ new Date()
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
]
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
return Array.from(new Set(grants.map((g) => g.resourceId)));
|
|
675
|
+
}
|
|
676
|
+
/** Increment access analytics (best-effort). */
|
|
677
|
+
async touchAccess(grantId) {
|
|
678
|
+
try {
|
|
679
|
+
await this.prisma.update({
|
|
680
|
+
where: {
|
|
681
|
+
id: grantId
|
|
682
|
+
},
|
|
683
|
+
data: {
|
|
684
|
+
lastAccessAt: /* @__PURE__ */ new Date(),
|
|
685
|
+
accessCount: {
|
|
686
|
+
increment: 1
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
} catch (err) {
|
|
691
|
+
logger.debug("[ShareService] touchAccess failed", {
|
|
692
|
+
grantId,
|
|
693
|
+
err
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// -------------------------------------------------------------------------
|
|
698
|
+
// Internals
|
|
699
|
+
// -------------------------------------------------------------------------
|
|
700
|
+
async findActiveByToken(token) {
|
|
701
|
+
if (!token) return null;
|
|
702
|
+
const grant = await this.prisma.findFirst({
|
|
703
|
+
where: {
|
|
704
|
+
token,
|
|
705
|
+
deletedAt: null
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
if (!grant) return null;
|
|
709
|
+
if (grant.status === ShareStatus.REVOKED) return null;
|
|
710
|
+
if (grant.expiresAt && grant.expiresAt.getTime() <= Date.now()) return null;
|
|
711
|
+
return grant;
|
|
712
|
+
}
|
|
713
|
+
async fireShareEmail(actor, grant, resourceName) {
|
|
714
|
+
if (!grant.token) return;
|
|
715
|
+
try {
|
|
716
|
+
await this.deps.sendShareEmail({
|
|
717
|
+
recipientEmail: grant.granteeEmail,
|
|
718
|
+
recipientUserId: grant.granteeUserId,
|
|
719
|
+
resourceType: this.resourceType,
|
|
720
|
+
resourceName,
|
|
721
|
+
role: grant.role,
|
|
722
|
+
acceptUrl: this.deps.buildAcceptUrl(grant.token),
|
|
723
|
+
message: grant.message,
|
|
724
|
+
sharerName: actor.name ?? null,
|
|
725
|
+
idempotencyKey: grant.id
|
|
726
|
+
});
|
|
727
|
+
} catch (err) {
|
|
728
|
+
logger.warn("[ShareService] share email failed; grant still created", {
|
|
729
|
+
grantId: grant.id,
|
|
730
|
+
resourceType: this.resourceType,
|
|
731
|
+
err
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// src/sharing/email.ts
|
|
738
|
+
var SHARE_EMAIL_TEMPLATE_SLUG = "resource-shared";
|
|
739
|
+
var RESOURCE_TYPE_LABELS = {
|
|
740
|
+
session: "terminal session",
|
|
741
|
+
vibe: "Vibe",
|
|
742
|
+
vibedeck: "VibeDeck",
|
|
743
|
+
note: "note",
|
|
744
|
+
aiprompt: "AI prompt",
|
|
745
|
+
aicontext: "AI context",
|
|
746
|
+
instructionfile: "instruction file",
|
|
747
|
+
tunnel: "tunnel",
|
|
748
|
+
action: "action",
|
|
749
|
+
file: "file",
|
|
750
|
+
folder: "folder",
|
|
751
|
+
conversation: "conversation",
|
|
752
|
+
catalog: "catalog",
|
|
753
|
+
component: "component",
|
|
754
|
+
template: "template",
|
|
755
|
+
docs: "docs site"
|
|
756
|
+
};
|
|
757
|
+
function resourceTypeLabel(resourceType) {
|
|
758
|
+
return RESOURCE_TYPE_LABELS[resourceType] ?? resourceType;
|
|
759
|
+
}
|
|
760
|
+
__name(resourceTypeLabel, "resourceTypeLabel");
|
|
761
|
+
var ROLE_LABELS = {
|
|
762
|
+
[ShareRole.VIEWER]: "Viewer",
|
|
763
|
+
[ShareRole.COMMENTER]: "Commenter",
|
|
764
|
+
[ShareRole.EDITOR]: "Editor",
|
|
765
|
+
[ShareRole.MANAGER]: "Manager"
|
|
766
|
+
};
|
|
767
|
+
function roleLabel(role) {
|
|
768
|
+
return ROLE_LABELS[role];
|
|
769
|
+
}
|
|
770
|
+
__name(roleLabel, "roleLabel");
|
|
771
|
+
function buildShareEmailVariables(args) {
|
|
772
|
+
return {
|
|
773
|
+
sharerName: args.sharerName ?? "A teammate",
|
|
774
|
+
resourceType: resourceTypeLabel(args.resourceType),
|
|
775
|
+
resourceName: args.resourceName,
|
|
776
|
+
roleLabel: roleLabel(args.role),
|
|
777
|
+
acceptUrl: args.acceptUrl,
|
|
778
|
+
message: args.message ?? ""
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
__name(buildShareEmailVariables, "buildShareEmailVariables");
|
|
782
|
+
function makeSendShareEmail(notifier) {
|
|
783
|
+
return async (args) => {
|
|
784
|
+
const recipient = {};
|
|
785
|
+
if (args.recipientEmail) recipient.email = args.recipientEmail;
|
|
786
|
+
if (args.recipientUserId) recipient.userId = args.recipientUserId;
|
|
787
|
+
if (!recipient.email && !recipient.userId) {
|
|
788
|
+
logger.warn("[ShareEmail] no addressable recipient; skipping", {
|
|
789
|
+
idempotencyKey: args.idempotencyKey
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
logger.info("[ShareEmail] Sending resource-shared", {
|
|
794
|
+
templateSlug: SHARE_EMAIL_TEMPLATE_SLUG,
|
|
795
|
+
resourceType: args.resourceType,
|
|
796
|
+
idempotencyKey: args.idempotencyKey
|
|
797
|
+
});
|
|
798
|
+
await notifier.sendWithTemplate({
|
|
799
|
+
templateSlug: SHARE_EMAIL_TEMPLATE_SLUG,
|
|
800
|
+
variables: buildShareEmailVariables(args),
|
|
801
|
+
recipient
|
|
802
|
+
});
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
__name(makeSendShareEmail, "makeSendShareEmail");
|
|
806
|
+
|
|
807
|
+
// src/sharing/sdl.ts
|
|
808
|
+
var SHARE_GRANT_SDL = (
|
|
809
|
+
/* GraphQL */
|
|
810
|
+
`
|
|
811
|
+
enum ShareRole {
|
|
812
|
+
VIEWER
|
|
813
|
+
COMMENTER
|
|
814
|
+
EDITOR
|
|
815
|
+
MANAGER
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
enum ShareGrantVisibility {
|
|
819
|
+
PRIVATE
|
|
820
|
+
SHARED_WITH_USERS
|
|
821
|
+
SHARED_WITH_VIBE
|
|
822
|
+
PUBLIC_LINK
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
enum ShareGrantStatus {
|
|
826
|
+
PENDING
|
|
827
|
+
ACCEPTED
|
|
828
|
+
REVOKED
|
|
829
|
+
EXPIRED
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
type ShareCapabilities {
|
|
833
|
+
canView: Boolean!
|
|
834
|
+
canComment: Boolean!
|
|
835
|
+
canEdit: Boolean!
|
|
836
|
+
canManage: Boolean!
|
|
837
|
+
canExecute: Boolean!
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
type ShareGrant @key(fields: "id") {
|
|
841
|
+
id: ID!
|
|
842
|
+
resourceType: String!
|
|
843
|
+
resourceId: ID!
|
|
844
|
+
granteeUserId: ID
|
|
845
|
+
granteeEmail: String
|
|
846
|
+
role: ShareRole!
|
|
847
|
+
visibility: ShareGrantVisibility!
|
|
848
|
+
status: ShareGrantStatus!
|
|
849
|
+
expiresAt: DateTime
|
|
850
|
+
message: String
|
|
851
|
+
createdBy: ID!
|
|
852
|
+
acceptedAt: DateTime
|
|
853
|
+
createdAt: DateTime!
|
|
854
|
+
"Public accept/link URL (present for invites and public links)."
|
|
855
|
+
shareUrl: String
|
|
856
|
+
"Capabilities implied by this grant's role."
|
|
857
|
+
capabilities: ShareCapabilities!
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
type ShareGrantList {
|
|
861
|
+
items: [ShareGrant!]!
|
|
862
|
+
total: Int!
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
"Safe, pre-login preview of a share invitation."
|
|
866
|
+
type ShareGrantPreview {
|
|
867
|
+
id: ID!
|
|
868
|
+
resourceType: String!
|
|
869
|
+
resourceName: String
|
|
870
|
+
role: ShareRole!
|
|
871
|
+
visibility: ShareGrantVisibility!
|
|
872
|
+
status: ShareGrantStatus!
|
|
873
|
+
sharerName: String
|
|
874
|
+
expiresAt: DateTime
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
input ShareEntityInput {
|
|
878
|
+
resourceId: ID!
|
|
879
|
+
email: String!
|
|
880
|
+
role: ShareRole!
|
|
881
|
+
message: String
|
|
882
|
+
expiresAt: DateTime
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
input UpdateShareRoleInput {
|
|
886
|
+
resourceId: ID!
|
|
887
|
+
shareId: ID!
|
|
888
|
+
role: ShareRole!
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
input RevokeShareInput {
|
|
892
|
+
resourceId: ID!
|
|
893
|
+
shareId: ID!
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
input CreateShareLinkInput {
|
|
897
|
+
resourceId: ID!
|
|
898
|
+
role: ShareRole!
|
|
899
|
+
expiresAt: DateTime
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
extend type Query {
|
|
903
|
+
"Pre-login preview of a share invite by token."
|
|
904
|
+
shareGrantByToken(token: String!): ShareGrantPreview @rbac(public: true)
|
|
905
|
+
"Grants where the current user is the accepted grantee."
|
|
906
|
+
sharesSharedWithMe(resourceType: String, pagination: PaginationInput): ShareGrantList! @rbac(scopeType: "self")
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
extend type Mutation {
|
|
910
|
+
"Accept a share invitation (recipient identified by the opaque token + email/id)."
|
|
911
|
+
acceptShare(token: String!): ShareGrant! @rbac(public: true)
|
|
912
|
+
"Decline a share invitation."
|
|
913
|
+
declineShare(token: String!): Boolean! @rbac(public: true)
|
|
914
|
+
}
|
|
915
|
+
`
|
|
916
|
+
);
|
|
917
|
+
function buildPerEntityShareSdl(entity, rbacResource) {
|
|
918
|
+
return (
|
|
919
|
+
/* GraphQL */
|
|
920
|
+
`
|
|
921
|
+
extend type Mutation {
|
|
922
|
+
share${entity}(input: ShareEntityInput!): ShareGrant!
|
|
923
|
+
@rbac(action: "share", resource: "${rbacResource}", resourceIdArg: "input.resourceId", scopeType: "wspace")
|
|
924
|
+
revoke${entity}Share(input: RevokeShareInput!): ShareGrant!
|
|
925
|
+
@rbac(action: "share", resource: "${rbacResource}", resourceIdArg: "input.resourceId", scopeType: "wspace")
|
|
926
|
+
update${entity}ShareRole(input: UpdateShareRoleInput!): ShareGrant!
|
|
927
|
+
@rbac(action: "share", resource: "${rbacResource}", resourceIdArg: "input.resourceId", scopeType: "wspace")
|
|
928
|
+
create${entity}ShareLink(input: CreateShareLinkInput!): ShareGrant!
|
|
929
|
+
@rbac(action: "share", resource: "${rbacResource}", resourceIdArg: "input.resourceId", scopeType: "wspace")
|
|
930
|
+
revoke${entity}ShareLink(input: RevokeShareInput!): ShareGrant!
|
|
931
|
+
@rbac(action: "share", resource: "${rbacResource}", resourceIdArg: "input.resourceId", scopeType: "wspace")
|
|
932
|
+
}
|
|
933
|
+
`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
__name(buildPerEntityShareSdl, "buildPerEntityShareSdl");
|
|
937
|
+
|
|
938
|
+
// src/sharing/prisma-fragment.ts
|
|
939
|
+
var SHARE_GRANT_PRISMA = `
|
|
940
|
+
enum ShareRole {
|
|
941
|
+
VIEWER
|
|
942
|
+
COMMENTER
|
|
943
|
+
EDITOR
|
|
944
|
+
MANAGER
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
enum ShareGrantVisibility {
|
|
948
|
+
PRIVATE
|
|
949
|
+
SHARED_WITH_USERS
|
|
950
|
+
SHARED_WITH_VIBE
|
|
951
|
+
PUBLIC_LINK
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
enum ShareGrantStatus {
|
|
955
|
+
PENDING
|
|
956
|
+
ACCEPTED
|
|
957
|
+
REVOKED
|
|
958
|
+
EXPIRED
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/// Generic resource share grant. \`resourceType\` discriminates the entity; one
|
|
962
|
+
/// row per (resource, recipient) or per public link. See @burdenoff/be-sdk/sharing.
|
|
963
|
+
model ShareGrant {
|
|
964
|
+
id String @id @default(uuid())
|
|
965
|
+
workspaceId String @map("workspace_id") @db.VarChar(255)
|
|
966
|
+
resourceType String @map("resource_type") @db.VarChar(64)
|
|
967
|
+
resourceId String @map("resource_id") @db.VarChar(255)
|
|
968
|
+
granteeUserId String? @map("grantee_user_id") @db.VarChar(255)
|
|
969
|
+
granteeEmail String? @map("grantee_email") @db.VarChar(320)
|
|
970
|
+
role ShareRole @default(VIEWER)
|
|
971
|
+
visibility ShareGrantVisibility @default(SHARED_WITH_USERS)
|
|
972
|
+
status ShareGrantStatus @default(PENDING)
|
|
973
|
+
token String? @unique @db.VarChar(64)
|
|
974
|
+
expiresAt DateTime? @map("expires_at")
|
|
975
|
+
message String? @db.Text
|
|
976
|
+
createdBy String @map("created_by") @db.VarChar(255)
|
|
977
|
+
onBehalfOf String @map("on_behalf_of") @db.VarChar(255)
|
|
978
|
+
acceptedAt DateTime? @map("accepted_at")
|
|
979
|
+
acceptedBy String? @map("accepted_by") @db.VarChar(255)
|
|
980
|
+
revokedAt DateTime? @map("revoked_at")
|
|
981
|
+
revokedBy String? @map("revoked_by") @db.VarChar(255)
|
|
982
|
+
lastAccessAt DateTime? @map("last_access_at")
|
|
983
|
+
accessCount Int @default(0) @map("access_count")
|
|
984
|
+
context Json?
|
|
985
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
986
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
987
|
+
deletedAt DateTime? @map("deleted_at")
|
|
988
|
+
|
|
989
|
+
@@index([workspaceId])
|
|
990
|
+
@@index([resourceType, resourceId])
|
|
991
|
+
@@index([granteeUserId])
|
|
992
|
+
@@index([granteeEmail])
|
|
993
|
+
@@index([status])
|
|
994
|
+
@@index([expiresAt])
|
|
995
|
+
@@index([resourceType, resourceId, status])
|
|
996
|
+
@@map("share_grants")
|
|
997
|
+
}
|
|
998
|
+
`;
|
|
999
|
+
var SHARE_GRANT_PARTIAL_UNIQUE_SQL = `
|
|
1000
|
+
CREATE UNIQUE INDEX IF NOT EXISTS share_grants_active_user_uq
|
|
1001
|
+
ON share_grants (resource_type, resource_id, grantee_user_id)
|
|
1002
|
+
WHERE status IN ('PENDING', 'ACCEPTED') AND deleted_at IS NULL AND grantee_user_id IS NOT NULL;
|
|
1003
|
+
`;
|
|
1004
|
+
|
|
1005
|
+
export { RESOURCE_CAPABILITY_MAP, RESOURCE_TYPE_LABELS, ROLE_CAPABILITIES, ROLE_ORDER, SHARE_EMAIL_TEMPLATE_SLUG, SHARE_GRANT_PARTIAL_UNIQUE_SQL, SHARE_GRANT_PRISMA, SHARE_GRANT_SDL, ShareRole, ShareService, ShareStatus, ShareVisibility, buildPerEntityShareSdl, buildShareEmailVariables, capabilitiesFor, legacyToRole, makeSendShareEmail, maxRole, normalizeStatus, resourceTypeLabel, roleAtLeast, roleLabel, roleToLegacy, sharing_exports };
|
|
1006
|
+
//# sourceMappingURL=chunk-DVAZQEOL.js.map
|
|
1007
|
+
//# sourceMappingURL=chunk-DVAZQEOL.js.map
|