@friggframework/core 2.0.0--canary.680.6bacd26.0 → 2.0.0--canary.663.8de669c.0

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 (86) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/application/commands/integration-commands.js +8 -18
  3. package/application/index.js +2 -6
  4. package/credential/repositories/credential-repository-documentdb.js +0 -34
  5. package/credential/repositories/credential-repository-interface.js +0 -15
  6. package/credential/repositories/credential-repository-mongo.js +0 -23
  7. package/credential/repositories/credential-repository-postgres.js +0 -28
  8. package/database/encryption/README.md +0 -2
  9. package/database/encryption/encryption-schema-registry.js +12 -41
  10. package/database/encryption/field-encryption-service.js +5 -69
  11. package/generated/prisma-mongodb/edge.js +4 -20
  12. package/generated/prisma-mongodb/index-browser.js +1 -17
  13. package/generated/prisma-mongodb/index.d.ts +95 -1543
  14. package/generated/prisma-mongodb/index.js +4 -20
  15. package/generated/prisma-mongodb/package.json +1 -1
  16. package/generated/prisma-mongodb/schema.prisma +0 -27
  17. package/generated/prisma-mongodb/wasm.js +4 -20
  18. package/generated/prisma-postgresql/edge.js +4 -20
  19. package/generated/prisma-postgresql/index-browser.js +1 -17
  20. package/generated/prisma-postgresql/index.d.ts +84 -1565
  21. package/generated/prisma-postgresql/index.js +4 -20
  22. package/generated/prisma-postgresql/package.json +1 -1
  23. package/generated/prisma-postgresql/schema.prisma +0 -26
  24. package/generated/prisma-postgresql/wasm.js +4 -20
  25. package/handlers/app-definition-loader.js +8 -19
  26. package/handlers/app-handler-helpers.js +3 -25
  27. package/handlers/backend-utils.js +25 -31
  28. package/handlers/middleware/admin-auth.js +1 -14
  29. package/handlers/routers/auth.js +3 -11
  30. package/handlers/routers/health.js +4 -11
  31. package/handlers/workers/integration-defined-workers.js +2 -5
  32. package/index.js +2 -4
  33. package/integrations/index.js +4 -0
  34. package/integrations/integration-router.js +23 -16
  35. package/integrations/utils/map-integration-dto.js +15 -1
  36. package/modules/index.js +0 -6
  37. package/modules/requester/requester.js +2 -27
  38. package/package.json +5 -5
  39. package/prisma-mongodb/schema.prisma +0 -27
  40. package/prisma-postgresql/schema.prisma +0 -26
  41. package/types/index.d.ts +0 -1
  42. package/credential/use-cases/credential-operations-for-user.js +0 -117
  43. package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
  44. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  45. package/integrations/use-cases/integration-operations-for-user.js +0 -225
  46. package/management-api/create-management-api-router.js +0 -49
  47. package/management-api/error-response.js +0 -116
  48. package/management-api/management-api-config.js +0 -82
  49. package/management-api/meta-router.js +0 -57
  50. package/management-api/openapi/build-openapi.js +0 -144
  51. package/management-api/openapi/components.js +0 -368
  52. package/management-api/openapi/v1-operations.js +0 -83
  53. package/management-api/openapi/v2-operations.js +0 -252
  54. package/management-api/route-registry.js +0 -294
  55. package/management-api/test-support/http-client.js +0 -39
  56. package/management-api/use-cases/get-management-api-meta.js +0 -57
  57. package/management-api/v1-deprecation.js +0 -120
  58. package/management-api/v2/create-v2-router.js +0 -85
  59. package/management-api/v2/dependencies.js +0 -256
  60. package/management-api/v2/dto.js +0 -123
  61. package/management-api/v2/handlers/authorization.js +0 -93
  62. package/management-api/v2/handlers/credentials.js +0 -29
  63. package/management-api/v2/handlers/entities.js +0 -74
  64. package/management-api/v2/handlers/integrations.js +0 -125
  65. package/management-api/v2/handlers/proxy.js +0 -34
  66. package/management-api/v2/request.js +0 -54
  67. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  68. package/modules/domain/authorization-session.js +0 -144
  69. package/modules/entity-type-registry.js +0 -311
  70. package/modules/proxy/proxy-errors.js +0 -20
  71. package/modules/proxy/proxy-path.js +0 -179
  72. package/modules/proxy/proxy-policy.js +0 -248
  73. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  74. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  75. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  76. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  77. package/modules/requester/proxy-request.js +0 -244
  78. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  79. package/modules/use-cases/entity-operations-for-user.js +0 -125
  80. package/modules/use-cases/entity-types.js +0 -115
  81. package/modules/use-cases/execute-entity-proxy-request.js +0 -239
  82. package/modules/use-cases/get-authorization-requirements.js +0 -100
  83. package/modules/use-cases/get-entity-module-for-user.js +0 -86
  84. package/modules/use-cases/multi-step-authorization.js +0 -302
  85. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  86. package/types/modules/index.d.ts +0 -58
