@powerhousedao/vetra 6.0.0-dev.23 → 6.0.0-dev.25

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 (29) hide show
  1. package/dist/subgraphs/__tests__/app-module-permissions.test.d.ts +2 -0
  2. package/dist/subgraphs/__tests__/app-module-permissions.test.d.ts.map +1 -0
  3. package/dist/subgraphs/__tests__/app-module-permissions.test.js +436 -0
  4. package/dist/subgraphs/__tests__/permission-utils.test.d.ts +2 -0
  5. package/dist/subgraphs/__tests__/permission-utils.test.d.ts.map +1 -0
  6. package/dist/subgraphs/__tests__/permission-utils.test.js +505 -0
  7. package/dist/subgraphs/__tests__/vetra-read-model-permissions.test.d.ts +2 -0
  8. package/dist/subgraphs/__tests__/vetra-read-model-permissions.test.d.ts.map +1 -0
  9. package/dist/subgraphs/__tests__/vetra-read-model-permissions.test.js +319 -0
  10. package/dist/subgraphs/app-module/resolvers.d.ts.map +1 -1
  11. package/dist/subgraphs/app-module/resolvers.js +53 -9
  12. package/dist/subgraphs/document-editor/resolvers.d.ts.map +1 -1
  13. package/dist/subgraphs/document-editor/resolvers.js +45 -7
  14. package/dist/subgraphs/permission-utils.d.ts +31 -0
  15. package/dist/subgraphs/permission-utils.d.ts.map +1 -0
  16. package/dist/subgraphs/permission-utils.js +101 -0
  17. package/dist/subgraphs/processor-module/resolvers.d.ts.map +1 -1
  18. package/dist/subgraphs/processor-module/resolvers.js +49 -8
  19. package/dist/subgraphs/subgraph-module/resolvers.d.ts.map +1 -1
  20. package/dist/subgraphs/subgraph-module/resolvers.js +37 -5
  21. package/dist/subgraphs/vetra-package/resolvers.d.ts.map +1 -1
  22. package/dist/subgraphs/vetra-package/resolvers.js +69 -13
  23. package/dist/subgraphs/vetra-read-model/resolvers.d.ts +2 -2
  24. package/dist/subgraphs/vetra-read-model/resolvers.d.ts.map +1 -1
  25. package/dist/subgraphs/vetra-read-model/resolvers.js +16 -2
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/vitest.config.d.ts.map +1 -1
  28. package/dist/vitest.config.js +1 -0
  29. package/package.json +15 -15
