@burdenoff/be-sdk 2026.530.1 → 2026.601.2

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