package/CHANGELOG.md CHANGED
@@ -1,14 +1,3 @@
1
- # Unreleased
2
-
3
- #### Behaviour change: module credential fields are now encrypted
4
-
5
- - `credentialFields` declared in an API module's `encryption` config (for example `Definition.encryption.credentialFields`) were never registered for encryption: `loadModuleEncryptionSchemas` required a path that does not exist and the error was swallowed. They are now registered, for modules bound by integrations and for modules registered in `appDefinition.entities` (ADR-059).
6
- - Existing rows are not rewritten. A credential saved before this release stays plaintext until it is next written, and is read as stored.
7
- - Reads now tolerate plaintext: a value is only treated as ciphertext when it has the exact `keyId:iv:ciphertext:encryptedKey` shape (previously any string with three or more colons, such as `https://user:pw@host:443`), and a value with that shape that fails to decrypt throws `FieldDecryptionError` in a deployed runtime (any stage, AWS Lambda or Netlify) and is returned as stored with a warning on a local run.
8
- - An app's own `encryption.schema` is no longer replaced when module fields are added to it.
9
-
10
- ---
11
-
12
1
  # v1.2.2 (Fri Aug 09 2024)
13
2
 
14
3
  #### 🐛 Bug Fix
@@ -30,9 +30,8 @@ const {
30
30
  PatchIntegrationConfig,
31
31
  } = require('../../integrations/use-cases/patch-integration-config');
32
32
  const {
33
- getEntityTypeRegistry,
34
- getEntityTypeDefinitions,
35
- } = require('../../modules/entity-type-registry');
33
+ getModulesDefinitionFromIntegrationClasses,
34
+ } = require('../../integrations/utils/map-integration-dto');
36
35
 
37
36
  const ERROR_CODE_MAP = {
38
37
  ENTITY_NOT_FOUND: 401,
@@ -52,16 +51,7 @@ function mapErrorToResponse(error) {
52
51
  };
53
52
  }
54
53
 
55
- /**
56
- * @param {Object} [params]
57
- * @param {Function} [params.integrationClass]
58
- * @param {Array<object>} [params.moduleDefinitions] - the app's offered module
59
- * definitions (`loadAppDefinition().moduleDefinitions`). Integration code
60
- * calls this from a constructor with no app definition in hand, so when it
61
- * is omitted the commands hydrate with this integration's own modules only
62
- * (a registry built from `{ integrations: [integrationClass] }`).
63
- */
64
- function createIntegrationCommands({ integrationClass, moduleDefinitions } = {}) {
54
+ function createIntegrationCommands({ integrationClass } = {}) {
65
55
  // Always use Frigg's default repositories and use cases
66
56
  const integrationRepository = createIntegrationRepository();
67
57
 
@@ -118,13 +108,13 @@ function createIntegrationCommands({ integrationClass, moduleDefinitions } = {})
118
108
 
119
109
  const moduleRepository = createModuleRepository();
120
110
 
111
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
112
+ integrationClass,
113
+ ]);
114
+
121
115
  const moduleFactory = new ModuleFactory({
122
116
  moduleRepository,
123
- moduleDefinitions:
124
- moduleDefinitions ??
125
- getEntityTypeDefinitions(
126
- getEntityTypeRegistry({ integrations: [integrationClass] })
127
- ),
117
+ moduleDefinitions,
128
118
  });
