@studious-lms/server 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/middleware/security.d.ts.map +1 -1
  2. package/dist/middleware/security.js +3 -3
  3. package/dist/middleware/security.js.map +1 -1
  4. package/dist/models/agenda.d.ts +2 -2
  5. package/dist/models/announcement.d.ts +2 -2
  6. package/dist/models/assignment.d.ts +33 -33
  7. package/dist/models/class.d.ts +2 -2
  8. package/dist/models/cloudStorage.d.ts +14 -1
  9. package/dist/models/cloudStorage.d.ts.map +1 -1
  10. package/dist/models/cloudStorage.js +123 -3
  11. package/dist/models/cloudStorage.js.map +1 -1
  12. package/dist/models/event.d.ts +3 -3
  13. package/dist/models/file.d.ts +27 -26
  14. package/dist/models/file.d.ts.map +1 -1
  15. package/dist/models/file.js +4 -39
  16. package/dist/models/file.js.map +1 -1
  17. package/dist/models/labChat.d.ts +6 -6
  18. package/dist/models/section.d.ts +1 -1
  19. package/dist/routers/_app.d.ts +354 -100
  20. package/dist/routers/_app.d.ts.map +1 -1
  21. package/dist/routers/_app.js +4 -2
  22. package/dist/routers/_app.js.map +1 -1
  23. package/dist/routers/agenda.d.ts +2 -2
  24. package/dist/routers/assignment.d.ts +31 -31
  25. package/dist/routers/class.d.ts +5 -5
  26. package/dist/routers/event.d.ts +2 -2
  27. package/dist/routers/file.d.ts +2 -2
  28. package/dist/routers/folder.d.ts +24 -0
  29. package/dist/routers/folder.d.ts.map +1 -1
  30. package/dist/routers/folder.js +27 -5
  31. package/dist/routers/folder.js.map +1 -1
  32. package/dist/routers/labChat.d.ts +2 -2
  33. package/dist/routers/newtonChat.d.ts +1 -1
  34. package/dist/routers/permissions.d.ts +120 -0
  35. package/dist/routers/permissions.d.ts.map +1 -0
  36. package/dist/routers/permissions.js +50 -0
  37. package/dist/routers/permissions.js.map +1 -0
  38. package/dist/routers/tag.d.ts +5 -5
  39. package/dist/services/agenda.d.ts +2 -2
  40. package/dist/services/assignment.d.ts +31 -31
  41. package/dist/services/class.d.ts +5 -5
  42. package/dist/services/class.js +3 -3
  43. package/dist/services/class.js.map +1 -1
  44. package/dist/services/event.d.ts +2 -2
  45. package/dist/services/file.d.ts +22 -5
  46. package/dist/services/file.d.ts.map +1 -1
  47. package/dist/services/file.js +81 -45
  48. package/dist/services/file.js.map +1 -1
  49. package/dist/services/folder.d.ts +97 -2
  50. package/dist/services/folder.d.ts.map +1 -1
  51. package/dist/services/folder.js +158 -87
  52. package/dist/services/folder.js.map +1 -1
  53. package/dist/services/labChat.d.ts +2 -2
  54. package/dist/services/permissions.d.ts +188 -0
  55. package/dist/services/permissions.d.ts.map +1 -0
  56. package/dist/services/permissions.js +661 -0
  57. package/dist/services/permissions.js.map +1 -0
  58. package/package.json +1 -1
  59. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
  60. package/prisma/schema.prisma +74 -0
  61. package/src/middleware/security.ts +1 -1
  62. package/src/models/cloudStorage.ts +134 -1
  63. package/src/models/file.ts +2 -37
  64. package/src/routers/_app.ts +2 -0
  65. package/src/routers/folder.ts +32 -2
  66. package/src/routers/permissions.ts +71 -0
  67. package/src/services/class.ts +1 -1
  68. package/src/services/file.ts +76 -47
  69. package/src/services/folder.ts +180 -91
  70. package/src/services/permissions.ts +778 -0
  71. package/tests/unit/services/permissions.test.ts +463 -0
