@flowerforce/flowerbase 1.7.6-beta.0 → 1.7.6-beta.2

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 (70) hide show
  1. package/README.md +125 -1
  2. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  3. package/dist/auth/providers/custom-function/controller.js +3 -8
  4. package/dist/constants.d.ts +10 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +11 -1
  7. package/dist/features/encryption/interface.d.ts +36 -0
  8. package/dist/features/encryption/interface.d.ts.map +1 -0
  9. package/dist/features/encryption/interface.js +2 -0
  10. package/dist/features/encryption/utils.d.ts +9 -0
  11. package/dist/features/encryption/utils.d.ts.map +1 -0
  12. package/dist/features/encryption/utils.js +34 -0
  13. package/dist/features/functions/controller.d.ts +2 -0
  14. package/dist/features/functions/controller.d.ts.map +1 -1
  15. package/dist/features/functions/controller.js +7 -1
  16. package/dist/features/rules/utils.d.ts.map +1 -1
  17. package/dist/features/rules/utils.js +1 -11
  18. package/dist/features/triggers/index.d.ts.map +1 -1
  19. package/dist/features/triggers/index.js +4 -0
  20. package/dist/features/triggers/utils.d.ts.map +1 -1
  21. package/dist/features/triggers/utils.js +30 -38
  22. package/dist/index.d.ts +3 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -4
  25. package/dist/monitoring/plugin.d.ts.map +1 -1
  26. package/dist/monitoring/plugin.js +31 -0
  27. package/dist/services/mongodb-atlas/index.d.ts +3 -0
  28. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  29. package/dist/services/mongodb-atlas/index.js +97 -17
  30. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  31. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  32. package/dist/utils/index.d.ts +1 -0
  33. package/dist/utils/index.d.ts.map +1 -1
  34. package/dist/utils/index.js +14 -3
  35. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  36. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  37. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  38. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  39. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  40. package/dist/utils/initializer/registerPlugins.js +27 -5
  41. package/package.json +4 -2
  42. package/src/auth/providers/custom-function/controller.ts +4 -10
  43. package/src/constants.ts +11 -2
  44. package/src/features/encryption/interface.ts +46 -0
  45. package/src/features/encryption/utils.ts +22 -0
  46. package/src/features/functions/__tests__/watch-filter.test.ts +11 -1
  47. package/src/features/functions/controller.ts +8 -0
  48. package/src/features/rules/utils.ts +1 -11
  49. package/src/features/triggers/index.ts +5 -1
  50. package/src/features/triggers/utils.ts +31 -42
  51. package/src/index.ts +10 -2
  52. package/src/monitoring/plugin.ts +33 -0
  53. package/src/monitoring/ui.collections.js +7 -10
  54. package/src/monitoring/ui.css +378 -0
  55. package/src/monitoring/ui.endpoints.js +5 -10
  56. package/src/monitoring/ui.events.js +2 -4
  57. package/src/monitoring/ui.functions.js +64 -71
  58. package/src/monitoring/ui.html +8 -0
  59. package/src/monitoring/ui.js +189 -0
  60. package/src/monitoring/ui.shared.js +237 -2
  61. package/src/monitoring/ui.triggers.js +2 -3
  62. package/src/monitoring/ui.users.js +5 -9
  63. package/src/services/mongodb-atlas/__tests__/watch-filter.test.ts +78 -0
  64. package/src/services/mongodb-atlas/index.ts +102 -19
  65. package/src/services/mongodb-atlas/model.ts +3 -1
  66. package/src/types/fastify-raw-body.d.ts +0 -9
  67. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  68. package/src/utils/index.ts +12 -1
  69. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  70. package/src/utils/initializer/registerPlugins.ts +45 -10