129
119
 
130
120
  const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
@@ -17,7 +17,6 @@ const { createUsageCommands } = require('./commands/usage-commands');
17
17
  *
18
18
  * @param {Object} params
19
19
  * @param {Object} params.integrationClass - Integration class (required)
20
- * @param {Array<object>} [params.moduleDefinitions] - the app's offered module definitions; defaults to the integration's own modules
21
20
  * @returns {Object} Unified commands object with all CRUD operations
22
21
  *
23
22
  * @example
@@ -25,12 +24,9 @@ const { createUsageCommands } = require('./commands/usage-commands');
25
24
  * const user = await commands.createUser({ username: 'user@example.com' });
26
25
  * const credential = await commands.createCredential({ userId: user.id, ... });
27
26
  */
28
- function createFriggCommands({ integrationClass, moduleDefinitions }) {
27
+ function createFriggCommands({ integrationClass }) {
29
28
  // All commands use Frigg's default repositories and use cases
30
- const integrationCommands = createIntegrationCommands({
31
- integrationClass,
32
- moduleDefinitions,
33
- });
29
+ const integrationCommands = createIntegrationCommands({ integrationClass });
34
30
 
35
31
  const userCommands = createUserCommands();
36
32
 
@@ -186,40 +186,6 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
186
186
  return this._mapCredential(decryptedCredential);
187
187
  }
188
188
 
189
- /**
190
- * All credentials owned by a user, newest first.
191
- *
192
- * @param {string} userId - User ID
193
- * @returns {Promise<Array<Object>>} Credentials with string IDs
194
- */
195
- async findCredentialsByUserId(userId) {
196
- const userObjectId = toObjectId(userId);
197
- if (!userObjectId) return [];
198
- const docs = await findManyDrained(
199
- this.prisma,
200
- 'Credential',
201
- { userId: userObjectId },
202
- { sort: { createdAt: -1 } }
203
- );
204
- const credentials = [];
205
- for (const doc of docs) {
206
- const decrypted = await this.encryptionService.decryptFields(
207
- 'Credential',
208
- doc
209
- );
210
- credentials.push({
211
- ...this._mapCredential(decrypted),
212
- id: fromObjectId(doc._id),
213
- userId: fromObjectId(doc.userId) ?? doc.userId,
214
- externalId: doc.externalId ?? null,
215
- authIsValid: doc.authIsValid ?? null,
216
- createdAt: doc.createdAt ?? null,
217
- updatedAt: doc.updatedAt ?? null,
218
- });
219
- }
220
- return credentials;
221
- }
222
-
223
189
  async updateCredential(credentialId, updates) {
224
190
  const objectId = toObjectId(credentialId);
225
191
  if (!objectId) return null;
@@ -80,21 +80,6 @@ class CredentialRepositoryInterface {
80
80
  );
81
81
  }
82
82
 
83
- /**
84
- * All credentials owned by a user, newest first.
85
- *
86
- * @param {string|number} userId - User ID (string from application layer)
87
- * @returns {Promise<Array<Object>>} Credentials with string IDs, `createdAt`
88
- * and `updatedAt`, and the (decrypted) data fields spread in, as
89
- * findCredentialById returns them. Empty when the user has none.
90
- * @abstract
91
- */
92
- async findCredentialsByUserId(userId) {
93
- throw new Error(
94
- 'Method findCredentialsByUserId must be implemented by subclass'
95
- );
96
- }
97
-
98
83
  /**
99
84
  * Update a credential by ID
100
85
  *
@@ -188,29 +188,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
188
188
  };
189
189
  }
190
190
 
191
- /**
192
- * All credentials owned by a user, newest first.
193
- *
194
- * @param {string} userId - User ID
195
- * @returns {Promise<Array<Object>>} Credentials with string IDs
196
- */
197
- async findCredentialsByUserId(userId) {
198
- if (!userId) return [];
199
- const credentials = await this.prisma.credential.findMany({
200
- where: { userId: String(userId) },
201
- orderBy: { createdAt: 'desc' },
202
- });
203
- return credentials.map((credential) => ({
204
- ...(credential.data || {}),
205
- id: credential.id,
206
- userId: credential.userId,
207
- externalId: credential.externalId,
208
- authIsValid: credential.authIsValid,
209
- createdAt: credential.createdAt,
210
- updatedAt: credential.updatedAt,
211
- }));
212
- }
213
-
214
191
  /**
215
192
  * Update a credential by ID
216
193
  * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
@@ -205,34 +205,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
205
205
  };
206
206
  }
207
207
 
208
- /**
209
- * All credentials owned by a user, newest first.
210
- *
211
- * @param {string} userId - User ID (string from application layer)
212
- * @returns {Promise<Array<Object>>} Credentials with string IDs
213
- */
214
- async findCredentialsByUserId(userId) {
215
- let intUserId;
216
- try {
217
- intUserId = this._convertId(userId);
218
- } catch {
219
- return [];
220
- }
221
- const credentials = await this.prisma.credential.findMany({
222
- where: { userId: intUserId },
223
- orderBy: { createdAt: 'desc' },
224
- });
225
- return credentials.map((credential) => ({
226
- ...(credential.data || {}),
227
- id: credential.id.toString(),
228
- userId: credential.userId?.toString(),
229
- externalId: credential.externalId,
230
- authIsValid: credential.authIsValid,
231
- createdAt: credential.createdAt,
232
- updatedAt: credential.updatedAt,
233
- }));
234
- }
235
-
236
208
  /**
237
209
  * Update a credential by ID
238
210
  *
@@ -456,8 +456,6 @@ Format: "keyId:encryptedText:encryptedKey"
456
456
  Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
457
457
  ```
458
458
 
459
- **Recognising ciphertext on read.** A value is decrypted only when it has exactly four colon-separated parts: base64 `keyId`, 32-hex-character `iv`, hex ciphertext, base64 `encryptedKey`. Plaintext that merely contains colons (`https://user:pw@host:443`) is left alone. A value with that shape that still fails to decrypt (a wrong, rotated or missing key) fails the read with `FieldDecryptionError` in a deployed runtime (see [When Encryption Runs](#when-encryption-runs)), so ciphertext never reaches an API module as if it were a token. Locally it is returned as stored and logged as a warning, so one bad field does not fail a whole read. A field that becomes encrypted later (for example a module's `credentialFields`) stays plaintext in existing rows until they are next written.
460
-
461
459
  **Why Envelope Encryption?**
462
460
 
463
461
  - Reduces KMS API calls (one DEK per field, cached)
@@ -10,11 +10,6 @@
10
10
 
11
11
  const { logger } = require('./logger');
12
12
  const { registerDeniedKeys } = require('../../logs/denied-keys');
13
- const {
14
- getEntityTypeRegistry,
15
- getEntityTypeDefinitions,
16
- EntityRegistryError,
17
- } = require('../../modules/entity-type-registry');
18
13
 
19
14
  /**
20
15
  * Core encryption schema (immutable - cannot be overridden by custom schemas)
@@ -44,12 +39,6 @@ const CORE_ENCRYPTION_SCHEMA = {
44
39
  Token: {
45
40
  fields: ['token'],
46
41
  },
47
-
48
- // Multi-step authorization (Management API v2): earlier steps can collect
49
- // emails, usernames or one-time codes.
50
- AuthorizationSession: {
51
- fields: ['stepData'],
52
- },
53
42
  };
54
43
 
55
44
  let customSchema = {};
@@ -171,9 +160,8 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
171
160
  * Each module can declare credentialFields to encrypt in its encryption config.
172
161
  *
173
162
  * @param {Array} integrations - Array of integration classes with modules
174
- * @param {object} [entities] - `appDefinition.entities` (ADR-059 registry)
175
163
  */
176
- function loadModuleEncryptionSchemas(integrations, entities) {
164
+ function loadModuleEncryptionSchemas(integrations) {
177
165
  if (!integrations) {
178
166
  throw new Error('integrations parameter is required');
179
167
  }
@@ -182,39 +170,27 @@ function loadModuleEncryptionSchemas(integrations, entities) {
182
170
  throw new Error('integrations must be an array');
183
171
  }
184
172
 
185
- // `entities` is the app definition's registry (ADR-059): a module an app
186
- // registers without any integration still carries credentials to encrypt.
187
- if (integrations.length === 0 && !entities) {
173
+ if (integrations.length === 0) {
188
174
  return;
189
175
  }
190
176
 
191
- const moduleDefinitions = getEntityTypeDefinitions(
192
- getEntityTypeRegistry({ integrations, entities })
193
- );
177
+ const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
178
+
179
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
194
180
  const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
195
181
 
196
- // Core fields are encrypted already and a custom schema may not repeat
197
- // them. Everything the app registered itself stays: module fields are added
198
- // to the custom schema, never substituted for it (other models included).
199
- const coreFields = new Set(CORE_ENCRYPTION_SCHEMA.Credential?.fields || []);
200
- const fields = [
201
- ...new Set([
202
- ...(customSchema.Credential?.fields || []),
203
- ...credentialFields.filter((field) => !coreFields.has(field)),
204
- ]),
205
- ];
206
-
207
- if (fields.length === 0) {
182
+ if (credentialFields.length === 0) {
208
183
  return;
209
184
  }
210
185
 
211
186
  const moduleSchema = {
212
- ...customSchema,
213
- Credential: { ...customSchema.Credential, fields },
187
+ Credential: {
188
+ fields: credentialFields
189
+ }
214
190
  };
215
191
 
216
192
  logger.info(
217
- `Registering module-level encryption for ${fields.length} credential fields`
193
+ `Registering module-level encryption for ${credentialFields.length} credential fields`
218
194
  );
219
195
 
220
196
  registerCustomSchema(moduleSchema);
@@ -266,15 +242,10 @@ function loadCustomEncryptionSchema() {
266
242
 
267
243
  // Load module-level encryption schemas from integrations
268
244
  const integrations = appDefinition.integrations;
269
- if (Array.isArray(integrations) || appDefinition.entities) {
270
- loadModuleEncryptionSchemas(
271
- Array.isArray(integrations) ? integrations : [],
272
- appDefinition.entities
273
- );
245
+ if (integrations && Array.isArray(integrations)) {
246
+ loadModuleEncryptionSchemas(integrations);
274
247
  }
275
248
  } catch (error) {
276
- // A bad entity registry is a configuration error, not "no custom schema".
277
- if (error instanceof EntityRegistryError) throw error;
278
249
  // Silently ignore errors - custom schema is optional
279
250
  // This handles cases like:
280
251
  // - Backend package.json not found (tests, standalone usage)
@@ -4,41 +4,8 @@
4
4
  * Infrastructure layer service that orchestrates field-level encryption/decryption.
5
5
  * Handles nested JSON paths (e.g., 'data.access_token') and bulk operations.
6
6
  */
7
- const { logger } = require('./logger');
8
- const { isDeployedRuntime } = require('./encryption-config');
9
-
10
- /**
11
- * Thrown when a value that looks like ciphertext cannot be decrypted in a
12
- * deployed runtime (a wrong, rotated or missing key). Returning the stored
13
- * ciphertext would hand it to API modules as if it were a token.
14
- */
15
- class FieldDecryptionError extends Error {
16
- constructor(message, options) {
17
- super(message, options);
18
- this.name = 'FieldDecryptionError';
19
- }
20
- }
21
-
22
- // Cryptor.encrypt() returns `${keyId}:${iv}:${ciphertext}:${encryptedKey}`:
23
- // keyId and encryptedKey are base64, iv is 16 bytes of hex, ciphertext is hex.
24
- // Plaintext with three colons (`https://user:pw@host:443`, `a:b:c:d`) must not
25
- // be mistaken for it.
26
- const BASE64_PART = /^[A-Za-z0-9+/]+={0,2}$/;
27
- const IV_PART = /^[0-9a-f]{32}$/i;
28
- const CIPHERTEXT_PART = /^(?:[0-9a-f]{2})+$/i;
29
-
30
7
  class FieldEncryptionService {
31
- /**
32
- * @param {Object} params
33
- * @param {Object} params.cryptor
34
- * @param {Object} params.schema
35
- * @param {() => boolean} [params.failClosedOnDecryptError] - whether a
36
- * ciphertext-shaped value that fails to decrypt throws. Defaults to
37
- * "running deployed" (encryption-config's isDeployedRuntime), read on
38
- * each failure. Locally such a value is returned as stored, with a
39
- * warning.
40
- */
41
- constructor({ cryptor, schema, failClosedOnDecryptError }) {
8
+ constructor({ cryptor, schema }) {
42
9
  if (!cryptor) {
43
10
  throw new Error('Cryptor instance required');
44
11
  }
@@ -48,10 +15,6 @@ class FieldEncryptionService {
48
15
 
49
16
  this.cryptor = cryptor;
50
17
  this.schema = schema;
51
- this.failClosedOnDecryptError =
52
- typeof failClosedOnDecryptError === 'function'
53
- ? failClosedOnDecryptError
54
- : () => isDeployedRuntime();
55
18
  }
56
19
 
57
20
  /**
@@ -134,33 +97,12 @@ class FieldEncryptionService {
134
97
  const decryptionPromises = fields.map(async (fieldPath) => {
135
98
  const value = this._getNestedValue(decrypted, fieldPath);
136
99
 
137
- if (!this._isEncrypted(value)) {
138
- return null;
139
- }
140
- try {
100
+ if (this._isEncrypted(value)) {
141
101
  const decryptedValue = await this.cryptor.decrypt(value);
142
102
  const deserializedValue = this._deserializeAfterDecryption(decryptedValue);
143
103
  return { fieldPath, decryptedValue: deserializedValue };
144
- } catch (error) {
145
- // Deployed: a ciphertext-shaped value under a wrong, rotated or
146
- // missing key fails the read rather than reaching an API
147
- // module as a token. Legacy plaintext never gets here: it does
148
- // not look like ciphertext (_isEncrypted).
149
- if (this.failClosedOnDecryptError()) {
150
- throw new FieldDecryptionError(
151
- `Could not decrypt ${modelName}.${fieldPath}`,
152
- { cause: error }
153
- );
154
- }
155
- // Local: a value that looks like ciphertext but is not is read
156
- // as it is stored. One such field must not fail the whole
157
- // read, findMany included.
158
- logger.warn(
159
- `Could not decrypt ${modelName}.${fieldPath}; returning the stored value`,
160
- { errorName: error?.name }
161
- );
162
- return null;
163
104
  }
105
+ return null;
164
106
  });
165
107
 
166
108
  const results = await Promise.all(decryptionPromises);
@@ -210,13 +152,7 @@ class FieldEncryptionService {
210
152
  }
211
153
 
212
154
  const parts = value.split(':');
213
- return (
214
- parts.length === 4 &&
215
- BASE64_PART.test(parts[0]) &&
216
- IV_PART.test(parts[1]) &&
217
- CIPHERTEXT_PART.test(parts[2]) &&
218
- BASE64_PART.test(parts[3])
219
- );
155
+ return parts.length >= 4;
220
156
  }
221
157
 
222
158
  _getNestedValue(obj, path) {
@@ -315,4 +251,4 @@ class FieldEncryptionService {
315
251
  }
316
252
  }
317
253
 
318
- module.exports = { FieldEncryptionService, FieldDecryptionError };
254
+ module.exports = { FieldEncryptionService };