@@ -143,8 +143,8 @@ export declare function attachAssignment(userId: string, eventId: string, assign
143
143
  type: import(".prisma/client").$Enums.AssignmentType;
144
144
  id: string;
145
145
  createdAt: Date | null;
146
- modifiedAt: Date | null;
147
146
  classId: string;
147
+ modifiedAt: Date | null;
148
148
  title: string;
149
149
  instructions: string;
150
150
  dueDate: Date;
@@ -185,8 +185,8 @@ export declare function detachAssignment(userId: string, eventId: string, assign
185
185
  type: import(".prisma/client").$Enums.AssignmentType;
186
186
  id: string;
187
187
  createdAt: Date | null;
188
- modifiedAt: Date | null;
189
188
  classId: string;
189
+ modifiedAt: Date | null;
190
190
  title: string;
191
191
  instructions: string;
192
192
  dueDate: Date;
@@ -1,4 +1,21 @@
1
- /** Generate a signed URL for file download. Validates user has access. */
1
+ /**
2
+ * Generate a signed URL for file download.
3
+ *
4
+ * Access resolution, in order — first match wins:
5
+ * 1. Owner of the file.
6
+ * 2. Member of the conversation the file is attached to (chat upload).
7
+ * 3. Contextual access via the file's class context: assignment / announcement
8
+ * / submission / annotations attached to a class the user teaches, learns
9
+ * in, or is a school admin for.
10
+ * 4. The central permissions resolver, which covers folder-scoped files —
11
+ * explicit Permission grants (USER / TAG / CLASS / DEPARTMENT / SCHOOL
12
+ * principals) plus scope defaults (school members read SCHOOL_STORAGE,
13
+ * class members read class folders, owners read personal folders).
14
+ *
15
+ * The resolver is invoked unconditionally as the last step, so files shared
16
+ * via the new Permission system reach this code path even when they have no
17
+ * matching domain context.
18
+ */
2
19
  export declare function getFileSignedUrl(userId: string, fileId: string): Promise<{
3
20
  url: string;
4
21
  }>;
@@ -13,6 +30,7 @@ export declare function moveFile(fileId: string, targetFolderId: string): Promis
13
30
  name: string;
14
31
  id: string;
15
32
  userId: string | null;
33
+ folderId: string | null;
16
34
  size: number | null;
17
35
  uploadedAt: Date | null;
18
36
  uploadStatus: import(".prisma/client").$Enums.UploadStatus;
@@ -29,7 +47,6 @@ export declare function moveFile(fileId: string, targetFolderId: string): Promis
29
47
  submissionId: string | null;
30
48
  annotationId: string | null;
31
49
  classDraftId: string | null;
32
- folderId: string | null;
33
50
  conversationId: string | null;
34
51
  messageId: string | null;
35
52
  announcementId: string | null;
@@ -46,6 +63,7 @@ export declare function renameFile(fileId: string, newName: string): Promise<{
46
63
  name: string;
47
64
  id: string;
48
65
  userId: string | null;
66
+ folderId: string | null;
49
67
  size: number | null;
50
68
  uploadedAt: Date | null;
51
69
  uploadStatus: import(".prisma/client").$Enums.UploadStatus;
@@ -62,7 +80,6 @@ export declare function renameFile(fileId: string, newName: string): Promise<{
62
80
  submissionId: string | null;
63
81
  annotationId: string | null;
64
82
  classDraftId: string | null;
65
- folderId: string | null;
66
83
  conversationId: string | null;
67
84
  messageId: string | null;
68
85
  announcementId: string | null;
@@ -89,6 +106,7 @@ export declare function moveFileScoped(userId: string, input: {
89
106
  name: string;
90
107
  id: string;
91
108
  userId: string | null;
109
+ folderId: string | null;
92
110
  size: number | null;
93
111
  uploadedAt: Date | null;
94
112
  uploadStatus: import(".prisma/client").$Enums.UploadStatus;
@@ -105,7 +123,6 @@ export declare function moveFileScoped(userId: string, input: {
105
123
  submissionId: string | null;
106
124
  annotationId: string | null;
107
125
  classDraftId: string | null;
108
- folderId: string | null;
109
126
  conversationId: string | null;
110
127
  messageId: string | null;
111
128
  announcementId: string | null;
@@ -126,6 +143,7 @@ export declare function renameFileScoped(userId: string, input: {
126
143
  name: string;
127
144
  id: string;
128
145
  userId: string | null;
146
+ folderId: string | null;
129
147
  size: number | null;
130
148
  uploadedAt: Date | null;
131
149
  uploadStatus: import(".prisma/client").$Enums.UploadStatus;
@@ -142,7 +160,6 @@ export declare function renameFileScoped(userId: string, input: {
142
160
  submissionId: string | null;
143
161
  annotationId: string | null;
144
162
  classDraftId: string | null;
145
- folderId: string | null;
146
163
  conversationId: string | null;
147
164
  messageId: string | null;
148
165
  announcementId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"/","sources":["services/file.ts"],"names":[],"mappings":"AAmCA,0EAA0E;AAC1E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GAkFpE;AAED,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmBvB;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBhB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;GA6BrE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsBlD;AAED,0BAA0B;AAC1B,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsB3C;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GA4BrE"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"/","sources":["services/file.ts"],"names":[],"mappings":"AA+CA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GAkFpE;AAED,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmBvB;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBhB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;GA6BrE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsBlD;AAED,0BAA0B;AAC1B,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsB3C;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GA4BrE"}
@@ -1,14 +1,26 @@
1
1
  /**
2
- * File service – signed URLs, move, rename, delete. Handles permission checks
3
- * for assignment, submission, announcement, folder, and user-owned files.
2
+ * File service – signed URLs, move, rename, delete.
4
3
  *
5
- * The class files page calls the legacy class-scoped helpers; the unified
6
- * cloud-storage page calls the `*Scoped` / `*Generic` helpers below, which
7
- * derive permissions from the parent folder's CloudScope so the same endpoint
8
- * can serve class, school, and personal storage.
4
+ * Two access concepts coexist here, and they should not be conflated:
5
+ *
6
+ * 1. **Contextual access** — assignment, submission, announcement, and chat
7
+ * attachments. A teacher viewing a student's submission has access by
8
+ * virtue of teaching the class, not because anyone explicitly "shared"
9
+ * the file with them. These checks live inline in `getFileSignedUrl`
10
+ * below and predate the explicit-share system.
11
+ *
12
+ * 2. **Explicit-share access** — folder-scoped files in cloud storage.
13
+ * Permission flows through the central resolver in `permissions.ts`,
14
+ * which consults explicit `Permission` rows in addition to the
15
+ * scope-derived defaults. The cloud-storage helpers (`*Scoped` /
16
+ * `*Generic`) below all use this path.
17
+ *
18
+ * If you're tempted to refactor the contextual checks into the resolver,
19
+ * don't — they intentionally key off the file's domain context, not its
20
+ * filesystem location.
9
21
  */
10
22
 
11
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="461807a5-9bef-532d-8edf-7763337de98b")}catch(e){}}();
23
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f5c84cd2-4bba-5d8c-80c8-a64d756df4c6")}catch(e){}}();
12
24
  import { TRPCError } from "@trpc/server";
13
25
  import { getSignedUrl, deleteFile } from "../lib/googleCloudStorage.js";
14
26
  import { logger } from "../utils/logger.js";
@@ -16,11 +28,28 @@ import { findFileById, findFileWithFolder, findFileWithFolderAndThumbnail, findF
16
28
  import { findConversationMembership } from "../models/message.js";
17
29
  import { resolveFolderScope } from "../models/cloudStorage.js";
18
30
  import { requireFolderAccess } from "./folder.js";
31
+ import { requireFileAccess } from "./permissions.js";
19
32
  function isSchoolAdminForClass(classData, userId) {
20
- return Boolean(classData?.school?.memberships?.some((membership) => membership.userId === userId) ||
21
- classData?.school?.users?.some((admin) => admin.id === userId));
33
+ return Boolean(classData?.school?.memberships?.some((membership) => membership.userId === userId));
22
34
  }
23
- /** Generate a signed URL for file download. Validates user has access. */
35
+ /**
36
+ * Generate a signed URL for file download.
37
+ *
38
+ * Access resolution, in order — first match wins:
39
+ * 1. Owner of the file.
40
+ * 2. Member of the conversation the file is attached to (chat upload).
41
+ * 3. Contextual access via the file's class context: assignment / announcement
42
+ * / submission / annotations attached to a class the user teaches, learns
43
+ * in, or is a school admin for.
44
+ * 4. The central permissions resolver, which covers folder-scoped files —
45
+ * explicit Permission grants (USER / TAG / CLASS / DEPARTMENT / SCHOOL
46
+ * principals) plus scope defaults (school members read SCHOOL_STORAGE,
47
+ * class members read class folders, owners read personal folders).
48
+ *
49
+ * The resolver is invoked unconditionally as the last step, so files shared
50
+ * via the new Permission system reach this code path even when they have no
51
+ * matching domain context.
52
+ */
24
53
  export async function getFileSignedUrl(userId, fileId) {
25
54
  const file = await findFileById(fileId);
26
55
  if (!file) {
@@ -30,46 +59,53 @@ export async function getFileSignedUrl(userId, fileId) {
30
59
  });
31
60
  }
32
61
  let hasAccess = false;
33
- if (file.assignment?.class) {
34
- const isTeacher = file.assignment.class.teachers.some((t) => t.id === userId);
35
- const isStudent = file.assignment.class.students.some((s) => s.id === userId);
36
- const isSchoolAdmin = isSchoolAdminForClass(file.assignment.class, userId);
37
- logger.info(`Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}, isSchoolAdmin: ${isSchoolAdmin}`);
38
- hasAccess = isTeacher || isStudent || isSchoolAdmin;
39
- }
40
- if (file.announcement?.class) {
41
- const ann = file.announcement;
42
- const isTeacher = ann.class.teachers.some((t) => t.id === userId);
43
- const isStudent = ann.class.students.some((s) => s.id === userId);
44
- const isSchoolAdmin = isSchoolAdminForClass(ann.class, userId);
45
- hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;
46
- }
47
- if (file.submission?.assignment?.classId) {
62
+ if (file.userId === userId) {
63
+ hasAccess = true;
64
+ }
65
+ if (!hasAccess && file.conversationId) {
66
+ const membership = await findConversationMembership(file.conversationId, userId);
67
+ hasAccess = Boolean(membership);
68
+ }
69
+ if (!hasAccess && file.assignment?.class) {
70
+ const cls = file.assignment.class;
48
71
  hasAccess =
49
- hasAccess ||
50
- file.submission.studentId === userId ||
51
- file.submission.assignment.class.teachers.some((t) => t.id === userId) ||
52
- isSchoolAdminForClass(file.submission.assignment.class, userId);
72
+ cls.teachers.some((t) => t.id === userId) ||
73
+ cls.students.some((s) => s.id === userId) ||
74
+ isSchoolAdminForClass(cls, userId);
53
75
  }
54
- if (file.annotations?.assignment?.classId) {
76
+ if (!hasAccess && file.announcement?.class) {
77
+ const cls = file.announcement.class;
55
78
  hasAccess =
56
- hasAccess ||
57
- file.annotations.studentId === userId ||
58
- file.annotations.assignment.class.teachers.some((t) => t.id === userId) ||
59
- isSchoolAdminForClass(file.annotations.assignment.class, userId);
79
+ cls.teachers.some((t) => t.id === userId) ||
80
+ cls.students.some((s) => s.id === userId) ||
81
+ isSchoolAdminForClass(cls, userId);
60
82
  }
61
- if (file.userId === userId) {
62
- hasAccess = true;
83
+ if (!hasAccess && file.submission?.assignment?.classId) {
84
+ const cls = file.submission.assignment.class;
85
+ hasAccess =
86
+ file.submission.studentId === userId ||
87
+ cls.teachers.some((t) => t.id === userId) ||
88
+ isSchoolAdminForClass(cls, userId);
63
89
  }
64
- if (file.conversationId) {
65
- const membership = await findConversationMembership(file.conversationId, userId);
66
- hasAccess = hasAccess || Boolean(membership);
90
+ if (!hasAccess && file.annotations?.assignment?.classId) {
91
+ const cls = file.annotations.assignment.class;
92
+ hasAccess =
93
+ file.annotations.studentId === userId ||
94
+ cls.teachers.some((t) => t.id === userId) ||
95
+ isSchoolAdminForClass(cls, userId);
67
96
  }
68
- if (file.folder?.class) {
69
- const isTeacher = file.folder.class.teachers.some((t) => t.id === userId);
70
- const isStudent = file.folder.class.students.some((s) => s.id === userId);
71
- const isSchoolAdmin = isSchoolAdminForClass(file.folder.class, userId);
72
- hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;
97
+ // Final fallback: the explicit-share + scope-default resolver. Covers
98
+ // folder-scoped files shared via Permission grants (incl. SCHOOL/CLASS/TAG
99
+ // principals) and any institution member reading SCHOOL_STORAGE files.
100
+ // Safe to call for unfiled files — the resolver returns owner-only there.
101
+ if (!hasAccess) {
102
+ try {
103
+ await requireFileAccess(userId, fileId, "READ");
104
+ hasAccess = true;
105
+ }
106
+ catch {
107
+ // resolver denied — fall through to FORBIDDEN below.
108
+ }
73
109
  }
74
110
  if (!hasAccess) {
75
111
  throw new TRPCError({
@@ -234,4 +270,4 @@ export async function deleteFileGeneric(userId, fileId) {
234
270
  return { success: true };
235
271
  }
236
272
  //# sourceMappingURL=file.js.map
237
- //# debugId=461807a5-9bef-532d-8edf-7763337de98b
273
+ //# debugId=f5c84cd2-4bba-5d8c-80c8-a64d756df4c6
@@ -1 +1 @@
1
- {"version":3,"file":"file.js","sources":["services/file.ts"],"sourceRoot":"/","sourcesContent":["/**\n * File service – signed URLs, move, rename, delete. Handles permission checks\n * for assignment, submission, announcement, folder, and user-owned files.\n *\n * The class files page calls the legacy class-scoped helpers; the unified\n * cloud-storage page calls the `*Scoped` / `*Generic` helpers below, which\n * derive permissions from the parent folder's CloudScope so the same endpoint\n * can serve class, school, and personal storage.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { getSignedUrl, deleteFile } from \"../lib/googleCloudStorage.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findFileById,\n findFileWithFolder,\n findFileWithFolderAndThumbnail,\n findFolderById,\n updateFileFolder,\n updateFileName,\n deleteFileById,\n} from \"../models/file.js\";\nimport { findConversationMembership } from \"../models/message.js\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { resolveFolderScope } from \"../models/cloudStorage.js\";\nimport { requireFolderAccess } from \"./folder.js\";\n\nfunction isSchoolAdminForClass(classData: any, userId: string) {\n return Boolean(\n classData?.school?.memberships?.some(\n (membership: { userId: string }) => membership.userId === userId\n ) ||\n classData?.school?.users?.some((admin: { id: string }) => admin.id === userId)\n );\n}\n\n/** Generate a signed URL for file download. Validates user has access. */\nexport async function getFileSignedUrl(userId: string, fileId: string) {\n const file = await findFileById(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File does not exist\",\n });\n }\n\n let hasAccess = false;\n\n if (file.assignment?.class) {\n const isTeacher = file.assignment.class.teachers.some(\n (t) => t.id === userId\n );\n const isStudent = file.assignment.class.students.some(\n (s) => s.id === userId\n );\n const isSchoolAdmin = isSchoolAdminForClass(file.assignment.class, userId);\n logger.info(\n `Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}, isSchoolAdmin: ${isSchoolAdmin}`\n );\n hasAccess = isTeacher || isStudent || isSchoolAdmin;\n }\n\n if ((file as any).announcement?.class) {\n const ann = (file as any).announcement;\n const isTeacher = ann.class.teachers.some((t: any) => t.id === userId);\n const isStudent = ann.class.students.some((s: any) => s.id === userId);\n const isSchoolAdmin = isSchoolAdminForClass(ann.class, userId);\n hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;\n }\n\n if (file.submission?.assignment?.classId) {\n hasAccess =\n hasAccess ||\n file.submission.studentId === userId ||\n file.submission.assignment.class.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(file.submission.assignment.class, userId);\n }\n\n if (file.annotations?.assignment?.classId) {\n hasAccess =\n hasAccess ||\n file.annotations.studentId === userId ||\n file.annotations.assignment.class.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(file.annotations.assignment.class, userId);\n }\n\n if (file.userId === userId) {\n hasAccess = true;\n }\n\n if (file.conversationId) {\n const membership = await findConversationMembership(file.conversationId, userId);\n hasAccess = hasAccess || Boolean(membership);\n }\n\n if (file.folder?.class) {\n const isTeacher = file.folder.class.teachers.some((t) => t.id === userId);\n const isStudent = file.folder.class.students.some((s) => s.id === userId);\n const isSchoolAdmin = isSchoolAdminForClass(file.folder.class, userId);\n hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;\n }\n\n if (!hasAccess) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this file\",\n });\n }\n\n try {\n const signedUrl = await getSignedUrl(file.path);\n return { url: signedUrl };\n } catch (error) {\n logger.error(\"Error generating signed URL:\", error as Record<string, unknown>);\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to generate download URL\",\n });\n }\n}\n\nexport async function moveFile(\n fileId: string,\n targetFolderId: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n const targetFolder = await findFolderById(targetFolderId);\n if (!targetFolder) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Target folder not found\",\n });\n }\n\n return updateFileFolder(fileId, targetFolderId);\n}\n\nexport async function renameFile(\n fileId: string,\n newName: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (!newName.trim()) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n\n return updateFileName(fileId, newName.trim());\n}\n\nexport async function deleteFileRecord(fileId: string, classId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (file.folder?.classId !== classId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"File does not belong to this class\",\n });\n }\n\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, unknown>);\n }\n\n await deleteFileById(fileId);\n return { success: true };\n}\n\n/**\n * Scope-aware move: works for class, school, and personal storage. Source\n * and target must live in the same scope (no cross-scope moves).\n */\nexport async function moveFileScoped(\n userId: string,\n input: { fileId: string; targetFolderId: string }\n) {\n const file = await findFileWithFolder(input.fileId);\n if (!file?.folderId) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n const [sourceScope, targetScope] = await Promise.all([\n resolveFolderScope(file.folderId),\n resolveFolderScope(input.targetFolderId),\n ]);\n if (!sourceScope || !targetScope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (JSON.stringify(sourceScope) !== JSON.stringify(targetScope)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move files between scopes\",\n });\n }\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n await requireFolderAccess(userId, input.targetFolderId, \"WRITE\");\n return updateFileFolder(input.fileId, input.targetFolderId);\n}\n\n/** Scope-aware rename. */\nexport async function renameFileScoped(\n userId: string,\n input: { fileId: string; newName: string }\n) {\n const trimmed = input.newName.trim();\n if (!trimmed) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n const file = await findFileWithFolder(input.fileId);\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n if (file.folderId) {\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n } else if (file.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't own this file\",\n });\n }\n return updateFileName(input.fileId, trimmed);\n}\n\n/**\n * Generic delete that works across class / school / personal storage by\n * deriving permissions from the parent folder's CloudScope (or owning user\n * for unfiled files).\n */\nexport async function deleteFileGeneric(userId: string, fileId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n if (file.folderId) {\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n } else if (file.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't own this file\",\n });\n }\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(\n `Failed to delete file ${file.path}:`,\n error as Record<string, unknown>\n );\n }\n await deleteFileById(fileId);\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"461807a5-9bef-532d-8edf-7763337de98b"}
1
+ {"version":3,"file":"file.js","sources":["services/file.ts"],"sourceRoot":"/","sourcesContent":["/**\n * File service – signed URLs, move, rename, delete.\n *\n * Two access concepts coexist here, and they should not be conflated:\n *\n * 1. **Contextual access** — assignment, submission, announcement, and chat\n * attachments. A teacher viewing a student's submission has access by\n * virtue of teaching the class, not because anyone explicitly \"shared\"\n * the file with them. These checks live inline in `getFileSignedUrl`\n * below and predate the explicit-share system.\n *\n * 2. **Explicit-share access** — folder-scoped files in cloud storage.\n * Permission flows through the central resolver in `permissions.ts`,\n * which consults explicit `Permission` rows in addition to the\n * scope-derived defaults. The cloud-storage helpers (`*Scoped` /\n * `*Generic`) below all use this path.\n *\n * If you're tempted to refactor the contextual checks into the resolver,\n * don't — they intentionally key off the file's domain context, not its\n * filesystem location.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { getSignedUrl, deleteFile } from \"../lib/googleCloudStorage.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findFileById,\n findFileWithFolder,\n findFileWithFolderAndThumbnail,\n findFolderById,\n updateFileFolder,\n updateFileName,\n deleteFileById,\n} from \"../models/file.js\";\nimport { findConversationMembership } from \"../models/message.js\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { resolveFolderScope } from \"../models/cloudStorage.js\";\nimport { requireFolderAccess } from \"./folder.js\";\nimport { requireFileAccess } from \"./permissions.js\";\n\nfunction isSchoolAdminForClass(classData: any, userId: string) {\n return Boolean(\n classData?.school?.memberships?.some(\n (membership: { userId: string }) => membership.userId === userId\n )\n );\n}\n\n/**\n * Generate a signed URL for file download.\n *\n * Access resolution, in order — first match wins:\n * 1. Owner of the file.\n * 2. Member of the conversation the file is attached to (chat upload).\n * 3. Contextual access via the file's class context: assignment / announcement\n * / submission / annotations attached to a class the user teaches, learns\n * in, or is a school admin for.\n * 4. The central permissions resolver, which covers folder-scoped files —\n * explicit Permission grants (USER / TAG / CLASS / DEPARTMENT / SCHOOL\n * principals) plus scope defaults (school members read SCHOOL_STORAGE,\n * class members read class folders, owners read personal folders).\n *\n * The resolver is invoked unconditionally as the last step, so files shared\n * via the new Permission system reach this code path even when they have no\n * matching domain context.\n */\nexport async function getFileSignedUrl(userId: string, fileId: string) {\n const file = await findFileById(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File does not exist\",\n });\n }\n\n let hasAccess = false;\n\n if (file.userId === userId) {\n hasAccess = true;\n }\n\n if (!hasAccess && file.conversationId) {\n const membership = await findConversationMembership(file.conversationId, userId);\n hasAccess = Boolean(membership);\n }\n\n if (!hasAccess && file.assignment?.class) {\n const cls = file.assignment.class;\n hasAccess =\n cls.teachers.some((t) => t.id === userId) ||\n cls.students.some((s) => s.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n if (!hasAccess && (file as any).announcement?.class) {\n const cls = (file as any).announcement.class;\n hasAccess =\n cls.teachers.some((t: { id: string }) => t.id === userId) ||\n cls.students.some((s: { id: string }) => s.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n if (!hasAccess && file.submission?.assignment?.classId) {\n const cls = file.submission.assignment.class;\n hasAccess =\n file.submission.studentId === userId ||\n cls.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n if (!hasAccess && file.annotations?.assignment?.classId) {\n const cls = file.annotations.assignment.class;\n hasAccess =\n file.annotations.studentId === userId ||\n cls.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n // Final fallback: the explicit-share + scope-default resolver. Covers\n // folder-scoped files shared via Permission grants (incl. SCHOOL/CLASS/TAG\n // principals) and any institution member reading SCHOOL_STORAGE files.\n // Safe to call for unfiled files — the resolver returns owner-only there.\n if (!hasAccess) {\n try {\n await requireFileAccess(userId, fileId, \"READ\");\n hasAccess = true;\n } catch {\n // resolver denied — fall through to FORBIDDEN below.\n }\n }\n\n if (!hasAccess) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this file\",\n });\n }\n\n try {\n const signedUrl = await getSignedUrl(file.path);\n return { url: signedUrl };\n } catch (error) {\n logger.error(\"Error generating signed URL:\", error as Record<string, unknown>);\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to generate download URL\",\n });\n }\n}\n\nexport async function moveFile(\n fileId: string,\n targetFolderId: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n const targetFolder = await findFolderById(targetFolderId);\n if (!targetFolder) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Target folder not found\",\n });\n }\n\n return updateFileFolder(fileId, targetFolderId);\n}\n\nexport async function renameFile(\n fileId: string,\n newName: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (!newName.trim()) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n\n return updateFileName(fileId, newName.trim());\n}\n\nexport async function deleteFileRecord(fileId: string, classId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (file.folder?.classId !== classId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"File does not belong to this class\",\n });\n }\n\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, unknown>);\n }\n\n await deleteFileById(fileId);\n return { success: true };\n}\n\n/**\n * Scope-aware move: works for class, school, and personal storage. Source\n * and target must live in the same scope (no cross-scope moves).\n */\nexport async function moveFileScoped(\n userId: string,\n input: { fileId: string; targetFolderId: string }\n) {\n const file = await findFileWithFolder(input.fileId);\n if (!file?.folderId) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n const [sourceScope, targetScope] = await Promise.all([\n resolveFolderScope(file.folderId),\n resolveFolderScope(input.targetFolderId),\n ]);\n if (!sourceScope || !targetScope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (JSON.stringify(sourceScope) !== JSON.stringify(targetScope)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move files between scopes\",\n });\n }\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n await requireFolderAccess(userId, input.targetFolderId, \"WRITE\");\n return updateFileFolder(input.fileId, input.targetFolderId);\n}\n\n/** Scope-aware rename. */\nexport async function renameFileScoped(\n userId: string,\n input: { fileId: string; newName: string }\n) {\n const trimmed = input.newName.trim();\n if (!trimmed) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n const file = await findFileWithFolder(input.fileId);\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n if (file.folderId) {\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n } else if (file.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't own this file\",\n });\n }\n return updateFileName(input.fileId, trimmed);\n}\n\n/**\n * Generic delete that works across class / school / personal storage by\n * deriving permissions from the parent folder's CloudScope (or owning user\n * for unfiled files).\n */\nexport async function deleteFileGeneric(userId: string, fileId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n if (file.folderId) {\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n } else if (file.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't own this file\",\n });\n }\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(\n `Failed to delete file ${file.path}:`,\n error as Record<string, unknown>\n );\n }\n await deleteFileById(fileId);\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"f5c84cd2-4bba-5d8c-80c8-a64d756df4c6"}
@@ -1,11 +1,31 @@
1
1
  import { CloudScope } from "../models/cloudStorage.js";
2
2
  import { type DirectFileData } from "../lib/fileUpload.js";
3
- type AccessLevel = "READ" | "WRITE";
3
+ import { type AccessLevel } from "./permissions.js";
4
4
  interface AccessResult {
5
5
  scope: CloudScope;
6
6
  canWrite: boolean;
7
+ /**
8
+ * True when the viewer's access comes through an explicit grant rather than
9
+ * scope membership. Used by `browseFolder` to surface a "Shared with me"
10
+ * heading instead of the underlying scope's title — the grantee shouldn't
11
+ * see the folder labelled with the owner's scope.
12
+ */
13
+ viaGrant: boolean;
7
14
  }
8
- export declare function requireFolderAccess(userId: string, folderId: string, level: AccessLevel): Promise<AccessResult>;
15
+ /**
16
+ * Folder-level access check. Delegates to the central resolver which consults
17
+ * explicit grants in addition to the scope default. Returns the scope and a
18
+ * canWrite flag indicating whether the user has WRITE here (used by the UI to
19
+ * gate the "New folder" / "Upload" buttons during a READ-level browse).
20
+ *
21
+ * Pass `mutatesSelf: true` for actions that target the folder itself (rename,
22
+ * delete, move-this-folder). Drive-style: explicit grants don't authorize
23
+ * those — only scope-default WRITE does. Mutations of children should NOT
24
+ * pass this flag.
25
+ */
26
+ export declare function requireFolderAccess(userId: string, folderId: string, level: AccessLevel, opts?: {
27
+ mutatesSelf?: boolean;
28
+ }): Promise<AccessResult>;
9
29
  /** Browse a folder. Pass `folderId` to drill into a specific folder, or a `scope` to land on its root. */
10
30
  export declare function browseFolder(userId: string, input: {
11
31
  folderId: string;
@@ -54,6 +74,76 @@ export declare function browseFolder(userId: string, input: {
54
74
  }[];
55
75
  canWrite: boolean;
56
76
  scope: CloudScope;
77
+ /**
78
+ * When the viewer's access comes through an explicit grant (not scope
79
+ * membership), the UI should label the root as "Shared with me" rather
80
+ * than the underlying scope's title. The grantee may not even be a
81
+ * member of that scope — showing "My Files" or "Class Files" would be
82
+ * misleading. Null when the viewer is a regular scope member.
83
+ */
84
+ displayRoot: {
85
+ kind: "SHARED_WITH_ME";
86
+ name: string;
87
+ } | null;
88
+ }>;
89
+ /**
90
+ * Drive-style "Shared with me" — surfaces every top-most folder/file the user
91
+ * has an explicit grant on (filtering out anything already covered by their
92
+ * scope defaults). Shaped like a folder so the existing folder-browser UI can
93
+ * render it without special cases.
94
+ *
95
+ * The pseudo-folder has a synthetic id (`shared-with-me`) and the foreign-key
96
+ * columns set to null — the UI should never try to mutate it directly. The
97
+ * union shape matches `browseFolder`'s return type so the same client query
98
+ * can render either.
99
+ */
100
+ export declare function browseSharedWithMe(userId: string): Promise<{
101
+ folder: {
102
+ id: string;
103
+ name: string;
104
+ color: null;
105
+ classId: null;
106
+ schoolStorageId: null;
107
+ personalOwnerId: null;
108
+ scope: "GENERIC";
109
+ parentFolderId: null;
110
+ parentFolder: null;
111
+ childFolders: {
112
+ id: string;
113
+ name: string;
114
+ color: string | null;
115
+ _count: {
116
+ files: number;
117
+ childFolders: number;
118
+ };
119
+ }[];
120
+ files: {
121
+ id: string;
122
+ name: string;
123
+ type: string;
124
+ size: number | null;
125
+ path: string;
126
+ uploadedAt: Date | null;
127
+ user: {
128
+ id: string;
129
+ username: string;
130
+ } | null;
131
+ }[];
132
+ };
133
+ breadcrumbs: {
134
+ id: string;
135
+ name: string;
136
+ }[];
137
+ canWrite: boolean;
138
+ scope: {
139
+ kind: "PERSONAL";
140
+ userId: string;
141
+ };
142
+ /** Same `displayRoot` shape as `browseFolder` so the client union is uniform. */
143
+ displayRoot: {
144
+ kind: "SHARED_WITH_ME";
145
+ name: string;
146
+ };
57
147
  }>;
58
148
  export declare function getRootFolder(userId: string, scope: CloudScope): Promise<{
59
149
  files: {
@@ -210,6 +300,11 @@ export declare function listUserScopes(userId: string): Promise<{
210
300
  color: string | null;
211
301
  section: string;
212
302
  }[];
303
+ sharedWithMe: {
304
+ kind: "SHARED_WITH_ME";
305
+ folderCount: number;
306
+ fileCount: number;
307
+ };
213
308
  }>;
214
309
  export {};
215
310
  //# sourceMappingURL=folder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"folder.d.ts","sourceRoot":"/","sources":["services/folder.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,UAAU,EASX,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B,KAAK,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpC,UAAU,YAAY;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAoFD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,yBAOnB;AAcD,0GAA0G;AAC1G,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCpD;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGpE;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAG/D;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;KAG1E;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;aAQtE;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IACL,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;;;;;;;;;GAqBF;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;GAkB3D;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;GAclE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,oBAAoB,EAAE,MAAM,CAAA;CAAE;;GA4C1D;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,cAAc,EAAE,CAAA;CAAE,8DAIrD;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;GAWzE;AAED,gEAAgE;AAChE,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;GAwClD"}
1
+ {"version":3,"file":"folder.d.ts","sourceRoot":"/","sources":["services/folder.ts"],"names":[],"mappings":"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"}