@flowerforce/flowerbase 1.2.1-beta.2 → 1.2.1-beta.20
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.
- package/README.md +37 -6
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +55 -4
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +52 -6
- package/dist/auth/providers/anon-user/controller.d.ts +8 -0
- package/dist/auth/providers/anon-user/controller.d.ts.map +1 -0
- package/dist/auth/providers/anon-user/controller.js +90 -0
- package/dist/auth/providers/anon-user/dtos.d.ts +10 -0
- package/dist/auth/providers/anon-user/dtos.d.ts.map +1 -0
- package/dist/auth/providers/anon-user/dtos.js +2 -0
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +35 -25
- package/dist/auth/providers/custom-function/dtos.d.ts +4 -1
- package/dist/auth/providers/custom-function/dtos.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +159 -73
- package/dist/auth/providers/local-userpass/dtos.d.ts +17 -2
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +76 -14
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +55 -61
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -4
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +31 -12
- package/dist/features/functions/dtos.d.ts +3 -0
- package/dist/features/functions/dtos.d.ts.map +1 -1
- package/dist/features/functions/interface.d.ts +3 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.d.ts +3 -2
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +19 -7
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +49 -7
- package/dist/features/triggers/interface.d.ts +1 -0
- package/dist/features/triggers/interface.d.ts.map +1 -1
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +67 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -13
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +72 -2
- package/dist/services/mongodb-atlas/model.d.ts +3 -2
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +66 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts +2 -1
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/shared/models/handleUserRegistration.model.js +1 -0
- package/dist/utils/context/helpers.d.ts +6 -6
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/index.d.ts +1 -1
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +176 -9
- package/dist/utils/context/interface.d.ts +1 -1
- package/dist/utils/context/interface.d.ts.map +1 -1
- package/dist/utils/crypto/index.d.ts +1 -0
- package/dist/utils/crypto/index.d.ts.map +1 -1
- package/dist/utils/crypto/index.js +6 -2
- package/dist/utils/initializer/exposeRoutes.js +1 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +12 -4
- package/dist/utils/roles/helpers.js +2 -1
- package/package.json +1 -2
- package/src/auth/controller.ts +71 -5
- package/src/auth/plugins/jwt.test.ts +93 -0
- package/src/auth/plugins/jwt.ts +67 -8
- package/src/auth/providers/anon-user/controller.ts +91 -0
- package/src/auth/providers/anon-user/dtos.ts +10 -0
- package/src/auth/providers/custom-function/controller.ts +40 -31
- package/src/auth/providers/custom-function/dtos.ts +5 -1
- package/src/auth/providers/local-userpass/controller.ts +211 -101
- package/src/auth/providers/local-userpass/dtos.ts +20 -2
- package/src/auth/utils.ts +66 -83
- package/src/constants.ts +14 -2
- package/src/features/functions/controller.ts +42 -12
- package/src/features/functions/dtos.ts +3 -0
- package/src/features/functions/interface.ts +3 -0
- package/src/features/functions/utils.ts +29 -8
- package/src/features/triggers/index.ts +44 -1
- package/src/features/triggers/interface.ts +1 -0
- package/src/features/triggers/utils.ts +89 -37
- package/src/index.ts +49 -13
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
- package/src/services/mongodb-atlas/index.ts +100 -2
- package/src/services/mongodb-atlas/model.ts +16 -3
- package/src/shared/handleUserRegistration.ts +83 -2
- package/src/shared/models/handleUserRegistration.model.ts +2 -1
- package/src/utils/__tests__/registerPlugins.test.ts +5 -1
- package/src/utils/context/index.ts +238 -18
- package/src/utils/context/interface.ts +1 -1
- package/src/utils/crypto/index.ts +5 -1
- package/src/utils/initializer/exposeRoutes.ts +1 -1
- package/src/utils/initializer/registerPlugins.ts +8 -0
- package/src/utils/roles/helpers.ts +3 -2
|
@@ -1,10 +1,116 @@
|
|
|
1
1
|
import { createRequire } from 'node:module'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
2
4
|
import vm from 'vm'
|
|
3
5
|
import { EJSON } from 'bson'
|
|
4
6
|
import { StateManager } from '../../state'
|
|
5
7
|
import { generateContextData } from './helpers'
|
|
6
8
|
import { GenerateContextParams } from './interface'
|
|
7
9
|
|
|
10
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)') as (
|
|
11
|
+
specifier: string
|
|
12
|
+
) => Promise<Record<string, unknown>>
|
|
13
|
+
|
|
14
|
+
const transformImportsToRequire = (code: string): string => {
|
|
15
|
+
let importIndex = 0
|
|
16
|
+
const lines = code.split(/\r?\n/)
|
|
17
|
+
|
|
18
|
+
return lines
|
|
19
|
+
.map((line) => {
|
|
20
|
+
const trimmed = line.trim()
|
|
21
|
+
|
|
22
|
+
if (/^import\s+type\s+/.test(trimmed)) {
|
|
23
|
+
return ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sideEffectMatch = trimmed.match(/^import\s+['"]([^'"]+)['"]\s*;?$/)
|
|
27
|
+
if (sideEffectMatch) {
|
|
28
|
+
return `require('${sideEffectMatch[1]}')`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const match = trimmed.match(/^import\s+(.+?)\s+from\s+['"]([^'"]+)['"]\s*;?$/)
|
|
32
|
+
if (!match) return line
|
|
33
|
+
|
|
34
|
+
const [, importClause, source] = match
|
|
35
|
+
const clause = importClause.trim()
|
|
36
|
+
|
|
37
|
+
if (clause.startsWith('{') && clause.endsWith('}')) {
|
|
38
|
+
const named = clause.slice(1, -1).trim()
|
|
39
|
+
return `const { ${named} } = require('${source}')`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const namespaceMatch = clause.match(/^\*\s+as\s+(\w+)$/)
|
|
43
|
+
if (namespaceMatch) {
|
|
44
|
+
return `const ${namespaceMatch[1]} = require('${source}')`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (clause.includes(',')) {
|
|
48
|
+
const [defaultPart, restRaw] = clause.split(',', 2)
|
|
49
|
+
const defaultName = defaultPart.trim()
|
|
50
|
+
const rest = restRaw.trim()
|
|
51
|
+
const tmpName = `__fb_import_${importIndex++}`
|
|
52
|
+
const linesOut = [`const ${tmpName} = require('${source}')`]
|
|
53
|
+
|
|
54
|
+
if (defaultName) {
|
|
55
|
+
linesOut.push(`const ${defaultName} = ${tmpName}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (rest.startsWith('{') && rest.endsWith('}')) {
|
|
59
|
+
const named = rest.slice(1, -1).trim()
|
|
60
|
+
linesOut.push(`const { ${named} } = ${tmpName}`)
|
|
61
|
+
} else {
|
|
62
|
+
const nsMatch = rest.match(/^\*\s+as\s+(\w+)$/)
|
|
63
|
+
if (nsMatch) {
|
|
64
|
+
linesOut.push(`const ${nsMatch[1]} = ${tmpName}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return linesOut.join('\n')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return `const ${clause} = require('${source}')`
|
|
72
|
+
})
|
|
73
|
+
.join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const wrapEsmModule = (code: string): string => {
|
|
77
|
+
const prelude = [
|
|
78
|
+
'const __fb_module = { exports: {} };',
|
|
79
|
+
'let exports = __fb_module.exports;',
|
|
80
|
+
'let module = __fb_module;',
|
|
81
|
+
'const __fb_require = globalThis.__fb_require;',
|
|
82
|
+
'const require = __fb_require;',
|
|
83
|
+
'const __filename = globalThis.__fb_filename;',
|
|
84
|
+
'const __dirname = globalThis.__fb_dirname;'
|
|
85
|
+
].join('\n')
|
|
86
|
+
|
|
87
|
+
const trailer = [
|
|
88
|
+
'globalThis.__fb_module = __fb_module;',
|
|
89
|
+
'globalThis.__fb_exports = exports;'
|
|
90
|
+
].join('\n')
|
|
91
|
+
|
|
92
|
+
return `${prelude}\n${code}\n${trailer}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const resolveImportTarget = (specifier: string, customRequire: NodeRequire): string => {
|
|
96
|
+
try {
|
|
97
|
+
const resolved = customRequire.resolve(specifier)
|
|
98
|
+
if (resolved.startsWith('node:')) return resolved
|
|
99
|
+
if (path.isAbsolute(resolved)) {
|
|
100
|
+
return pathToFileURL(resolved).href
|
|
101
|
+
}
|
|
102
|
+
return resolved
|
|
103
|
+
} catch {
|
|
104
|
+
return specifier
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const shouldFallbackFromVmModules = (error: unknown): boolean => {
|
|
109
|
+
if (!error || typeof error !== 'object') return false
|
|
110
|
+
const code = (error as { code?: string }).code
|
|
111
|
+
return code === 'ERR_VM_MODULES_DISABLED' || code === 'ERR_VM_MODULES_NOT_SUPPORTED'
|
|
112
|
+
}
|
|
113
|
+
|
|
8
114
|
/**
|
|
9
115
|
* > Used to generate the current context
|
|
10
116
|
* @testable
|
|
@@ -28,7 +134,7 @@ export async function GenerateContext({
|
|
|
28
134
|
deserializeArgs = true,
|
|
29
135
|
enqueue,
|
|
30
136
|
request
|
|
31
|
-
}: GenerateContextParams) {
|
|
137
|
+
}: GenerateContextParams): Promise<unknown> {
|
|
32
138
|
if (!currentFunction) return
|
|
33
139
|
|
|
34
140
|
const functionsQueue = StateManager.select("functionsQueue")
|
|
@@ -48,28 +154,142 @@ export async function GenerateContext({
|
|
|
48
154
|
request
|
|
49
155
|
})
|
|
50
156
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
157
|
+
type ExportedFunction = (...args: unknown[]) => unknown
|
|
158
|
+
type SandboxModule = { exports: unknown }
|
|
159
|
+
type SandboxContext = vm.Context & {
|
|
160
|
+
exports?: unknown
|
|
161
|
+
module?: SandboxModule
|
|
162
|
+
__fb_module?: SandboxModule
|
|
163
|
+
__fb_exports?: unknown
|
|
164
|
+
__fb_require?: NodeRequire
|
|
165
|
+
__fb_filename?: string
|
|
166
|
+
__fb_dirname?: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const isExportedFunction = (value: unknown): value is ExportedFunction =>
|
|
170
|
+
typeof value === 'function'
|
|
171
|
+
|
|
172
|
+
const getDefaultExport = (value: unknown): ExportedFunction | undefined => {
|
|
173
|
+
if (!value || typeof value !== 'object') return undefined
|
|
174
|
+
if (!('default' in value)) return undefined
|
|
175
|
+
const maybeDefault = (value as { default?: unknown }).default
|
|
176
|
+
return isExportedFunction(maybeDefault) ? maybeDefault : undefined
|
|
62
177
|
}
|
|
63
|
-
|
|
64
|
-
|
|
178
|
+
|
|
179
|
+
const resolveExport = (ctx: SandboxContext): ExportedFunction | undefined => {
|
|
180
|
+
const moduleExports = ctx.module?.exports ?? ctx.__fb_module?.exports
|
|
181
|
+
if (isExportedFunction(moduleExports)) return moduleExports
|
|
182
|
+
const contextExports = ctx.exports ?? ctx.__fb_exports
|
|
183
|
+
if (isExportedFunction(contextExports)) return contextExports
|
|
184
|
+
return getDefaultExport(moduleExports) ?? getDefaultExport(contextExports)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const sandboxModule: SandboxModule = { exports: {} }
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const entryFile = require.main?.filename ?? process.cwd()
|
|
191
|
+
const customRequire = createRequire(entryFile)
|
|
192
|
+
|
|
193
|
+
const vmContext: SandboxContext = vm.createContext({
|
|
194
|
+
...contextData,
|
|
195
|
+
require: customRequire,
|
|
196
|
+
exports: sandboxModule.exports,
|
|
197
|
+
module: sandboxModule,
|
|
198
|
+
__filename,
|
|
199
|
+
__dirname,
|
|
200
|
+
__fb_require: customRequire,
|
|
201
|
+
__fb_filename: __filename,
|
|
202
|
+
__fb_dirname: __dirname
|
|
203
|
+
}) as SandboxContext
|
|
204
|
+
|
|
205
|
+
const vmModules = vm as typeof vm & {
|
|
206
|
+
SourceTextModule?: typeof vm.SourceTextModule
|
|
207
|
+
SyntheticModule?: typeof vm.SyntheticModule
|
|
208
|
+
}
|
|
209
|
+
const hasStaticImport = /\bimport\s+/.test(functionToRun.code)
|
|
210
|
+
let usedVmModules = false
|
|
211
|
+
|
|
212
|
+
if (hasStaticImport && vmModules.SourceTextModule && vmModules.SyntheticModule) {
|
|
213
|
+
try {
|
|
214
|
+
const moduleCache = new Map<string, vm.Module>()
|
|
215
|
+
|
|
216
|
+
const loadModule = async (specifier: string): Promise<vm.Module> => {
|
|
217
|
+
const importTarget = resolveImportTarget(specifier, customRequire)
|
|
218
|
+
const cached = moduleCache.get(importTarget)
|
|
219
|
+
if (cached) return cached
|
|
220
|
+
|
|
221
|
+
const namespace = await dynamicImport(importTarget)
|
|
222
|
+
const exportNames = Object.keys(namespace)
|
|
223
|
+
if ('default' in namespace && !exportNames.includes('default')) {
|
|
224
|
+
exportNames.push('default')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const syntheticModule = new vmModules.SyntheticModule(
|
|
228
|
+
exportNames,
|
|
229
|
+
function () {
|
|
230
|
+
for (const name of exportNames) {
|
|
231
|
+
this.setExport(name, namespace[name])
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{ context: vmContext, identifier: importTarget }
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
moduleCache.set(importTarget, syntheticModule)
|
|
238
|
+
return syntheticModule
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const importModuleDynamically =
|
|
242
|
+
((specifier: string) => loadModule(specifier) as unknown as vm.Module) as unknown as
|
|
243
|
+
vm.SourceTextModuleOptions['importModuleDynamically']
|
|
244
|
+
|
|
245
|
+
const sourceModule = new vmModules.SourceTextModule(
|
|
246
|
+
wrapEsmModule(functionToRun.code),
|
|
247
|
+
{
|
|
248
|
+
context: vmContext,
|
|
249
|
+
identifier: entryFile,
|
|
250
|
+
initializeImportMeta: (meta) => {
|
|
251
|
+
meta.url = pathToFileURL(entryFile).href
|
|
252
|
+
},
|
|
253
|
+
importModuleDynamically
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
await sourceModule.link(loadModule)
|
|
258
|
+
await sourceModule.evaluate()
|
|
259
|
+
usedVmModules = true
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (!shouldFallbackFromVmModules(error)) {
|
|
262
|
+
throw error
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!usedVmModules) {
|
|
268
|
+
const codeToRun = functionToRun.code.includes('import ')
|
|
269
|
+
? transformImportsToRequire(functionToRun.code)
|
|
270
|
+
: functionToRun.code
|
|
271
|
+
vm.runInContext(codeToRun, vmContext)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(error)
|
|
277
|
+
throw error
|
|
65
278
|
}
|
|
66
279
|
|
|
67
280
|
if (deserializeArgs) {
|
|
68
|
-
return await
|
|
281
|
+
return await (sandboxModule.exports as ExportedFunction)(
|
|
282
|
+
...EJSON.deserialize(args)
|
|
283
|
+
)
|
|
69
284
|
}
|
|
70
285
|
|
|
71
|
-
return await
|
|
286
|
+
return await (sandboxModule.exports as ExportedFunction)(...args)
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const res = await functionsQueue.add(run, enqueue)
|
|
290
|
+
return res
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(error)
|
|
293
|
+
throw error
|
|
72
294
|
}
|
|
73
|
-
const res = await functionsQueue.add(run, enqueue)
|
|
74
|
-
return res
|
|
75
295
|
}
|
|
@@ -20,5 +20,5 @@ export interface GenerateContextParams {
|
|
|
20
20
|
|
|
21
21
|
type ContextRequest = Pick<FastifyRequest, "ips" | "host" | "hostname" | "url" | "method" | "ip" | "id">
|
|
22
22
|
export interface GenerateContextDataParams extends Omit<GenerateContextParams, 'args'> {
|
|
23
|
-
GenerateContext: (params: GenerateContextParams) => Promise<
|
|
23
|
+
GenerateContext: (params: GenerateContextParams) => Promise<unknown>
|
|
24
24
|
}
|
|
@@ -36,6 +36,10 @@ export const comparePassword = async (plaintext: string, storedPassword: string)
|
|
|
36
36
|
* > Generate a random token
|
|
37
37
|
* @param length -> the token length
|
|
38
38
|
*/
|
|
39
|
-
export const generateToken = (length =
|
|
39
|
+
export const generateToken = (length = 64) => {
|
|
40
40
|
return crypto.randomBytes(length).toString('hex')
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
export const hashToken = (token: string) => {
|
|
44
|
+
return crypto.createHash('sha256').update(token).digest('hex')
|
|
45
|
+
}
|
|
@@ -18,7 +18,7 @@ export const exposeRoutes = async (fastify: FastifyInstance) => {
|
|
|
18
18
|
const headerHost = req.headers.host ?? 'localhost:3000'
|
|
19
19
|
const hostname = headerHost.split(':')[0]
|
|
20
20
|
const port = DEFAULT_CONFIG?.PORT ?? 3000
|
|
21
|
-
const host = `${hostname}:${port}`
|
|
21
|
+
const host = port === 8080 ? hostname : `${hostname}:${port}`
|
|
22
22
|
const wsSchema = 'wss'
|
|
23
23
|
|
|
24
24
|
return {
|
|
@@ -5,6 +5,7 @@ import fastifyRawBody from 'fastify-raw-body'
|
|
|
5
5
|
import { CorsConfig } from '../../'
|
|
6
6
|
import { authController } from '../../auth/controller'
|
|
7
7
|
import jwtAuthPlugin from '../../auth/plugins/jwt'
|
|
8
|
+
import { anonUserController } from '../../auth/providers/anon-user/controller'
|
|
8
9
|
import { customFunctionController } from '../../auth/providers/custom-function/controller'
|
|
9
10
|
import { localUserPassController } from '../../auth/providers/local-userpass/controller'
|
|
10
11
|
import { API_VERSION } from '../../constants'
|
|
@@ -133,6 +134,13 @@ const getRegisterConfig = async ({
|
|
|
133
134
|
options: {
|
|
134
135
|
prefix: `${API_VERSION}/app/:appId/auth/providers/custom-function`
|
|
135
136
|
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
pluginName: 'anonUserController',
|
|
140
|
+
plugin: anonUserController,
|
|
141
|
+
options: {
|
|
142
|
+
prefix: `${API_VERSION}/app/:appId/auth/providers/anon-user`
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
] as RegisterConfig[]
|
|
138
146
|
}
|
|
@@ -34,7 +34,7 @@ const evaluateComplexExpression = async (
|
|
|
34
34
|
condition: [string, Record<string, any>],
|
|
35
35
|
params: MachineContext['params'],
|
|
36
36
|
user: MachineContext['user']
|
|
37
|
-
) => {
|
|
37
|
+
): Promise<boolean> => {
|
|
38
38
|
const [key, config] = condition
|
|
39
39
|
|
|
40
40
|
const functionConfig = config['%function']
|
|
@@ -67,5 +67,6 @@ const evaluateComplexExpression = async (
|
|
|
67
67
|
functionsList,
|
|
68
68
|
services
|
|
69
69
|
})
|
|
70
|
-
|
|
70
|
+
const isTruthy = Boolean(response)
|
|
71
|
+
return key === '%%true' ? isTruthy : !isTruthy
|
|
71
72
|
}
|