@burdenoff/be-sdk 2026.530.1 → 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.
@@ -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