@opensaas/stack-core 0.1.5 → 0.1.7

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 (45) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +5 -5
  4. package/dist/access/engine.d.ts +9 -3
  5. package/dist/access/engine.d.ts.map +1 -1
  6. package/dist/access/engine.js +6 -2
  7. package/dist/access/engine.js.map +1 -1
  8. package/dist/access/types.d.ts +1 -0
  9. package/dist/access/types.d.ts.map +1 -1
  10. package/dist/config/index.d.ts +6 -2
  11. package/dist/config/index.d.ts.map +1 -1
  12. package/dist/config/index.js +12 -2
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/config/plugin-engine.d.ts +25 -0
  15. package/dist/config/plugin-engine.d.ts.map +1 -0
  16. package/dist/config/plugin-engine.js +240 -0
  17. package/dist/config/plugin-engine.js.map +1 -0
  18. package/dist/config/types.d.ts +129 -0
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/context/index.d.ts +11 -1
  21. package/dist/context/index.d.ts.map +1 -1
  22. package/dist/context/index.js +116 -86
  23. package/dist/context/index.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcp/handler.d.ts +1 -1
  28. package/dist/mcp/handler.d.ts.map +1 -1
  29. package/dist/mcp/handler.js +2 -2
  30. package/dist/mcp/handler.js.map +1 -1
  31. package/package.json +5 -1
  32. package/src/access/engine.ts +10 -5
  33. package/src/access/types.ts +1 -0
  34. package/src/config/index.ts +17 -2
  35. package/src/config/plugin-engine.ts +302 -0
  36. package/src/config/types.ts +147 -0
  37. package/src/context/index.ts +137 -95
  38. package/src/index.ts +4 -0
  39. package/src/mcp/handler.ts +6 -6
  40. package/tests/context.test.ts +13 -13
  41. package/tests/nested-access-and-hooks.test.ts +13 -13
  42. package/tests/password-type-distribution.test.ts +3 -3
  43. package/tests/password-types.test.ts +5 -5
  44. package/tests/sudo.test.ts +406 -0
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,406 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { getContext } from '../src/context/index.js'
3
+ import { config, list } from '../src/config/index.js'
4
+ import { text, integer } from '../src/fields/index.js'
5
+ import type { PrismaClient } from '@prisma/client'
6
+
7
+ describe('Sudo Context', () => {
8
+ // Mock Prisma client
9
+ const mockPrisma = {
10
+ post: {
11
+ findFirst: vi.fn(),
12
+ findMany: vi.fn(),
13
+ findUnique: vi.fn(),
14
+ create: vi.fn(),
15
+ update: vi.fn(),
16
+ delete: vi.fn(),
17
+ count: vi.fn(),
18
+ },
19
+ } as unknown as PrismaClient
20
+
21
+ // Track hook execution
22
+ const hookExecutions: string[] = []
23
+
24
+ const testConfig = config({
25
+ db: {
26
+ provider: 'sqlite',
27
+ url: 'file:./test.db',
28
+ },
29
+ lists: {
30
+ Post: list({
31
+ fields: {
32
+ title: text({
33
+ validation: { isRequired: true },
34
+ hooks: {
35
+ resolveInput: async ({ inputValue }) => {
36
+ hookExecutions.push('field-resolveInput')
37
+ return inputValue
38
+ },
39
+ beforeOperation: async () => {
40
+ hookExecutions.push('field-beforeOperation')
41
+ },
42
+ afterOperation: async () => {
43
+ hookExecutions.push('field-afterOperation')
44
+ },
45
+ },
46
+ }),
47
+ secretField: text({
48
+ access: {
49
+ read: async () => false,
50
+ create: async () => false,
51
+ update: async () => false,
52
+ },
53
+ }),
54
+ views: integer({ defaultValue: 0 }),
55
+ },
56
+ access: {
57
+ operation: {
58
+ query: async () => false,
59
+ create: async () => false,
60
+ update: async () => false,
61
+ delete: async () => false,
62
+ },
63
+ },
64
+ hooks: {
65
+ resolveInput: async ({ resolvedData }) => {
66
+ hookExecutions.push('list-resolveInput')
67
+ return resolvedData
68
+ },
69
+ validateInput: async () => {
70
+ hookExecutions.push('list-validateInput')
71
+ },
72
+ beforeOperation: async () => {
73
+ hookExecutions.push('list-beforeOperation')
74
+ },
75
+ afterOperation: async () => {
76
+ hookExecutions.push('list-afterOperation')
77
+ },
78
+ },
79
+ }),
80
+ },
81
+ })
82
+
83
+ beforeEach(() => {
84
+ vi.clearAllMocks()
85
+ hookExecutions.length = 0
86
+ })
87
+
88
+ describe('Query Operations', () => {
89
+ it('should bypass operation-level access control with sudo()', async () => {
90
+ const context = getContext(testConfig, mockPrisma, null)
91
+ const sudoContext = context.sudo()
92
+
93
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
94
+
95
+ // Regular context should return empty array (access denied)
96
+ const regularResult = await context.db.post.findMany()
97
+ expect(regularResult).toEqual([])
98
+
99
+ // Sudo context should return results
100
+ const sudoResult = await sudoContext.db.post.findMany()
101
+ expect(sudoResult).toHaveLength(1)
102
+ expect(sudoResult[0].title).toBe('Test Post')
103
+ })
104
+
105
+ it('should bypass field-level read access control with sudo()', async () => {
106
+ const context = getContext(testConfig, mockPrisma, null)
107
+ const sudoContext = context.sudo()
108
+
109
+ mockPrisma.post.findFirst.mockResolvedValue({
110
+ id: '1',
111
+ title: 'Test Post',
112
+ secretField: 'secret-value',
113
+ views: 10,
114
+ })
115
+
116
+ // Sudo context should return all fields including secretField
117
+ const sudoResult = await sudoContext.db.post.findUnique({ where: { id: '1' } })
118
+ expect(sudoResult?.secretField).toBe('secret-value')
119
+ })
120
+
121
+ it('should execute field afterOperation hooks with sudo()', async () => {
122
+ const context = getContext(testConfig, mockPrisma, null)
123
+ const sudoContext = context.sudo()
124
+
125
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
126
+
127
+ await sudoContext.db.post.findMany()
128
+
129
+ expect(hookExecutions).toContain('field-afterOperation')
130
+ })
131
+ })
132
+
133
+ describe('Create Operations', () => {
134
+ it('should bypass operation-level access control with sudo()', async () => {
135
+ const context = getContext(testConfig, mockPrisma, null)
136
+ const sudoContext = context.sudo()
137
+
138
+ const mockPost = { id: '1', title: 'New Post', views: 0 }
139
+ mockPrisma.post.create.mockResolvedValue(mockPost)
140
+
141
+ // Regular context should return null (access denied)
142
+ const regularResult = await context.db.post.create({
143
+ data: { title: 'New Post' },
144
+ })
145
+ expect(regularResult).toBeNull()
146
+
147
+ // Sudo context should create successfully
148
+ hookExecutions.length = 0
149
+ const sudoResult = await sudoContext.db.post.create({
150
+ data: { title: 'New Post' },
151
+ })
152
+ expect(sudoResult).toMatchObject({ title: 'New Post' })
153
+ expect(mockPrisma.post.create).toHaveBeenCalledWith({
154
+ data: { title: 'New Post' },
155
+ })
156
+ })
157
+
158
+ it('should bypass field-level write access control with sudo()', async () => {
159
+ const context = getContext(testConfig, mockPrisma, null)
160
+ const sudoContext = context.sudo()
161
+
162
+ const mockPost = { id: '1', title: 'New Post', secretField: 'secret', views: 0 }
163
+ mockPrisma.post.create.mockResolvedValue(mockPost)
164
+
165
+ // Sudo context should allow writing to secretField
166
+ await sudoContext.db.post.create({
167
+ data: { title: 'New Post', secretField: 'secret' },
168
+ })
169
+
170
+ // Verify that secretField was passed to Prisma
171
+ expect(mockPrisma.post.create).toHaveBeenCalledWith({
172
+ data: { title: 'New Post', secretField: 'secret' },
173
+ })
174
+ })
175
+
176
+ it('should execute all hooks with sudo()', async () => {
177
+ const context = getContext(testConfig, mockPrisma, null)
178
+ const sudoContext = context.sudo()
179
+
180
+ const mockPost = { id: '1', title: 'New Post', views: 0 }
181
+ mockPrisma.post.create.mockResolvedValue(mockPost)
182
+
183
+ await sudoContext.db.post.create({
184
+ data: { title: 'New Post' },
185
+ })
186
+
187
+ // Verify all hooks were executed
188
+ expect(hookExecutions).toContain('list-resolveInput')
189
+ expect(hookExecutions).toContain('field-resolveInput')
190
+ expect(hookExecutions).toContain('list-validateInput')
191
+ expect(hookExecutions).toContain('field-beforeOperation')
192
+ expect(hookExecutions).toContain('list-beforeOperation')
193
+ expect(hookExecutions).toContain('list-afterOperation')
194
+ expect(hookExecutions).toContain('field-afterOperation')
195
+ })
196
+
197
+ it('should still validate required fields with sudo()', async () => {
198
+ const context = getContext(testConfig, mockPrisma, null)
199
+ const sudoContext = context.sudo()
200
+
201
+ // Should throw validation error for missing required field
202
+ await expect(
203
+ sudoContext.db.post.create({
204
+ data: { views: 10 },
205
+ }),
206
+ ).rejects.toThrow('Title must be text')
207
+ })
208
+ })
209
+
210
+ describe('Update Operations', () => {
211
+ it('should bypass operation-level access control with sudo()', async () => {
212
+ const context = getContext(testConfig, mockPrisma, null)
213
+ const sudoContext = context.sudo()
214
+
215
+ const existingPost = { id: '1', title: 'Old Title', views: 5 }
216
+ const updatedPost = { id: '1', title: 'New Title', views: 5 }
217
+
218
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
219
+ mockPrisma.post.findFirst.mockResolvedValue(null)
220
+ mockPrisma.post.update.mockResolvedValue(updatedPost)
221
+
222
+ // Regular context should return null (access denied)
223
+ const regularResult = await context.db.post.update({
224
+ where: { id: '1' },
225
+ data: { title: 'New Title' },
226
+ })
227
+ expect(regularResult).toBeNull()
228
+
229
+ // Reset mocks
230
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
231
+ hookExecutions.length = 0
232
+
233
+ // Sudo context should update successfully
234
+ const sudoResult = await sudoContext.db.post.update({
235
+ where: { id: '1' },
236
+ data: { title: 'New Title' },
237
+ })
238
+ expect(sudoResult).toMatchObject({ title: 'New Title' })
239
+ })
240
+
241
+ it('should bypass field-level write access control with sudo()', async () => {
242
+ const context = getContext(testConfig, mockPrisma, null)
243
+ const sudoContext = context.sudo()
244
+
245
+ const existingPost = { id: '1', title: 'Old Title', secretField: 'old-secret', views: 5 }
246
+ const updatedPost = { id: '1', title: 'Old Title', secretField: 'new-secret', views: 5 }
247
+
248
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
249
+ mockPrisma.post.update.mockResolvedValue(updatedPost)
250
+
251
+ // Sudo context should allow updating secretField
252
+ await sudoContext.db.post.update({
253
+ where: { id: '1' },
254
+ data: { secretField: 'new-secret' },
255
+ })
256
+
257
+ // Verify that secretField was passed to Prisma
258
+ expect(mockPrisma.post.update).toHaveBeenCalledWith({
259
+ where: { id: '1' },
260
+ data: { secretField: 'new-secret' },
261
+ })
262
+ })
263
+
264
+ it('should execute all hooks with sudo()', async () => {
265
+ const context = getContext(testConfig, mockPrisma, null)
266
+ const sudoContext = context.sudo()
267
+
268
+ const existingPost = { id: '1', title: 'Old Title', views: 5 }
269
+ const updatedPost = { id: '1', title: 'New Title', views: 5 }
270
+
271
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
272
+ mockPrisma.post.update.mockResolvedValue(updatedPost)
273
+
274
+ await sudoContext.db.post.update({
275
+ where: { id: '1' },
276
+ data: { title: 'New Title' },
277
+ })
278
+
279
+ // Verify all hooks were executed
280
+ expect(hookExecutions).toContain('list-resolveInput')
281
+ expect(hookExecutions).toContain('field-resolveInput')
282
+ expect(hookExecutions).toContain('list-validateInput')
283
+ expect(hookExecutions).toContain('field-beforeOperation')
284
+ expect(hookExecutions).toContain('list-beforeOperation')
285
+ expect(hookExecutions).toContain('list-afterOperation')
286
+ expect(hookExecutions).toContain('field-afterOperation')
287
+ })
288
+ })
289
+
290
+ describe('Delete Operations', () => {
291
+ it('should bypass operation-level access control with sudo()', async () => {
292
+ const context = getContext(testConfig, mockPrisma, null)
293
+ const sudoContext = context.sudo()
294
+
295
+ const existingPost = { id: '1', title: 'Post to Delete', views: 5 }
296
+
297
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
298
+ mockPrisma.post.findFirst.mockResolvedValue(null)
299
+ mockPrisma.post.delete.mockResolvedValue(existingPost)
300
+
301
+ // Regular context should return null (access denied)
302
+ const regularResult = await context.db.post.delete({
303
+ where: { id: '1' },
304
+ })
305
+ expect(regularResult).toBeNull()
306
+
307
+ // Reset mocks
308
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
309
+ hookExecutions.length = 0
310
+
311
+ // Sudo context should delete successfully
312
+ const sudoResult = await sudoContext.db.post.delete({
313
+ where: { id: '1' },
314
+ })
315
+ expect(sudoResult).toMatchObject({ title: 'Post to Delete' })
316
+ })
317
+
318
+ it('should execute all hooks with sudo()', async () => {
319
+ const context = getContext(testConfig, mockPrisma, null)
320
+ const sudoContext = context.sudo()
321
+
322
+ const existingPost = { id: '1', title: 'Post to Delete', views: 5 }
323
+
324
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
325
+ mockPrisma.post.delete.mockResolvedValue(existingPost)
326
+
327
+ await sudoContext.db.post.delete({
328
+ where: { id: '1' },
329
+ })
330
+
331
+ // Verify hooks were executed
332
+ expect(hookExecutions).toContain('field-beforeOperation')
333
+ expect(hookExecutions).toContain('list-beforeOperation')
334
+ expect(hookExecutions).toContain('list-afterOperation')
335
+ expect(hookExecutions).toContain('field-afterOperation')
336
+ })
337
+ })
338
+
339
+ describe('Count Operations', () => {
340
+ it('should bypass operation-level access control with sudo()', async () => {
341
+ const context = getContext(testConfig, mockPrisma, null)
342
+ const sudoContext = context.sudo()
343
+
344
+ mockPrisma.post.count.mockResolvedValue(10)
345
+
346
+ // Regular context should return 0 (access denied)
347
+ const regularResult = await context.db.post.count()
348
+ expect(regularResult).toBe(0)
349
+
350
+ // Sudo context should return actual count
351
+ const sudoResult = await sudoContext.db.post.count()
352
+ expect(sudoResult).toBe(10)
353
+ })
354
+ })
355
+
356
+ describe('Sudo Context Properties', () => {
357
+ it('should maintain the same session object', async () => {
358
+ const session = { userId: 'user-123', role: 'admin' }
359
+ const context = getContext(testConfig, mockPrisma, session)
360
+ const sudoContext = context.sudo()
361
+
362
+ expect(sudoContext.session).toEqual(session)
363
+ expect(sudoContext.session).toBe(context.session)
364
+ })
365
+
366
+ it('should maintain the same prisma client', async () => {
367
+ const context = getContext(testConfig, mockPrisma, null)
368
+ const sudoContext = context.sudo()
369
+
370
+ expect(sudoContext.prisma).toBe(context.prisma)
371
+ })
372
+
373
+ it('should maintain the same storage utilities', async () => {
374
+ const context = getContext(testConfig, mockPrisma, null)
375
+ const sudoContext = context.sudo()
376
+
377
+ expect(sudoContext.storage).toBe(context.storage)
378
+ })
379
+
380
+ it('should allow chaining sudo() calls', async () => {
381
+ const context = getContext(testConfig, mockPrisma, null)
382
+ const sudoContext1 = context.sudo()
383
+ const sudoContext2 = sudoContext1.sudo()
384
+
385
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
386
+
387
+ const result = await sudoContext2.db.post.findMany()
388
+ expect(result).toHaveLength(1)
389
+ })
390
+
391
+ it('should create independent contexts', async () => {
392
+ const context = getContext(testConfig, mockPrisma, null)
393
+ const sudoContext = context.sudo()
394
+
395
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
396
+
397
+ // Regular context still denies access
398
+ const regularResult = await context.db.post.findMany()
399
+ expect(regularResult).toEqual([])
400
+
401
+ // Sudo context allows access
402
+ const sudoResult = await sudoContext.db.post.findMany()
403
+ expect(sudoResult).toHaveLength(1)
404
+ })
405
+ })
406
+ })