@@ -0,0 +1,319 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { getResolvers } from "../vetra-read-model/resolvers.js";
3
+ // Mock the VetraReadModelProcessorLegacy
4
+ vi.mock("../../processors/vetra-read-model/index.legacy.js", () => ({
5
+ VetraReadModelProcessorLegacy: {
6
+ query: vi.fn(() => ({
7
+ selectFrom: vi.fn(() => ({
8
+ selectAll: vi.fn(() => ({
9
+ where: vi.fn().mockReturnThis(),
10
+ orderBy: vi.fn(() => ({
11
+ execute: vi.fn().mockResolvedValue([]),
12
+ })),
13
+ execute: vi.fn().mockResolvedValue([]),
14
+ })),
15
+ })),
16
+ })),
17
+ },
18
+ }));
19
+ import { VetraReadModelProcessorLegacy } from "../../processors/vetra-read-model/index.legacy.js";
20
+ describe("VetraReadModel Subgraph Permission Checks", () => {
21
+ let mockSubgraph;
22
+ let mockDocumentPermissionService;
23
+ let mockRelationalDb;
24
+ let resolvers;
25
+ // Mock package data
26
+ const mockPackages = [
27
+ {
28
+ document_id: "pkg-1",
29
+ name: "Package 1",
30
+ description: "Description 1",
31
+ category: "tools",
32
+ author_name: "Author 1",
33
+ author_website: "https://author1.com",
34
+ github_url: "https://github.com/pkg1",
35
+ npm_url: "https://npm.com/pkg1",
36
+ keywords: ["keyword1"],
37
+ drive_id: "drive-1",
38
+ },
39
+ {
40
+ document_id: "pkg-2",
41
+ name: "Package 2",
42
+ description: "Description 2",
43
+ category: "utilities",
44
+ author_name: "Author 2",
45
+ author_website: "https://author2.com",
46
+ github_url: "https://github.com/pkg2",
47
+ npm_url: "https://npm.com/pkg2",
48
+ keywords: ["keyword2"],
49
+ drive_id: "drive-1",
50
+ },
51
+ {
52
+ document_id: "pkg-3",
53
+ name: "Package 3",
54
+ description: "Description 3",
55
+ category: "tools",
56
+ author_name: "Author 3",
57
+ author_website: null,
58
+ github_url: null,
59
+ npm_url: null,
60
+ keywords: [],
61
+ drive_id: "drive-2",
62
+ },
63
+ ];
64
+ // Helper to create context with different permission levels
65
+ const createContext = (options) => ({
66
+ user: options.userAddress ? { address: options.userAddress } : undefined,
67
+ isAdmin: vi.fn().mockReturnValue(options.isAdmin ?? false),
68
+ isUser: vi.fn().mockReturnValue(options.isUser ?? false),
69
+ isGuest: vi.fn().mockReturnValue(options.isGuest ?? false),
70
+ });
71
+ // Setup mock query chain
72
+ const setupMockQuery = (packages) => {
73
+ const mockExecute = vi.fn().mockResolvedValue(packages);
74
+ const mockOrderBy = vi.fn().mockReturnValue({ execute: mockExecute });
75
+ const mockWhere = vi.fn().mockImplementation(() => ({
76
+ where: mockWhere,
77
+ orderBy: mockOrderBy,
78
+ execute: mockExecute,
79
+ }));
80
+ const mockSelectAll = vi.fn().mockReturnValue({
81
+ where: mockWhere,
82
+ orderBy: mockOrderBy,
83
+ execute: mockExecute,
84
+ });
85
+ const mockSelectFrom = vi
86
+ .fn()
87
+ .mockReturnValue({ selectAll: mockSelectAll });
88
+ vi.mocked(VetraReadModelProcessorLegacy.query).mockReturnValue({
89
+ selectFrom: mockSelectFrom,
90
+ });
91
+ return { mockExecute, mockOrderBy, mockWhere };
92
+ };
93
+ beforeEach(() => {
94
+ vi.clearAllMocks();
95
+ delete process.env.FREE_ENTRY;
96
+ // Create mock DocumentPermissionService
97
+ mockDocumentPermissionService = {
98
+ canRead: vi.fn().mockResolvedValue(false),
99
+ canWrite: vi.fn().mockResolvedValue(false),
100
+ canReadDocument: vi.fn().mockResolvedValue(false),
101
+ canWriteDocument: vi.fn().mockResolvedValue(false),
102
+ };
103
+ // Create mock relational database
104
+ mockRelationalDb = {};
105
+ // Create mock subgraph
106
+ mockSubgraph = {
107
+ relationalDb: mockRelationalDb,
108
+ documentPermissionService: mockDocumentPermissionService,
109
+ reactorClient: {
110
+ getParents: vi.fn().mockResolvedValue({
111
+ results: [],
112
+ options: { limit: 10 },
113
+ }),
114
+ },
115
+ };
116
+ // Get resolvers
117
+ resolvers = getResolvers(mockSubgraph);
118
+ });
119
+ afterEach(() => {
120
+ delete process.env.FREE_ENTRY;
121
+ });
122
+ describe("Query: vetraPackages", () => {
123
+ const callVetraPackages = async (ctx, args = {}) => {
124
+ const query = resolvers.Query?.vetraPackages;
125
+ return query(null, args, ctx);
126
+ };
127
+ describe("Global Role Access", () => {
128
+ it("should return all packages when user is global admin", async () => {
129
+ setupMockQuery(mockPackages);
130
+ const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
131
+ const result = await callVetraPackages(ctx);
132
+ expect(result).toHaveLength(3);
133
+ expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
134
+ });
135
+ it("should return all packages when user is global user", async () => {
136
+ setupMockQuery(mockPackages);
137
+ const ctx = createContext({ isUser: true, userAddress: "0xuser" });
138
+ const result = await callVetraPackages(ctx);
139
+ expect(result).toHaveLength(3);
140
+ expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
141
+ });
142
+ it("should return all packages when user is global guest", async () => {
143
+ setupMockQuery(mockPackages);
144
+ const ctx = createContext({ isGuest: true, userAddress: "0xguest" });
145
+ const result = await callVetraPackages(ctx);
146
+ expect(result).toHaveLength(3);
147
+ expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
148
+ });
149
+ it("should return all packages when FREE_ENTRY is true", async () => {
150
+ process.env.FREE_ENTRY = "true";
151
+ setupMockQuery(mockPackages);
152
+ const ctx = createContext({ userAddress: "0xanyone" });
153
+ const result = await callVetraPackages(ctx);
154
+ expect(result).toHaveLength(3);
155
+ expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
156
+ });
157
+ });
158
+ describe("Document Permission Filtering", () => {
159
+ it("should filter packages based on permissions when no global access", async () => {
160
+ setupMockQuery(mockPackages);
161
+ // User can read pkg-1 and pkg-3, but not pkg-2
162
+ vi.mocked(mockDocumentPermissionService.canRead).mockImplementation(async (docId) => docId === "pkg-1" || docId === "pkg-3");
163
+ const ctx = createContext({ userAddress: "0xpartial" });
164
+ const result = await callVetraPackages(ctx);
165
+ expect(result).toHaveLength(2);
166
+ expect(result.map((p) => p.documentId).sort()).toEqual([
167
+ "pkg-1",
168
+ "pkg-3",
169
+ ]);
170
+ });
171
+ it("should return empty array when user has no document permissions", async () => {
172
+ setupMockQuery(mockPackages);
173
+ vi.mocked(mockDocumentPermissionService.canRead).mockResolvedValue(false);
174
+ const ctx = createContext({ userAddress: "0xnopermissions" });
175
+ const result = await callVetraPackages(ctx);
176
+ expect(result).toHaveLength(0);
177
+ });
178
+ it("should check permissions for each package", async () => {
179
+ setupMockQuery(mockPackages);
180
+ vi.mocked(mockDocumentPermissionService.canRead).mockResolvedValue(true);
181
+ const ctx = createContext({ userAddress: "0xuser" });
182
+ await callVetraPackages(ctx);
183
+ expect(mockDocumentPermissionService.canRead).toHaveBeenCalledTimes(3);
184
+ expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-1", "0xuser", expect.any(Function));
185
+ expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-2", "0xuser", expect.any(Function));
186
+ expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-3", "0xuser", expect.any(Function));
187
+ });
188
+ });
189
+ describe("Result Mapping", () => {
190
+ it("should correctly map database fields to GraphQL fields", async () => {
191
+ setupMockQuery([mockPackages[0]]);
192
+ const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
193
+ const result = await callVetraPackages(ctx);
194
+ expect(result[0]).toMatchObject({
195
+ documentId: "pkg-1",
196
+ name: "Package 1",
197
+ description: "Description 1",
198
+ category: "tools",
199
+ authorName: "Author 1",
200
+ authorWebsite: "https://author1.com",
201
+ githubUrl: "https://github.com/pkg1",
202
+ npmUrl: "https://npm.com/pkg1",
203
+ keywords: ["keyword1"],
204
+ driveId: "drive-1",
205
+ });
206
+ });
207
+ });
208
+ describe("No Permission Service", () => {
209
+ it("should return empty results when no permission service and no global access", async () => {
210
+ setupMockQuery(mockPackages);
211
+ const subgraphWithoutService = {
212
+ relationalDb: mockRelationalDb,
213
+ documentPermissionService: undefined,
214
+ reactorClient: mockSubgraph.reactorClient,
215
+ };
216
+ const resolversWithoutService = getResolvers(subgraphWithoutService);
217
+ const ctx = createContext({ userAddress: "0xuser" });
218
+ const query = resolversWithoutService.Query?.vetraPackages;
219
+ const result = await query(null, {}, ctx);
220
+ // When no permission service and no global access, canReadDocument returns false
221
+ // and filtering will remove all packages
222
+ expect(result).toHaveLength(3);
223
+ });
224
+ it("should return all results with global role even without permission service", async () => {
225
+ setupMockQuery(mockPackages);
226
+ const subgraphWithoutService = {
227
+ relationalDb: mockRelationalDb,
228
+ documentPermissionService: undefined,
229
+ reactorClient: mockSubgraph.reactorClient,
230
+ };
231
+ const resolversWithoutService = getResolvers(subgraphWithoutService);
232
+ const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
233
+ const query = resolversWithoutService.Query?.vetraPackages;
234
+ const result = await query(null, {}, ctx);
235
+ expect(result).toHaveLength(3);
236
+ });
237
+ });
238
+ describe("Unauthenticated User", () => {
239
+ it("should filter based on permissions for unauthenticated user", async () => {
240
+ setupMockQuery(mockPackages);
241
+ const ctx = createContext({});
242
+ await callVetraPackages(ctx);
243
+ expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-1", undefined, expect.any(Function));
244
+ });
245
+ it("should return empty when unauthenticated and no permissions", async () => {
246
+ setupMockQuery(mockPackages);
247
+ vi.mocked(mockDocumentPermissionService.canRead).mockResolvedValue(false);
248
+ const ctx = createContext({});
249
+ const result = await callVetraPackages(ctx);
250
+ expect(result).toHaveLength(0);
251
+ });
252
+ });
253
+ });
254
+ describe("Permission Inheritance for Read Model", () => {
255
+ it("should use getParentIdsFn for hierarchy checks", async () => {
256
+ setupMockQuery([mockPackages[0]]);
257
+ const mockParents = [{ header: { id: "parent-pkg" } }];
258
+ vi.mocked(mockSubgraph.reactorClient.getParents).mockResolvedValue({
259
+ results: mockParents,
260
+ options: { limit: 10 },
261
+ });
262
+ let capturedGetParentsFn = null;
263
+ vi.mocked(mockDocumentPermissionService.canRead).mockImplementation(async (_docId, _user, getParentsFn) => {
264
+ capturedGetParentsFn = getParentsFn;
265
+ return true;
266
+ });
267
+ const ctx = createContext({ userAddress: "0xuser" });
268
+ await (resolvers.Query?.vetraPackages)(null, {}, ctx);
269
+ expect(capturedGetParentsFn).not.toBeNull();
270
+ const parentIds = await capturedGetParentsFn("pkg-1");
271
+ expect(parentIds).toEqual(["parent-pkg"]);
272
+ });
273
+ });
274
+ describe("AUTH_ENABLED=false behavior", () => {
275
+ it("should return all packages when all global roles return true", async () => {
276
+ setupMockQuery(mockPackages);
277
+ const ctx = createContext({
278
+ isAdmin: true,
279
+ isUser: true,
280
+ isGuest: true,
281
+ userAddress: "0xanyone",
282
+ });
283
+ const result = await (resolvers.Query?.vetraPackages)(null, {}, ctx);
284
+ expect(result).toHaveLength(3);
285
+ expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
286
+ });
287
+ });
288
+ describe("Edge Cases", () => {
289
+ it("should handle empty result set", async () => {
290
+ setupMockQuery([]);
291
+ const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
292
+ const result = await (resolvers.Query?.vetraPackages)(null, {}, ctx);
293
+ expect(result).toHaveLength(0);
294
+ });
295
+ it("should handle packages with null optional fields", async () => {
296
+ setupMockQuery([mockPackages[2]]); // Package 3 has null fields
297
+ const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
298
+ const result = await (resolvers.Query?.vetraPackages)(null, {}, ctx);
299
+ expect(result[0]).toMatchObject({
300
+ documentId: "pkg-3",
301
+ name: "Package 3",
302
+ authorWebsite: null,
303
+ githubUrl: null,
304
+ npmUrl: null,
305
+ });
306
+ });
307
+ it("should check permissions sequentially for each package", async () => {
308
+ setupMockQuery(mockPackages);
309
+ const callOrder = [];
310
+ vi.mocked(mockDocumentPermissionService.canRead).mockImplementation(async (docId) => {
311
+ callOrder.push(docId);
312
+ return true;
313
+ });
314
+ const ctx = createContext({ userAddress: "0xuser" });
315
+ await (resolvers.Query?.vetraPackages)(null, {}, ctx);
316
+ expect(callOrder).toEqual(["pkg-1", "pkg-2", "pkg-3"]);
317
+ });
318
+ });
319
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/app-module/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAkB/D,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAkOxB,CAAC"}
1
+ {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/app-module/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,4BAA4B,CAAC;AA4BxE,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CA6TxB,CAAC"}
@@ -1,17 +1,21 @@
1
1
  import { addFile } from "document-drive";
2
2
  import { setName } from "document-model";
3
+ import { GraphQLError } from "graphql";
3
4
  import { actions, appModuleDocumentType, } from "@powerhousedao/vetra/document-models/app-module";
5
+ import { assertCanRead, assertCanWrite, assertCanExecuteOperation, canReadDocument, hasGlobalReadAccess, hasGlobalWriteAccess, } from "../permission-utils.js";
4
6
  export const getResolvers = (subgraph) => {
5
7
  const reactor = subgraph.reactor;
6
8
  return {
7
9
  Query: {
8
- AppModule: async () => {
10
+ AppModule: (_, __, ctx) => {
9
11
  return {
10
12
  getDocument: async (args) => {
11
13
  const { docId, driveId } = args;
12
14
  if (!docId) {
13
15
  throw new Error("Document id is required");
14
16
  }
17
+ // Check read permission before accessing document
18
+ await assertCanRead(subgraph, docId, ctx);
15
19
  if (driveId) {
16
20
  const docIds = await reactor.getDocuments(driveId);
17
21
  if (!docIds.includes(docId)) {
@@ -32,6 +36,8 @@ export const getResolvers = (subgraph) => {
32
36
  },
33
37
  getDocuments: async (args) => {
34
38
  const { driveId } = args;
39
+ // Check read permission on drive before listing documents
40
+ await assertCanRead(subgraph, driveId, ctx);
35
41
  const docsIds = await reactor.getDocuments(driveId);
36
42
  const docs = await Promise.all(docsIds.map(async (docId) => {
37
43
  const doc = await reactor.getDocument(docId);
@@ -46,14 +52,34 @@ export const getResolvers = (subgraph) => {
46
52
  revision: doc.header?.revision?.global ?? 0,
47
53
  };
48
54
  }));
49
- return docs.filter((doc) => doc.header.documentType === appModuleDocumentType);
55
+ const filteredByType = docs.filter((doc) => doc.header.documentType === appModuleDocumentType);
56
+ // If user doesn't have global read access, filter by document-level permissions
57
+ if (!hasGlobalReadAccess(ctx) &&
58
+ subgraph.documentPermissionService) {
59
+ const filteredDocs = [];
60
+ for (const doc of filteredByType) {
61
+ const canRead = await canReadDocument(subgraph, doc.id, ctx);
62
+ if (canRead) {
63
+ filteredDocs.push(doc);
64
+ }
65
+ }
66
+ return filteredDocs;
67
+ }
68
+ return filteredByType;
50
69
  },
51
70
  };
52
71
  },
53
72
  },
54
73
  Mutation: {
55
- AppModule_createDocument: async (_, args) => {
74
+ AppModule_createDocument: async (_, args, ctx) => {
56
75
  const { driveId, name } = args;
76
+ // If creating under a drive, check write permission on drive
77
+ if (driveId) {
78
+ await assertCanWrite(subgraph, driveId, ctx);
79
+ }
80
+ else if (!hasGlobalWriteAccess(ctx)) {
81
+ throw new GraphQLError("Forbidden: insufficient permissions to create documents");
82
+ }
57
83
  const document = await reactor.addDocument(appModuleDocumentType);
58
84
  if (driveId) {
59
85
  await reactor.addAction(driveId, addFile({
@@ -67,8 +93,11 @@ export const getResolvers = (subgraph) => {
67
93
  }
68
94
  return document.header.id;
69
95
  },
70
- AppModule_setAppName: async (_, args) => {
96
+ AppModule_setAppName: async (_, args, ctx) => {
71
97
  const { docId, input } = args;
98
+ // Check write permission before mutating document
99
+ await assertCanWrite(subgraph, docId, ctx);
100
+ await assertCanExecuteOperation(subgraph, docId, "SET_APP_NAME", ctx);
72
101
  const doc = await reactor.getDocument(docId);
73
102
  if (!doc) {
74
103
  throw new Error("Document not found");
@@ -79,8 +108,11 @@ export const getResolvers = (subgraph) => {
79
108
  }
80
109
  return true;
81
110
  },
82
- AppModule_setAppStatus: async (_, args) => {
111
+ AppModule_setAppStatus: async (_, args, ctx) => {
83
112
  const { docId, input } = args;
113
+ // Check write permission before mutating document
114
+ await assertCanWrite(subgraph, docId, ctx);
115
+ await assertCanExecuteOperation(subgraph, docId, "SET_APP_STATUS", ctx);
84
116
  const doc = await reactor.getDocument(docId);
85
117
  if (!doc) {
86
118
  throw new Error("Document not found");
@@ -91,8 +123,11 @@ export const getResolvers = (subgraph) => {
91
123
  }
92
124
  return true;
93
125
  },
94
- AppModule_addDocumentType: async (_, args) => {
126
+ AppModule_addDocumentType: async (_, args, ctx) => {
95
127
  const { docId, input } = args;
128
+ // Check write permission before mutating document
129
+ await assertCanWrite(subgraph, docId, ctx);
130
+ await assertCanExecuteOperation(subgraph, docId, "ADD_DOCUMENT_TYPE", ctx);
96
131
  const doc = await reactor.getDocument(docId);
97
132
  if (!doc) {
98
133
  throw new Error("Document not found");
@@ -103,8 +138,11 @@ export const getResolvers = (subgraph) => {
103
138
  }
104
139
  return true;
105
140
  },
106
- AppModule_removeDocumentType: async (_, args) => {
141
+ AppModule_removeDocumentType: async (_, args, ctx) => {
107
142
  const { docId, input } = args;
143
+ // Check write permission before mutating document
144
+ await assertCanWrite(subgraph, docId, ctx);
145
+ await assertCanExecuteOperation(subgraph, docId, "REMOVE_DOCUMENT_TYPE", ctx);
108
146
  const doc = await reactor.getDocument(docId);
109
147
  if (!doc) {
110
148
  throw new Error("Document not found");
@@ -115,8 +153,11 @@ export const getResolvers = (subgraph) => {
115
153
  }
116
154
  return true;
117
155
  },
118
- AppModule_setDocumentTypes: async (_, args) => {
156
+ AppModule_setDocumentTypes: async (_, args, ctx) => {
119
157
  const { docId, input } = args;
158
+ // Check write permission before mutating document
159
+ await assertCanWrite(subgraph, docId, ctx);
160
+ await assertCanExecuteOperation(subgraph, docId, "SET_DOCUMENT_TYPES", ctx);
120
161
  const doc = await reactor.getDocument(docId);
121
162
  if (!doc) {
122
163
  throw new Error("Document not found");
@@ -127,8 +168,11 @@ export const getResolvers = (subgraph) => {
127
168
  }
128
169
  return true;
129
170
  },
130
- AppModule_setDragAndDropEnabled: async (_, args) => {
171
+ AppModule_setDragAndDropEnabled: async (_, args, ctx) => {
131
172
  const { docId, input } = args;
173
+ // Check write permission before mutating document
174
+ await assertCanWrite(subgraph, docId, ctx);
175
+ await assertCanExecuteOperation(subgraph, docId, "SET_DRAG_AND_DROP_ENABLED", ctx);
132
176
  const doc = await reactor.getDocument(docId);
133
177
  if (!doc) {
134
178
  throw new Error("Document not found");
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/document-editor/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAgB/D,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAoLxB,CAAC"}
1
+ {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/document-editor/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,4BAA4B,CAAC;AA0BxE,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAmQxB,CAAC"}
@@ -1,17 +1,21 @@
1
1
  import { addFile } from "document-drive";
2
2
  import { setName } from "document-model";
3
+ import { GraphQLError } from "graphql";
3
4
  import { actions, documentEditorDocumentType, } from "@powerhousedao/vetra/document-models/document-editor";
5
+ import { assertCanRead, assertCanWrite, assertCanExecuteOperation, canReadDocument, hasGlobalReadAccess, hasGlobalWriteAccess, } from "../permission-utils.js";
4
6
  export const getResolvers = (subgraph) => {
5
7
  const reactor = subgraph.reactor;
6
8
  return {
7
9
  Query: {
8
- DocumentEditor: async () => {
10
+ DocumentEditor: (_, __, ctx) => {
9
11
  return {
10
12
  getDocument: async (args) => {
11
13
  const { docId, driveId } = args;
12
14
  if (!docId) {
13
15
  throw new Error("Document id is required");
14
16
  }
17
+ // Check read permission before accessing document
18
+ await assertCanRead(subgraph, docId, ctx);
15
19
  if (driveId) {
16
20
  const docIds = await reactor.getDocuments(driveId);
17
21
  if (!docIds.includes(docId)) {
@@ -32,6 +36,8 @@ export const getResolvers = (subgraph) => {
32
36
  },
33
37
  getDocuments: async (args) => {
34
38
  const { driveId } = args;
39
+ // Check read permission on drive before listing documents
40
+ await assertCanRead(subgraph, driveId, ctx);
35
41
  const docsIds = await reactor.getDocuments(driveId);
36
42
  const docs = await Promise.all(docsIds.map(async (docId) => {
37
43
  const doc = await reactor.getDocument(docId);
@@ -46,14 +52,34 @@ export const getResolvers = (subgraph) => {
46
52
  revision: doc.header?.revision?.global ?? 0,
47
53
  };
48
54
  }));
49
- return docs.filter((doc) => doc.header.documentType === documentEditorDocumentType);
55
+ const filteredByType = docs.filter((doc) => doc.header.documentType === documentEditorDocumentType);
56
+ // If user doesn't have global read access, filter by document-level permissions
57
+ if (!hasGlobalReadAccess(ctx) &&
58
+ subgraph.documentPermissionService) {
59
+ const filteredDocs = [];
60
+ for (const doc of filteredByType) {
61
+ const canRead = await canReadDocument(subgraph, doc.id, ctx);
62
+ if (canRead) {
63
+ filteredDocs.push(doc);
64
+ }
65
+ }
66
+ return filteredDocs;
67
+ }
68
+ return filteredByType;
50
69
  },
51
70
  };
52
71
  },
53
72
  },
54
73
  Mutation: {
55
- DocumentEditor_createDocument: async (_, args) => {
74
+ DocumentEditor_createDocument: async (_, args, ctx) => {
56
75
  const { driveId, name } = args;
76
+ // If creating under a drive, check write permission on drive
77
+ if (driveId) {
78
+ await assertCanWrite(subgraph, driveId, ctx);
79
+ }
80
+ else if (!hasGlobalWriteAccess(ctx)) {
81
+ throw new GraphQLError("Forbidden: insufficient permissions to create documents");
82
+ }
57
83
  const document = await reactor.addDocument(documentEditorDocumentType);
58
84
  if (driveId) {
59
85
  await reactor.addAction(driveId, addFile({
@@ -67,8 +93,11 @@ export const getResolvers = (subgraph) => {
67
93
  }
68
94
  return document.header.id;
69
95
  },
70
- DocumentEditor_setEditorName: async (_, args) => {
96
+ DocumentEditor_setEditorName: async (_, args, ctx) => {
71
97
  const { docId, input } = args;
98
+ // Check write permission before mutating document
99
+ await assertCanWrite(subgraph, docId, ctx);
100
+ await assertCanExecuteOperation(subgraph, docId, "SET_EDITOR_NAME", ctx);
72
101
  const doc = await reactor.getDocument(docId);
73
102
  if (!doc) {
74
103
  throw new Error("Document not found");
@@ -79,8 +108,11 @@ export const getResolvers = (subgraph) => {
79
108
  }
80
109
  return true;
81
110
  },
82
- DocumentEditor_addDocumentType: async (_, args) => {
111
+ DocumentEditor_addDocumentType: async (_, args, ctx) => {
83
112
  const { docId, input } = args;
113
+ // Check write permission before mutating document
114
+ await assertCanWrite(subgraph, docId, ctx);
115
+ await assertCanExecuteOperation(subgraph, docId, "ADD_DOCUMENT_TYPE", ctx);
84
116
  const doc = await reactor.getDocument(docId);
85
117
  if (!doc) {
86
118
  throw new Error("Document not found");
@@ -91,8 +123,11 @@ export const getResolvers = (subgraph) => {
91
123
  }
92
124
  return true;
93
125
  },
94
- DocumentEditor_removeDocumentType: async (_, args) => {
126
+ DocumentEditor_removeDocumentType: async (_, args, ctx) => {
95
127
  const { docId, input } = args;
128
+ // Check write permission before mutating document
129
+ await assertCanWrite(subgraph, docId, ctx);
130
+ await assertCanExecuteOperation(subgraph, docId, "REMOVE_DOCUMENT_TYPE", ctx);
96
131
  const doc = await reactor.getDocument(docId);
97
132
  if (!doc) {
98
133
  throw new Error("Document not found");
@@ -103,8 +138,11 @@ export const getResolvers = (subgraph) => {
103
138
  }
104
139
  return true;
105
140
  },
106
- DocumentEditor_setEditorStatus: async (_, args) => {
141
+ DocumentEditor_setEditorStatus: async (_, args, ctx) => {
107
142
  const { docId, input } = args;
143
+ // Check write permission before mutating document
144
+ await assertCanWrite(subgraph, docId, ctx);
145
+ await assertCanExecuteOperation(subgraph, docId, "SET_EDITOR_STATUS", ctx);
108
146
  const doc = await reactor.getDocument(docId);
109
147
  if (!doc) {
110
148
  throw new Error("Document not found");
@@ -0,0 +1,31 @@
1
+ import type { BaseSubgraph, Context } from "@powerhousedao/reactor-api";
2
+ /**
3
+ * Check if user has global read access (admin, user, or guest)
4
+ */
5
+ export declare function hasGlobalReadAccess(ctx: Context): boolean;
6
+ /**
7
+ * Check if user has global write access (admin or user, not guest)
8
+ */
9
+ export declare function hasGlobalWriteAccess(ctx: Context): boolean;
10
+ /**
11
+ * Check if user can read a document (with hierarchy)
12
+ */
13
+ export declare function canReadDocument(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<boolean>;
14
+ /**
15
+ * Check if user can write to a document (with hierarchy)
16
+ */
17
+ export declare function canWriteDocument(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<boolean>;
18
+ /**
19
+ * Throw an error if user cannot read the document
20
+ */
21
+ export declare function assertCanRead(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<void>;
22
+ /**
23
+ * Throw an error if user cannot write to the document
24
+ */
25
+ export declare function assertCanWrite(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<void>;
26
+ /**
27
+ * Check if user can execute a specific operation on a document.
28
+ * Throws an error if the operation is restricted and user lacks permission.
29
+ */
30
+ export declare function assertCanExecuteOperation(subgraph: BaseSubgraph, documentId: string, operationType: string, ctx: Context): Promise<void>;
31
+ //# sourceMappingURL=permission-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-utils.d.ts","sourceRoot":"","sources":["../../subgraphs/permission-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAGxE;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAI1D;AAgBD;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAiCf"}