@@ -0,0 +1,105 @@
1
+ import { UUID } from 'mongodb'
2
+ import type { EncryptionSchemas } from '../../features/encryption/interface'
3
+ import { buildSchemaMap } from '../initializer/mongodbCSFLE'
4
+
5
+ describe('buildSchemaMap', () => {
6
+ const genericSchemas: EncryptionSchemas = {
7
+ 'appDb.records': {
8
+ bsonType: 'object',
9
+ encryptMetadata: {
10
+ keyAlias: 'root-key'
11
+ },
12
+ properties: {
13
+ publicText: {
14
+ encrypt: {
15
+ bsonType: 'string',
16
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'
17
+ }
18
+ },
19
+ protectedText: {
20
+ encrypt: {
21
+ bsonType: 'string',
22
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic',
23
+ keyAlias: 'root-key'
24
+ }
25
+ },
26
+ nestedObject: {
27
+ bsonType: 'object',
28
+ encryptMetadata: { keyAlias: 'nested-key' },
29
+ properties: {
30
+ deepObject: {
31
+ bsonType: 'object',
32
+ properties: {
33
+ deepSecret: {
34
+ encrypt: {
35
+ bsonType: 'string',
36
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random',
37
+ keyAlias: 'deep-key'
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ it('resolves keyAlias to keyId for root and nested schemas', () => {
49
+ const rootKeyId = new UUID()
50
+ const nestedKeyId = new UUID()
51
+ const deepKeyId = new UUID()
52
+
53
+ const schemaMap = buildSchemaMap(genericSchemas, [
54
+ { dataKeyAlias: 'root-key', dataKeyId: rootKeyId },
55
+ { dataKeyAlias: 'nested-key', dataKeyId: nestedKeyId },
56
+ { dataKeyAlias: 'deep-key', dataKeyId: deepKeyId }
57
+ ])
58
+
59
+ expect(schemaMap['appDb.records']).toEqual({
60
+ bsonType: 'object',
61
+ encryptMetadata: { keyId: [rootKeyId] },
62
+ properties: {
63
+ publicText: {
64
+ encrypt: {
65
+ bsonType: 'string',
66
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'
67
+ }
68
+ },
69
+ protectedText: {
70
+ encrypt: {
71
+ bsonType: 'string',
72
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic',
73
+ keyId: [rootKeyId]
74
+ }
75
+ },
76
+ nestedObject: {
77
+ bsonType: 'object',
78
+ encryptMetadata: { keyId: [nestedKeyId] },
79
+ properties: {
80
+ deepObject: {
81
+ bsonType: 'object',
82
+ properties: {
83
+ deepSecret: {
84
+ encrypt: {
85
+ bsonType: 'string',
86
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random',
87
+ keyId: [deepKeyId]
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ })
96
+ })
97
+
98
+ it('throws when nested keyAlias cannot be resolved', () => {
99
+ const rootKeyId = new UUID()
100
+
101
+ expect(() =>
102
+ buildSchemaMap(genericSchemas, [{ dataKeyAlias: 'root-key', dataKeyId: rootKeyId }])
103
+ ).toThrow('Key with alias deep-key could not be found in the Key Vault.')
104
+ })
105
+ })
@@ -1,5 +1,16 @@
1
- import fs from 'fs'
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
2
3
 
3
4
  export const readFileContent = (filePath: string) => fs.readFileSync(filePath, 'utf-8')
4
5
  export const readJsonContent = (filePath: string) =>
5
6
  JSON.parse(readFileContent(filePath)) as unknown
7
+
8
+ export const recursivelyCollectFiles = (dir: string): string[] => {
9
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
10
+ const fullPath = path.join(dir, entry.name)
11
+ if (entry.isDirectory()) {
12
+ return recursivelyCollectFiles(fullPath)
13
+ }
14
+ return entry.isFile() ? [fullPath] : []
15
+ })
16
+ }
@@ -0,0 +1,224 @@
1
+ import {
2
+ ClientEncryption,
3
+ MongoClient,
4
+ UUID,
5
+ Binary,
6
+ type Document,
7
+ type AWSEncryptionKeyOptions,
8
+ type AWSKMSProviderConfiguration,
9
+ type AzureEncryptionKeyOptions,
10
+ type AzureKMSProviderConfiguration,
11
+ type GCPKMSProviderConfiguration,
12
+ type GCPEncryptionKeyOptions,
13
+ type KMIPKMSProviderConfiguration,
14
+ type KMIPEncryptionKeyOptions,
15
+ type LocalKMSProviderConfiguration,
16
+ type AutoEncryptionExtraOptions,
17
+ type KMSProviders,
18
+ type AutoEncryptionOptions,
19
+ } from "mongodb";
20
+ import { EncryptionSchemaProperty, MappedEncryptionSchema, MappedEncryptionSchemaProperty, type EncryptionSchemas } from "../../features/encryption/interface";
21
+ import { DEFAULT_CONFIG } from "../../constants";
22
+
23
+ type KMSProviderConfig =
24
+ | {
25
+ /**
26
+ * The alias of the key. It must be referenced in the schema map
27
+ * to select which key to use for encryption.
28
+ */
29
+ keyAlias: string
30
+ /**
31
+ * KMS Provider name.
32
+ */
33
+ provider: "aws"
34
+ /**
35
+ * KMS Provider specific authorization configuration.
36
+ */
37
+ config: AWSKMSProviderConfiguration
38
+ /**
39
+ * Configuration of the master key.
40
+ */
41
+ masterKey: AWSEncryptionKeyOptions
42
+ } | {
43
+ keyAlias: string
44
+ provider: "azure"
45
+ config: AzureKMSProviderConfiguration,
46
+ masterKey: AzureEncryptionKeyOptions
47
+ } | {
48
+ keyAlias: string
49
+ provider: "gcp"
50
+ config: GCPKMSProviderConfiguration,
51
+ masterKey: GCPEncryptionKeyOptions
52
+ } | {
53
+ keyAlias: string
54
+ provider: "kmip"
55
+ config: KMIPKMSProviderConfiguration,
56
+ masterKey: KMIPEncryptionKeyOptions
57
+ } | {
58
+ keyAlias: string
59
+ provider: "local"
60
+ config: LocalKMSProviderConfiguration,
61
+ }
62
+
63
+ export type MongoDbEncryptionConfig = {
64
+ kmsProviders: KMSProviderConfig[],
65
+ /**
66
+ * The Key Vault database name
67
+ * @default encryption
68
+ */
69
+ keyVaultDb?: string
70
+ /**
71
+ * The Key Vault database collection
72
+ * @default __keyVault
73
+ */
74
+ keyVaultCollection?: string
75
+ extraOptions?: AutoEncryptionExtraOptions
76
+ }
77
+
78
+ /**
79
+ * @internal
80
+ */
81
+ type RequiredConfig = Required<Omit<MongoDbEncryptionConfig, "extraOptions">>
82
+
83
+ type DataKey = { dataKeyId: UUID, dataKeyAlias: string }
84
+
85
+ async function ensureUniqueKeyAltNameIndex(db: ReturnType<MongoClient['db']>, config: RequiredConfig): Promise<void> {
86
+ await db.collection(config.keyVaultCollection).createIndex(
87
+ { keyAltNames: 1 },
88
+ {
89
+ unique: true,
90
+ partialFilterExpression: { keyAltNames: { $exists: true } },
91
+ }
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Ensure provided KMS Providers DEK keys exist in the key vault. If not, they are created.
97
+ */
98
+ async function ensureDataEncryptionKeys(
99
+ clientEncryption: ClientEncryption,
100
+ keyVaultDb: ReturnType<MongoClient['db']>,
101
+ config: RequiredConfig
102
+ ): Promise<DataKey[]> {
103
+ const keys: DataKey[] = []
104
+
105
+ for (const kmsProvider of config.kmsProviders) {
106
+ const existingKey = await keyVaultDb.collection(config.keyVaultCollection).findOne({
107
+ keyAltNames: kmsProvider.keyAlias,
108
+ });
109
+
110
+ if (existingKey?._id instanceof Binary) {
111
+ keys.push({ dataKeyId: existingKey._id, dataKeyAlias: kmsProvider.keyAlias })
112
+ continue
113
+ }
114
+
115
+ const dataKeyId = await clientEncryption.createDataKey(kmsProvider.provider, {
116
+ masterKey: "masterKey" in kmsProvider ? kmsProvider.masterKey : undefined,
117
+ keyAltNames: [kmsProvider.keyAlias],
118
+ });
119
+ console.log(`[MongoDB Encryption] Created new key with alias ${kmsProvider.keyAlias}`)
120
+ keys.push({ dataKeyId, dataKeyAlias: kmsProvider.keyAlias })
121
+ }
122
+
123
+ return keys
124
+ }
125
+
126
+ /**
127
+ * Recursively resolve key aliases in an encryption schema to their corresponding key IDs.
128
+ */
129
+ const resolveKeyAliases = (schema: EncryptionSchemaProperty, dataKeys: DataKey[]): MappedEncryptionSchemaProperty => {
130
+ if ("encrypt" in schema) {
131
+ if (!schema.encrypt.keyAlias) {
132
+ return schema
133
+ }
134
+ const keyId = dataKeys.find(k => k.dataKeyAlias === schema.encrypt.keyAlias)?.dataKeyId
135
+ if (!keyId) {
136
+ throw new Error(`Key with alias ${schema.encrypt.keyAlias} could not be found in the Key Vault.`)
137
+ }
138
+ return {
139
+ encrypt: {
140
+ bsonType: schema.encrypt.bsonType,
141
+ algorithm: schema.encrypt.algorithm,
142
+ keyId: [keyId]
143
+ }
144
+ }
145
+ }
146
+ const mappedSchema: MappedEncryptionSchema = {
147
+ bsonType: "object",
148
+ properties: Object.entries(schema.properties).reduce((acc, [property, config]) => {
149
+ acc[property] = resolveKeyAliases(config, dataKeys)
150
+ return acc
151
+ }, {} as Record<string, MappedEncryptionSchemaProperty>)
152
+ }
153
+
154
+ if (schema.encryptMetadata) {
155
+ const keyId = dataKeys.find(k => k.dataKeyAlias === schema.encryptMetadata!.keyAlias)?.dataKeyId
156
+ if (!keyId) {
157
+ throw new Error(`Key with alias ${schema.encryptMetadata.keyAlias} could not be found in the Key Vault.`)
158
+ }
159
+ mappedSchema.encryptMetadata = { keyId: [keyId] }
160
+ }
161
+
162
+ return mappedSchema
163
+ }
164
+
165
+ export const buildSchemaMap = (schemas: EncryptionSchemas, dataKeys: DataKey[]) => {
166
+ return Object.entries(schemas).reduce((acc, [key, schema]) => {
167
+ acc[key] = resolveKeyAliases(schema, dataKeys)
168
+ return acc
169
+ }, {} as Record<string, Document>)
170
+ }
171
+
172
+ /**
173
+ * Setup MongoDB Client-Side Field Level Encryption (CSFLE).
174
+ * @see https://www.mongodb.com/docs/manual/core/csfle
175
+ */
176
+ export const setupMongoDbCSFLE = async (
177
+ config: MongoDbEncryptionConfig & { mongodbUrl: string; schemas?: EncryptionSchemas }
178
+ ): Promise<AutoEncryptionOptions> => {
179
+ if (config.kmsProviders.length === 0) {
180
+ throw new Error('At least one KMS Provider is required when using MongoDB encryption')
181
+ }
182
+
183
+ const requiredConfig: RequiredConfig = {
184
+ kmsProviders: config.kmsProviders,
185
+ keyVaultDb: config.keyVaultDb ?? DEFAULT_CONFIG.MONGODB_ENCRYPTION_CONFIG.keyVaultDb,
186
+ keyVaultCollection: config.keyVaultDb ?? DEFAULT_CONFIG.MONGODB_ENCRYPTION_CONFIG.keyVaultCollection,
187
+ }
188
+
189
+ const kmsProviders = requiredConfig.kmsProviders.reduce(
190
+ (acc, { provider, config }) => ({ ...acc, [provider]: config }),
191
+ {} as KMSProviders
192
+ )
193
+
194
+ const keyVaultNamespace = `${requiredConfig.keyVaultDb}.${requiredConfig.keyVaultCollection}`
195
+ const keyVaultClient = new MongoClient(config.mongodbUrl, {
196
+ maxPoolSize: 1,
197
+ autoEncryption: {
198
+ keyVaultNamespace,
199
+ kmsProviders,
200
+ extraOptions: config.extraOptions
201
+ }
202
+ });
203
+
204
+ await keyVaultClient.connect();
205
+
206
+ const keyVaultDb = keyVaultClient.db(requiredConfig.keyVaultDb);
207
+ await ensureUniqueKeyAltNameIndex(keyVaultDb, requiredConfig)
208
+
209
+ const clientEncryption = new ClientEncryption(keyVaultClient, {
210
+ keyVaultNamespace,
211
+ kmsProviders,
212
+ });
213
+
214
+ const dataKeys = await ensureDataEncryptionKeys(clientEncryption, keyVaultDb, requiredConfig)
215
+
216
+ await keyVaultClient.close()
217
+
218
+ return {
219
+ keyVaultNamespace,
220
+ kmsProviders,
221
+ schemaMap: config.schemas ? buildSchemaMap(config.schemas, dataKeys) : undefined,
222
+ extraOptions: config.extraOptions
223
+ }
224
+ }
@@ -1,5 +1,5 @@
1
1
  import cors from '@fastify/cors'
2
- import fastifyMongodb from '@fastify/mongodb'
2
+ import fastifyMongodb, { FastifyMongodbOptions } from '@fastify/mongodb'
3
3
  import { FastifyInstance } from 'fastify'
4
4
  import fastifyRawBody from 'fastify-raw-body'
5
5
  import { CorsConfig } from '../../'
@@ -10,7 +10,9 @@ import { customFunctionController } from '../../auth/providers/custom-function/c
10
10
  import { localUserPassController } from '../../auth/providers/local-userpass/controller'
11
11
  import { API_VERSION, DEFAULT_CONFIG } from '../../constants'
12
12
  import { Functions } from '../../features/functions/interface'
13
+ import { EncryptionSchemas } from '../../features/encryption/interface'
13
14
  import monitoringPlugin from '../../monitoring/plugin'
15
+ import { setupMongoDbCSFLE, MongoDbEncryptionConfig } from './mongodbCSFLE'
14
16
 
15
17
  type RegisterFunction = FastifyInstance['register']
16
18
  type RegisterParameters = Parameters<RegisterFunction>
@@ -21,6 +23,8 @@ type RegisterPluginsParams = {
21
23
  jwtSecret: string
22
24
  functionsList: Functions
23
25
  corsConfig?: CorsConfig
26
+ encryptionSchemas?: EncryptionSchemas
27
+ mongodbEncryptionConfig?: MongoDbEncryptionConfig
24
28
  }
25
29
 
26
30
  type RegisterConfig = {
@@ -41,14 +45,18 @@ export const registerPlugins = async ({
41
45
  mongodbUrl,
42
46
  jwtSecret,
43
47
  functionsList,
44
- corsConfig
48
+ corsConfig,
49
+ mongodbEncryptionConfig,
50
+ encryptionSchemas
45
51
  }: RegisterPluginsParams) => {
46
52
  try {
47
53
  const registersConfig = await getRegisterConfig({
48
54
  mongodbUrl,
49
55
  jwtSecret,
50
56
  corsConfig,
51
- functionsList
57
+ functionsList,
58
+ mongodbEncryptionConfig,
59
+ encryptionSchemas
52
60
  })
53
61
 
54
62
  registersConfig.forEach(({ plugin, options, pluginName }) => {
@@ -75,15 +83,23 @@ export const registerPlugins = async ({
75
83
  const getRegisterConfig = async ({
76
84
  mongodbUrl,
77
85
  jwtSecret,
78
- corsConfig
79
- }: Pick<RegisterPluginsParams, 'jwtSecret' | 'mongodbUrl' | 'functionsList' | 'corsConfig'>): Promise<
80
- RegisterConfig[]
81
- > => {
86
+ corsConfig,
87
+ encryptionSchemas,
88
+ mongodbEncryptionConfig,
89
+ }: Omit<RegisterPluginsParams, "register">): Promise<RegisterConfig[]> => {
82
90
  const corsOptions = corsConfig ?? {
83
91
  origin: '*',
84
92
  methods: ['POST', 'GET']
85
93
  }
86
94
 
95
+ const autoEncryption = mongodbEncryptionConfig
96
+ ? await setupMongoDbCSFLE({
97
+ mongodbUrl,
98
+ schemas: encryptionSchemas,
99
+ ...mongodbEncryptionConfig
100
+ })
101
+ : undefined
102
+
87
103
  const baseConfig = [
88
104
  {
89
105
  pluginName: 'cors',
@@ -94,9 +110,28 @@ const getRegisterConfig = async ({
94
110
  pluginName: 'fastifyMongodb',
95
111
  plugin: fastifyMongodb,
96
112
  options: {
113
+ url: mongodbUrl,
97
114
  forceClose: true,
98
- url: mongodbUrl
99
- }
115
+ autoEncryption
116
+ } satisfies FastifyMongodbOptions
117
+ },
118
+ /**
119
+ * When auto-encryption is active, add another MongoDB client with bypass for change streams.
120
+ * The $changeStream operator does not support automatic encryption, only decryption.
121
+ * @see https://www.mongodb.com/docs/manual/core/csfle/reference/supported-operations
122
+ */
123
+ autoEncryption && {
124
+ pluginName: 'fastifyMongodb',
125
+ plugin: fastifyMongodb,
126
+ options: {
127
+ name: "changestream",
128
+ url: mongodbUrl,
129
+ forceClose: true,
130
+ autoEncryption: {
131
+ ...autoEncryption,
132
+ bypassAutoEncryption: true
133
+ }
134
+ } satisfies FastifyMongodbOptions
100
135
  },
101
136
  {
102
137
  pluginName: 'jwtAuthPlugin',
@@ -153,5 +188,5 @@ const getRegisterConfig = async ({
153
188
  } as RegisterConfig)
154
189
  }
155
190
 
156
- return baseConfig
191
+ return baseConfig.filter(Boolean)
157
192
  }