@flowerforce/flowerbase 1.8.4-beta.1 → 1.8.4-beta.3

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.
@@ -1,9 +1,11 @@
1
+ import fs from 'node:fs'
1
2
  import { createRequire } from 'node:module'
2
3
  import path from 'node:path'
3
4
  import { pathToFileURL } from 'node:url'
4
5
  import vm from 'vm'
5
6
  import { EJSON } from 'bson'
6
7
  import { StateManager } from '../../state'
8
+ import { Function as AppFunction } from '../../features/functions/interface'
7
9
  import { generateContextData } from './helpers'
8
10
  import { GenerateContextParams } from './interface'
9
11
 
@@ -92,6 +94,42 @@ const wrapEsmModule = (code: string): string => {
92
94
  return `${prelude}\n${code}\n${trailer}`
93
95
  }
94
96
 
97
+ const transpileSandboxModule = (code: string): string => {
98
+ const exportedNames: string[] = []
99
+ let transformed = code.includes('import ')
100
+ ? transformImportsToRequire(code)
101
+ : code
102
+
103
+ transformed = transformed.replace(
104
+ /^\s*export\s+function\s+([A-Za-z_$][\w$]*)\s*\(/gm,
105
+ (_match, name: string) => {
106
+ exportedNames.push(name)
107
+ return `function ${name}(`
108
+ }
109
+ )
110
+
111
+ transformed = transformed.replace(
112
+ /^\s*export\s+(const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm,
113
+ (_match, kind: string, name: string) => {
114
+ exportedNames.push(name)
115
+ return `${kind} ${name}`
116
+ }
117
+ )
118
+
119
+ transformed = transformed.replace(
120
+ /^\s*export\s+default\s+/gm,
121
+ 'module.exports = '
122
+ )
123
+
124
+ if (exportedNames.length === 0) {
125
+ return transformed
126
+ }
127
+
128
+ return `${transformed}\n${[...new Set(exportedNames)]
129
+ .map((name) => `exports.${name} = ${name}`)
130
+ .join('\n')}`
131
+ }
132
+
95
133
  const resolveImportTarget = (specifier: string, customRequire: NodeRequire): string => {
96
134
  try {
97
135
  const resolved = customRequire.resolve(specifier)
@@ -123,6 +161,82 @@ type SandboxContext = vm.Context & {
123
161
  __fb_dirname?: string
124
162
  }
125
163
 
164
+ type SandboxExecutionContext = ReturnType<typeof generateContextData>
165
+
166
+ const resolveModulePath = (specifier: string, parentFile: string): string | undefined => {
167
+ const parentDir = path.dirname(parentFile)
168
+ const basePath = path.resolve(parentDir, specifier)
169
+ const candidates = [
170
+ basePath,
171
+ `${basePath}.js`,
172
+ `${basePath}.ts`,
173
+ path.join(basePath, 'index.js'),
174
+ path.join(basePath, 'index.ts')
175
+ ]
176
+
177
+ return candidates.find((candidate) => {
178
+ try {
179
+ return fs.statSync(candidate).isFile()
180
+ } catch {
181
+ return false
182
+ }
183
+ })
184
+ }
185
+
186
+ const executeSandboxModule = ({
187
+ code,
188
+ contextData,
189
+ filePath,
190
+ moduleCache
191
+ }: {
192
+ code: string
193
+ contextData: SandboxExecutionContext
194
+ filePath: string
195
+ moduleCache: Map<string, unknown>
196
+ }): unknown => {
197
+ if (moduleCache.has(filePath)) {
198
+ return moduleCache.get(filePath)
199
+ }
200
+
201
+ const sandboxModule: SandboxModule = { exports: {} }
202
+ moduleCache.set(filePath, sandboxModule.exports)
203
+ const baseRequire = createRequire(filePath)
204
+
205
+ const localRequire = ((specifier: string) => {
206
+ if (specifier.startsWith('.') || specifier.startsWith('/')) {
207
+ const resolvedPath = resolveModulePath(specifier, filePath)
208
+ if (resolvedPath) {
209
+ return executeSandboxModule({
210
+ code: fs.readFileSync(resolvedPath, 'utf-8'),
211
+ contextData,
212
+ filePath: resolvedPath,
213
+ moduleCache
214
+ })
215
+ }
216
+ }
217
+
218
+ return baseRequire(specifier)
219
+ }) as NodeRequire
220
+
221
+ const vmContext = vm.createContext({
222
+ ...contextData,
223
+ require: localRequire,
224
+ exports: sandboxModule.exports,
225
+ module: sandboxModule,
226
+ __filename: filePath,
227
+ __dirname: path.dirname(filePath),
228
+ __fb_require: localRequire,
229
+ __fb_filename: filePath,
230
+ __fb_dirname: path.dirname(filePath)
231
+ }) as SandboxContext
232
+
233
+ vm.runInContext(transpileSandboxModule(code), vmContext, { filename: filePath })
234
+ sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
235
+ moduleCache.set(filePath, sandboxModule.exports)
236
+
237
+ return sandboxModule.exports
238
+ }
239
+
126
240
  const isExportedFunction = (value: unknown): value is ExportedFunction =>
127
241
  typeof value === 'function'
128
242
 
@@ -141,10 +255,28 @@ const resolveExport = (ctx: SandboxContext): ExportedFunction | undefined => {
141
255
  return getDefaultExport(moduleExports) ?? getDefaultExport(contextExports)
142
256
  }
143
257
 
144
- const buildVmContext = (contextData: ReturnType<typeof generateContextData>) => {
258
+ const buildVmContext = (
259
+ contextData: ReturnType<typeof generateContextData>,
260
+ currentFunction?: AppFunction
261
+ ) => {
145
262
  const sandboxModule: SandboxModule = { exports: {} }
146
- const entryFile = require.main?.filename ?? process.cwd()
147
- const customRequire = createRequire(entryFile)
263
+ const entryFile = currentFunction?.sourcePath ?? require.main?.filename ?? process.cwd()
264
+ const moduleCache = new Map<string, unknown>()
265
+ const customRequire = ((specifier: string) => {
266
+ if ((specifier.startsWith('.') || specifier.startsWith('/')) && currentFunction?.sourcePath) {
267
+ const resolvedPath = resolveModulePath(specifier, currentFunction.sourcePath)
268
+ if (resolvedPath) {
269
+ return executeSandboxModule({
270
+ code: fs.readFileSync(resolvedPath, 'utf-8'),
271
+ contextData,
272
+ filePath: resolvedPath,
273
+ moduleCache
274
+ })
275
+ }
276
+ }
277
+
278
+ return createRequire(entryFile)(specifier)
279
+ }) as NodeRequire
148
280
 
149
281
  const vmContext: SandboxContext = vm.createContext({
150
282
  ...contextData,
@@ -206,7 +338,10 @@ export async function GenerateContext({
206
338
  GenerateContextSync,
207
339
  request
208
340
  })
209
- const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(contextData)
341
+ const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(
342
+ contextData,
343
+ functionToRun
344
+ )
210
345
 
211
346
  const vmModules = vm as typeof vm & {
212
347
  SourceTextModule?: typeof vm.SourceTextModule
@@ -271,10 +406,7 @@ export async function GenerateContext({
271
406
  }
272
407
 
273
408
  if (!usedVmModules) {
274
- const codeToRun = functionToRun.code.includes('import ')
275
- ? transformImportsToRequire(functionToRun.code)
276
- : functionToRun.code
277
- vm.runInContext(codeToRun, vmContext)
409
+ vm.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile })
278
410
  }
279
411
 
280
412
  sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
@@ -323,12 +455,9 @@ export function GenerateContextSync({
323
455
  GenerateContextSync,
324
456
  request
325
457
  })
326
- const { sandboxModule, vmContext } = buildVmContext(contextData)
327
- const codeToRun = functionToRun.code.includes('import ')
328
- ? transformImportsToRequire(functionToRun.code)
329
- : functionToRun.code
458
+ const { sandboxModule, entryFile, vmContext } = buildVmContext(contextData, functionToRun)
330
459
 
331
- vm.runInContext(codeToRun, vmContext)
460
+ vm.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile })
332
461
  sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
333
462
  const fn = sandboxModule.exports as ExportedFunction
334
463
  if (deserializeArgs) {
@@ -7,6 +7,8 @@ import { PermissionExpression } from './interface'
7
7
  import { MachineContext } from './machines/interface'
8
8
 
9
9
  const functionsConditions = ['%%true', '%%false']
10
+ const andConditions = ['$and', '%and']
11
+ const orConditions = ['$or', '%or']
10
12
 
11
13
  const normalizeUserRole = (user?: MachineContext['user']) => {
12
14
  if (!user) return user
@@ -22,30 +24,121 @@ const normalizeUserRole = (user?: MachineContext['user']) => {
22
24
  : user
23
25
  }
24
26
 
25
- export const evaluateExpression = async (
27
+ const buildEvaluationContext = (
26
28
  params: MachineContext['params'],
27
- expression?: PermissionExpression,
28
29
  user?: MachineContext['user']
29
- ): Promise<boolean> => {
30
- if (!expression || typeof expression === 'boolean') return !!expression
30
+ ) => {
31
31
  const normalizedUser = normalizeUserRole(user)
32
32
 
33
- const value = {
34
- ...params.expansions,
35
- ...params.cursor,
33
+ return {
34
+ ...(params.expansions ?? {}),
35
+ ...(params.cursor ?? {}),
36
36
  '%%root': params.cursor,
37
37
  '%%prevRoot': params.expansions?.['%%prevRoot'],
38
38
  '%%user': normalizedUser,
39
39
  '%%true': true,
40
40
  '%%false': false
41
41
  }
42
+ }
43
+
44
+ const getFunctionCondition = (
45
+ expression: unknown
46
+ ): [string, Record<string, any>] | null => {
47
+ if (!expression || typeof expression !== 'object' || Array.isArray(expression)) {
48
+ return null
49
+ }
50
+
51
+ const entries = Object.entries(expression as Record<string, unknown>)
52
+ if (entries.length !== 1) {
53
+ return null
54
+ }
55
+
56
+ const [key, value] = entries[0]
57
+ if (!functionsConditions.includes(key)) {
58
+ return null
59
+ }
60
+
61
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
62
+ return null
63
+ }
64
+
65
+ return Object.prototype.hasOwnProperty.call(value, '%function')
66
+ ? [key, value as Record<string, any>]
67
+ : null
68
+ }
69
+
70
+ export const evaluateExpandedExpression = async (
71
+ expression: unknown,
72
+ params: MachineContext['params'],
73
+ user?: MachineContext['user']
74
+ ): Promise<boolean> => {
75
+ if (typeof expression === 'boolean') {
76
+ return expression
77
+ }
78
+
79
+ if (!expression || typeof expression !== 'object') {
80
+ return Boolean(expression)
81
+ }
82
+
83
+ const block = expression as Record<string, unknown>
84
+ const functionCondition = getFunctionCondition(block)
85
+
86
+ if (functionCondition) {
87
+ return evaluateComplexExpression(functionCondition, params, user)
88
+ }
89
+
90
+ const andKey = andConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key))
91
+ if (andKey) {
92
+ const conditions = Array.isArray(block[andKey]) ? (block[andKey] as unknown[]) : []
93
+ if (!conditions.length) return true
94
+
95
+ for (const condition of conditions) {
96
+ if (!(await evaluateExpandedExpression(condition, params, user))) {
97
+ return false
98
+ }
99
+ }
100
+
101
+ return true
102
+ }
103
+
104
+ const orKey = orConditions.find((key) => Object.prototype.hasOwnProperty.call(block, key))
105
+ if (orKey) {
106
+ const conditions = Array.isArray(block[orKey]) ? (block[orKey] as unknown[]) : []
107
+ if (!conditions.length) return true
108
+
109
+ for (const condition of conditions) {
110
+ if (await evaluateExpandedExpression(condition, params, user)) {
111
+ return true
112
+ }
113
+ }
114
+
115
+ return false
116
+ }
117
+
118
+ const keys = Object.keys(block)
119
+ if (keys.length > 1) {
120
+ for (const key of keys) {
121
+ if (!(await evaluateExpandedExpression({ [key]: block[key] }, params, user))) {
122
+ return false
123
+ }
124
+ }
125
+
126
+ return true
127
+ }
128
+
129
+ return rulesMatcherUtils.checkRule(block as never, buildEvaluationContext(params, user), {})
130
+ }
131
+
132
+ export const evaluateExpression = async (
133
+ params: MachineContext['params'],
134
+ expression?: PermissionExpression,
135
+ user?: MachineContext['user']
136
+ ): Promise<boolean> => {
137
+ if (!expression || typeof expression === 'boolean') return !!expression
138
+
139
+ const value = buildEvaluationContext(params, user)
42
140
  const conditions = expandQuery(expression, value)
43
- const complexCondition = Object.entries(conditions as Record<string, any>).find(
44
- ([key]) => functionsConditions.includes(key)
45
- )
46
- return complexCondition
47
- ? await evaluateComplexExpression(complexCondition, params, normalizedUser)
48
- : rulesMatcherUtils.checkRule(conditions, value, {})
141
+ return evaluateExpandedExpression(conditions, params, user)
49
142
  }
50
143
 
51
144
  const evaluateComplexExpression = async (
@@ -74,7 +167,7 @@ const evaluateComplexExpression = async (
74
167
  const expandedArguments =
75
168
  fnArguments && fnArguments.length
76
169
  ? ((expandQuery({ args: fnArguments }, expansionContext) as { args: unknown[] })
77
- .args ?? [])
170
+ .args ?? [])
78
171
  : [params.cursor]
79
172
 
80
173
  const response = await GenerateContext({
@@ -2,6 +2,7 @@ import { Document, OptionalId } from 'mongodb'
2
2
  import { User } from '../../../auth/dtos'
3
3
  import { Filter } from '../../../features/rules/interface'
4
4
  import { getValidRule } from '../../../services/mongodb-atlas/utils'
5
+ import { evaluateExpression } from '../helpers'
5
6
  import { Role } from '../interface'
6
7
  import { LogMachineInfoParams } from './interface'
7
8
 
@@ -28,6 +29,20 @@ export const getWinningRole = (
28
29
  return null
29
30
  }
30
31
 
32
+ export const getWinningRoleAsync = async (
33
+ document: OptionalId<Document> | null,
34
+ user: User,
35
+ roles: Role[] = []
36
+ ): Promise<Role | null> => {
37
+ if (!roles.length) return null
38
+ for (const role of roles) {
39
+ if (await checkApplyWhenAsync(role.apply_when, user, document)) {
40
+ return role
41
+ }
42
+ }
43
+ return null
44
+ }
45
+
31
46
  /**
32
47
  * Checks if the `apply_when` condition is valid for the given user and document.
33
48
  *
@@ -50,6 +65,25 @@ export const checkApplyWhen = (
50
65
  return !!validRule.length
51
66
  }
52
67
 
68
+ export const checkApplyWhenAsync = async (
69
+ apply_when: Role['apply_when'],
70
+ user: User,
71
+ document: OptionalId<Document> | null
72
+ ) => {
73
+ return evaluateExpression(
74
+ {
75
+ type: 'read',
76
+ roles: [],
77
+ cursor: document,
78
+ expansions: {
79
+ '%%prevRoot': undefined
80
+ }
81
+ },
82
+ apply_when,
83
+ user
84
+ )
85
+ }
86
+
53
87
  /**
54
88
  * Logs machine step information if logging is enabled.
55
89
  *