@friggframework/core 2.0.0--canary.686.df38e8e.0 → 2.0.0--canary.710.9045bbc.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.
- package/CHANGELOG.md +0 -16
- package/CLAUDE.md +41 -29
- package/README.md +0 -105
- package/application/commands/README.md +17 -11
- package/application/commands/entity-commands.js +18 -60
- package/application/commands/integration-commands.js +73 -20
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/application/index.js +2 -6
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/config.js +24 -48
- package/database/documentdb-encryption-service.js +22 -9
- package/database/documentdb-utils.js +0 -2
- package/database/encryption/README.md +17 -81
- package/database/encryption/documentdb-encryption-service.md +1 -78
- package/database/encryption/encryption-schema-registry.js +46 -123
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/prisma.js +38 -15
- package/database/use-cases/check-encryption-health-use-case.js +52 -43
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +4 -43
- package/generated/prisma-mongodb/index-browser.js +1 -40
- package/generated/prisma-mongodb/index.d.ts +111 -4739
- package/generated/prisma-mongodb/index.js +4 -43
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -92
- package/generated/prisma-mongodb/wasm.js +4 -43
- package/generated/prisma-postgresql/edge.js +4 -43
- package/generated/prisma-postgresql/index-browser.js +1 -40
- package/generated/prisma-postgresql/index.d.ts +33 -4966
- package/generated/prisma-postgresql/index.js +4 -43
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -87
- package/generated/prisma-postgresql/wasm.js +4 -43
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -121
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +10 -46
- package/handlers/routers/health.js +4 -13
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +3 -65
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +96 -131
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +31 -17
- package/integrations/repositories/integration-repository-mongo.js +129 -60
- package/integrations/repositories/integration-repository-postgres.js +134 -62
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/add-entities-to-integration.js +92 -0
- package/integrations/use-cases/create-integration.js +75 -72
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -7
- package/integrations/use-cases/remove-entities-from-integration.js +70 -0
- package/integrations/utils/assert-entity-ids-input.js +42 -0
- package/integrations/utils/map-integration-dto.js +14 -43
- package/jest-setup.js +13 -2
- package/jest-teardown.js +5 -2
- package/jest.config.js +4 -0
- package/jest.db.config.js +23 -0
- package/modules/index.js +0 -14
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/requester.js +2 -27
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +4 -10
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/modules/utils/map-module-dto.js +5 -3
- package/modules/utils/to-credential-id.js +14 -0
- package/package.json +7 -6
- package/prisma-mongodb/schema.prisma +0 -92
- package/prisma-postgresql/schema.prisma +0 -87
- package/queues/index.js +0 -25
- package/queues/queuer-util.js +6 -5
- package/types/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-bearer-token.js +7 -2
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/credential/use-cases/credential-operations-for-user.js +0 -202
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
- package/database/encryption/encryption-config.js +0 -219
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -187
- package/extensions/index.js +0 -41
- package/extensions/initialize-app.js +0 -123
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -220
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/integration-operations-for-user.js +0 -256
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -151
- package/management-api/meta-router.js +0 -76
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -408
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -264
- package/management-api/route-registry.js +0 -411
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -281
- package/management-api/v2/dto.js +0 -143
- package/management-api/v2/handlers/authorization.js +0 -94
- package/management-api/v2/handlers/credentials.js +0 -33
- package/management-api/v2/handlers/entities.js +0 -77
- package/management-api/v2/handlers/integrations.js +0 -128
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -85
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-scope.js +0 -15
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-errors.js +0 -20
- package/modules/proxy/proxy-path.js +0 -179
- package/modules/proxy/proxy-policy.js +0 -248
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -244
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/entity-operations-for-user.js +0 -162
- package/modules/use-cases/entity-types.js +0 -139
- package/modules/use-cases/execute-entity-proxy-request.js +0 -286
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/modules/use-cases/multi-step-authorization.js +0 -329
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -187
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- package/types/modules/index.d.ts +0 -197
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createUserRepository,
|
|
3
3
|
} = require('../../user/repositories/user-repository-factory');
|
|
4
|
-
const {
|
|
5
|
-
createOrganizationMembershipRepository,
|
|
6
|
-
} = require('../../user/repositories/organization-membership-repository-factory');
|
|
7
|
-
const {
|
|
8
|
-
FindIndividualUsersByOrganization,
|
|
9
|
-
} = require('../../user/use-cases/find-individual-users-by-organization');
|
|
10
4
|
|
|
11
5
|
const ERROR_CODE_MAP = {
|
|
12
6
|
USER_NOT_FOUND: 404,
|
|
@@ -32,12 +26,6 @@ function mapErrorToResponse(error) {
|
|
|
32
26
|
*/
|
|
33
27
|
function createUserCommands() {
|
|
34
28
|
const userRepository = createUserRepository();
|
|
35
|
-
const findIndividualUsersByOrganization =
|
|
36
|
-
new FindIndividualUsersByOrganization({
|
|
37
|
-
userRepository,
|
|
38
|
-
organizationMembershipRepository:
|
|
39
|
-
createOrganizationMembershipRepository(),
|
|
40
|
-
});
|
|
41
29
|
|
|
42
30
|
return {
|
|
43
31
|
/**
|
|
@@ -182,8 +170,7 @@ function createUserCommands() {
|
|
|
182
170
|
},
|
|
183
171
|
|
|
184
172
|
/**
|
|
185
|
-
* Find all individual users
|
|
186
|
-
* a membership and those the legacy organizationId pointer places there.
|
|
173
|
+
* Find all individual users linked to an organization user
|
|
187
174
|
* @param {string} organizationUserId - Organization user ID to search for
|
|
188
175
|
* @returns {Promise<Object[]>} Array of individual user objects (empty if none)
|
|
189
176
|
*/
|
|
@@ -195,9 +182,10 @@ function createUserCommands() {
|
|
|
195
182
|
throw error;
|
|
196
183
|
}
|
|
197
184
|
|
|
198
|
-
const users =
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
const users =
|
|
186
|
+
await userRepository.findIndividualUsersByOrganizationId(
|
|
187
|
+
organizationUserId
|
|
188
|
+
);
|
|
201
189
|
|
|
202
190
|
return (users || []).map((user) => ({
|
|
203
191
|
id: user._id?.toString() || user.id,
|
package/application/index.js
CHANGED
|
@@ -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
|
|
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
|
|
package/core/Worker.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs');
|
|
1
2
|
const _ = require('lodash');
|
|
2
3
|
const { RequiredPropertyError } = require('../errors');
|
|
3
4
|
const { get } = require('../assertions');
|
|
@@ -5,44 +6,18 @@ const { readQueueDelivery } = require('../queues/queue-delivery');
|
|
|
5
6
|
const { runMessageScope } = require('./invocation-scope');
|
|
6
7
|
const { getLogger } = require('../logs');
|
|
7
8
|
|
|
9
|
+
const sqs = new SQSClient({ region: process.env.AWS_REGION });
|
|
8
10
|
const log = getLogger('frigg.worker');
|
|
9
11
|
|
|
10
|
-
/**
|
|
11
|
-
* Worker - queue message producer/consumer base class.
|
|
12
|
-
*
|
|
13
|
-
* Subclass and override _run(). The queue transport is behind
|
|
14
|
-
* QueueClientInterface and can be injected (friggframework/frigg#545):
|
|
15
|
-
* `new MyWorker({ queueClient })`. Unlike #545 it is not required: without
|
|
16
|
-
* one the worker uses an SqsQueueClient configured exactly as before
|
|
17
|
-
* (AWS_REGION), created on first use rather than at module load, so
|
|
18
|
-
* requiring core constructs no AWS client and a Netlify/QStash worker that
|
|
19
|
-
* only calls _run() never touches SQS.
|
|
20
|
-
*/
|
|
21
12
|
class Worker {
|
|
22
|
-
/**
|
|
23
|
-
* @param {Object} [options]
|
|
24
|
-
* @param {import('../queues/queue-client-interface').QueueClientInterface} [options.queueClient]
|
|
25
|
-
*/
|
|
26
|
-
constructor(options = {}) {
|
|
27
|
-
this._queueClient = options.queueClient || null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_getQueueClient() {
|
|
31
|
-
if (!this._queueClient) {
|
|
32
|
-
const { SqsQueueClient } = require('../queues/sqs-queue-client');
|
|
33
|
-
this._queueClient = new SqsQueueClient({
|
|
34
|
-
clientConfig: { region: process.env.AWS_REGION },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return this._queueClient;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
13
|
async getQueueURL(params) {
|
|
41
14
|
// Passing params in because there will be multiple QueueNames
|
|
42
15
|
// let params = {
|
|
43
16
|
// QueueName: process.env.QueueName
|
|
44
17
|
// };
|
|
45
|
-
|
|
18
|
+
const command = new GetQueueUrlCommand(params);
|
|
19
|
+
const data = await sqs.send(command);
|
|
20
|
+
return data.QueueUrl;
|
|
46
21
|
}
|
|
47
22
|
|
|
48
23
|
async run(params, context = {}) {
|
|
@@ -116,7 +91,8 @@ class Worker {
|
|
|
116
91
|
}
|
|
117
92
|
|
|
118
93
|
async sendAsyncSQSMessage(params) {
|
|
119
|
-
const
|
|
94
|
+
const command = new SendMessageCommand(params);
|
|
95
|
+
const data = await sqs.send(command);
|
|
120
96
|
return data.MessageId;
|
|
121
97
|
}
|
|
122
98
|
|
package/core/create-handler.js
CHANGED
|
@@ -81,15 +81,6 @@ const createHandler = (optionByName = {}) => {
|
|
|
81
81
|
if (shouldUseDatabase) {
|
|
82
82
|
const { connectPrisma } = require('../database/prisma');
|
|
83
83
|
await connectPrisma();
|
|
84
|
-
|
|
85
|
-
// Extension bootstraps (e.g. db-credentials swapping a
|
|
86
|
-
// module's env for its DB loader) run once per
|
|
87
|
-
// process, after the connect and before any request
|
|
88
|
-
// builds a module. AWS and Netlify both reach here.
|
|
89
|
-
const {
|
|
90
|
-
initializeApp,
|
|
91
|
-
} = require('../extensions/initialize-app');
|
|
92
|
-
await initializeApp();
|
|
93
84
|
}
|
|
94
85
|
|
|
95
86
|
// Helps reuse the database connection. Lowers response times.
|
|
@@ -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
|
*
|
package/database/config.js
CHANGED
|
@@ -3,39 +3,6 @@
|
|
|
3
3
|
* Manages configuration for Prisma ORM operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Maps an app definition's `database` block to a Prisma database type.
|
|
8
|
-
*
|
|
9
|
-
* @param {Object|undefined} database - `Definition.database`
|
|
10
|
-
* @param {string} where - where the definition lives, for the error message
|
|
11
|
-
* @returns {'mongodb'|'postgresql'|'documentdb'}
|
|
12
|
-
*/
|
|
13
|
-
function databaseTypeFromConfig(database, where) {
|
|
14
|
-
if (!database) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
'[Frigg] App definition missing database configuration. ' +
|
|
17
|
-
`Add database: { postgres: { enable: true } } (or mongoDB/documentDB) to ${where}`
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Determine database type from enabled database
|
|
22
|
-
// Priority order: postgres > mongoDB > documentDB
|
|
23
|
-
if (database.postgres?.enable === true) {
|
|
24
|
-
return 'postgresql';
|
|
25
|
-
}
|
|
26
|
-
if (database.mongoDB?.enable === true) {
|
|
27
|
-
return 'mongodb';
|
|
28
|
-
}
|
|
29
|
-
if (database.documentDB?.enable === true) {
|
|
30
|
-
return 'documentdb';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
throw new Error(
|
|
34
|
-
'[Frigg] No database enabled in app definition. ' +
|
|
35
|
-
'Set one of: database.postgres.enable, database.mongoDB.enable, or database.documentDB.enable to true'
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
6
|
/**
|
|
40
7
|
* Determines database type from environment or app definition
|
|
41
8
|
*
|
|
@@ -54,20 +21,6 @@ function getDatabaseType() {
|
|
|
54
21
|
|
|
55
22
|
// Fallback: Load app definition
|
|
56
23
|
try {
|
|
57
|
-
// A definition preset with setAppDefinition() (provider-netlify's
|
|
58
|
-
// function preamble) wins: on Netlify process.cwd() is the function
|
|
59
|
-
// root at runtime, so discovery cannot find the backend.
|
|
60
|
-
const {
|
|
61
|
-
getPresetAppDefinition,
|
|
62
|
-
} = require('../handlers/app-definition-loader');
|
|
63
|
-
const preset = getPresetAppDefinition();
|
|
64
|
-
if (preset) {
|
|
65
|
-
return databaseTypeFromConfig(
|
|
66
|
-
preset.database,
|
|
67
|
-
'your backend/index.js'
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
24
|
const path = require('node:path');
|
|
72
25
|
const fs = require('node:fs');
|
|
73
26
|
const { findNearestBackendPackageJson } = require('../utils');
|
|
@@ -123,7 +76,30 @@ function getDatabaseType() {
|
|
|
123
76
|
}
|
|
124
77
|
|
|
125
78
|
database = backendModule?.Definition?.database;
|
|
126
|
-
|
|
79
|
+
|
|
80
|
+
if (!database) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
'[Frigg] App definition missing database configuration. ' +
|
|
83
|
+
`Add database: { postgres: { enable: true } } (or mongoDB/documentDB) to ${backendIndexPath}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Determine database type from enabled database
|
|
88
|
+
// Priority order: postgres > mongoDB > documentDB
|
|
89
|
+
if (database.postgres?.enable === true) {
|
|
90
|
+
return 'postgresql';
|
|
91
|
+
}
|
|
92
|
+
if (database.mongoDB?.enable === true) {
|
|
93
|
+
return 'mongodb';
|
|
94
|
+
}
|
|
95
|
+
if (database.documentDB?.enable === true) {
|
|
96
|
+
return 'documentdb';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(
|
|
100
|
+
'[Frigg] No database enabled in app definition. ' +
|
|
101
|
+
'Set one of: database.postgres.enable, database.mongoDB.enable, or database.documentDB.enable to true'
|
|
102
|
+
);
|
|
127
103
|
} catch (error) {
|
|
128
104
|
// Re-throw with context if it's our error
|
|
129
105
|
if (error.message.includes('[Frigg]')) {
|
|
@@ -4,7 +4,6 @@ const {
|
|
|
4
4
|
getFieldsToEncryptOnWrite,
|
|
5
5
|
loadCustomEncryptionSchema,
|
|
6
6
|
} = require('./encryption/encryption-schema-registry');
|
|
7
|
-
const { getEncryptionConfig } = require('./encryption/encryption-config');
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Encryption service specifically for DocumentDB repositories
|
|
@@ -42,29 +41,43 @@ class DocumentDBEncryptionService {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
* Initialize Cryptor with environment-based configuration
|
|
46
|
-
*
|
|
44
|
+
* Initialize Cryptor with environment-based configuration.
|
|
45
|
+
* Matches the logic from @friggframework/core/database/prisma.js
|
|
47
46
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Encryption is bypassed in dev/test/local stages.
|
|
48
|
+
* Production uses AWS KMS (if available) or AES encryption.
|
|
50
49
|
*
|
|
51
50
|
* @private
|
|
52
|
-
* @throws {EncryptionConfigurationError} deployed with no usable key
|
|
53
51
|
*/
|
|
54
52
|
_initializeCryptor() {
|
|
55
53
|
// Load custom encryption schema from app definition BEFORE checking configuration
|
|
56
54
|
// This ensures custom fields (like User.username) are registered before any encryption operations
|
|
57
55
|
loadCustomEncryptionSchema();
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
// Match logic from packages/core/database/prisma.js
|
|
58
|
+
const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
59
|
+
const bypassEncryption = ['dev', 'test', 'local'].includes(stage.toLowerCase());
|
|
60
60
|
|
|
61
|
-
if (
|
|
61
|
+
if (bypassEncryption) {
|
|
62
62
|
this.cryptor = null;
|
|
63
63
|
this.enabled = false;
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
// Determine encryption method (ensure boolean values)
|
|
68
|
+
const hasKMS = !!(process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '');
|
|
69
|
+
const hasAES = !!(process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '');
|
|
70
|
+
|
|
71
|
+
if (!hasKMS && !hasAES) {
|
|
72
|
+
console.warn('[DocumentDBEncryptionService] No encryption keys configured. Encryption disabled.');
|
|
73
|
+
this.cryptor = null;
|
|
74
|
+
this.enabled = false;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// KMS takes precedence over AES
|
|
79
|
+
const shouldUseAws = hasKMS;
|
|
80
|
+
this.cryptor = new Cryptor({ shouldUseAws });
|
|
68
81
|
this.enabled = true;
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -58,8 +58,6 @@ async function insertOne(client, collection, document) {
|
|
|
58
58
|
|
|
59
59
|
// Provide helpful context for common errors
|
|
60
60
|
if (error.code === 11000) {
|
|
61
|
-
// `code` lets callers recognise a lost race without parsing text
|
|
62
|
-
// (see database/unique-violation.js).
|
|
63
61
|
throw Object.assign(
|
|
64
62
|
new Error(`${errorMsg} - Duplicate key violation`),
|
|
65
63
|
{ code: 11000 }
|
|
@@ -13,7 +13,7 @@ This module provides **transparent field-level encryption** for sensitive data i
|
|
|
13
13
|
- ✅ **Hexagonal architecture**: Clean separation of concerns
|
|
14
14
|
- ✅ **AWS KMS support**: Enterprise-grade encryption with AWS Key Management Service
|
|
15
15
|
- ✅ **Local AES fallback**: Development mode using local encryption keys
|
|
16
|
-
- ✅ **
|
|
16
|
+
- ✅ **Environment-based**: Automatic bypass in dev/test/local environments
|
|
17
17
|
- ✅ **Envelope encryption**: Secure key management pattern
|
|
18
18
|
|
|
19
19
|
## Architecture
|
|
@@ -107,46 +107,18 @@ AES_KEY=your-32-character-secret-key-here
|
|
|
107
107
|
STAGE=production # or development, staging, etc.
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
**⚠️ Important**: Encryption is automatically **disabled** when `STAGE` is set to `dev`, `test`, or `local`, regardless of key configuration.
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
### Bypass Encryption
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
To explicitly disable encryption:
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| Deployed, any stage | `AES_KEY_ID` + `AES_KEY` | any | Encrypt with AES |
|
|
120
|
-
| Deployed, any stage | `AES_KEY_ID` without `AES_KEY` | no | **Refuses to start** (`EncryptionConfigurationError`) |
|
|
121
|
-
| Deployed, any stage | none | no | **Refuses to start** (`EncryptionConfigurationError`) |
|
|
122
|
-
| Deployed, any stage | none | yes | Plaintext, with a warning on every cold start |
|
|
123
|
-
| Local, `STAGE` (or `NODE_ENV`) is `dev`, `test` or `local` | any | any | Skipped |
|
|
124
|
-
| Local, any other stage | `KMS_KEY_ARN` or `AES_KEY_ID` | any | Encrypt |
|
|
125
|
-
| Local, any other stage | none | any | Plaintext, with a warning |
|
|
126
|
-
|
|
127
|
-
KMS wins when both keys are set. A configured key always wins over `FRIGG_ENCRYPTION_DISABLED`: switching encryption off while a key exists would leave already-encrypted data unreadable, so the opt-out is ignored (with a warning).
|
|
128
|
-
|
|
129
|
-
"Refuses to start" means the Prisma client is never created: the first database access on a cold start throws
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
EncryptionConfigurationError: [Frigg] No field-level encryption key is configured for stage "dev". A deployed Frigg app will not write credentials and other sensitive fields in plaintext. Fix: ...
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
and `/health/detailed` reports `checks.encryption.status: "unhealthy"` with the same message. If a key is configured but the encryption extension fails to initialise, the client also refuses to start rather than continuing without encryption.
|
|
136
|
-
|
|
137
|
-
To encrypt a local run, use a stage other than `dev`, `test` or `local` and configure a key.
|
|
138
|
-
|
|
139
|
-
### Running a Deployed Stage Without Encryption
|
|
140
|
-
|
|
141
|
-
Only do this on purpose, for a stage that never holds real credentials. Either set it in the app definition:
|
|
142
|
-
|
|
143
|
-
```javascript
|
|
144
|
-
const appDefinition = {
|
|
145
|
-
encryption: { fieldLevelEncryptionMethod: 'none' },
|
|
146
|
-
};
|
|
116
|
+
```bash
|
|
117
|
+
# Disable encryption (development only)
|
|
118
|
+
STAGE=development # or dev, test, local
|
|
147
119
|
```
|
|
148
120
|
|
|
149
|
-
|
|
121
|
+
Or simply don't configure any encryption keys. In Production field level encryption **must** be enabled.
|
|
150
122
|
|
|
151
123
|
## Encrypted Fields
|
|
152
124
|
|
|
@@ -456,8 +428,6 @@ Format: "keyId:encryptedText:encryptedKey"
|
|
|
456
428
|
Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
|
|
457
429
|
```
|
|
458
430
|
|
|
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
431
|
**Why Envelope Encryption?**
|
|
462
432
|
|
|
463
433
|
- Reduces KMS API calls (one DEK per field, cached)
|
|
@@ -538,9 +508,9 @@ return entities.map((e) => ({
|
|
|
538
508
|
|
|
539
509
|
**Verified:**
|
|
540
510
|
|
|
541
|
-
- ✅ `postgres-relation-decryption.test.js` - Proves the bug exists
|
|
542
|
-
- ✅ `postgres-decryption-fix-verification.test.js` - Verifies separate queries work
|
|
543
|
-
- ✅ `mongo-decryption-fix-verification.test.js` - Verifies fix for MongoDB
|
|
511
|
+
- ✅ `postgres-relation-decryption.db.test.js` - Proves the bug exists
|
|
512
|
+
- ✅ `postgres-decryption-fix-verification.db.test.js` - Verifies separate queries work
|
|
513
|
+
- ✅ `mongo-decryption-fix-verification.db.test.js` - Verifies fix for MongoDB
|
|
544
514
|
|
|
545
515
|
**Implementation Examples:**
|
|
546
516
|
|
|
@@ -583,8 +553,8 @@ class MyRepositoryDocumentDB {
|
|
|
583
553
|
|
|
584
554
|
#### Configuration
|
|
585
555
|
|
|
586
|
-
Uses the same
|
|
587
|
-
- `STAGE`:
|
|
556
|
+
Uses the same environment variables and Cryptor as the Prisma Extension:
|
|
557
|
+
- `STAGE`: Bypasses encryption for dev/test/local
|
|
588
558
|
- `KMS_KEY_ARN`: AWS KMS encryption (production)
|
|
589
559
|
- `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
|
|
590
560
|
|
|
@@ -645,27 +615,10 @@ curl http://localhost:3000/health/test-encryption
|
|
|
645
615
|
"encryptionWorks": true
|
|
646
616
|
}
|
|
647
617
|
|
|
648
|
-
# Response
|
|
618
|
+
# Response when encryption disabled:
|
|
649
619
|
{
|
|
650
620
|
"status": "disabled",
|
|
651
|
-
"
|
|
652
|
-
"runtime": "local",
|
|
653
|
-
"testResult": "Encryption bypassed for this stage"
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
# Response in a deployed stage with no key (the app itself refuses to start):
|
|
657
|
-
{
|
|
658
|
-
"status": "unhealthy",
|
|
659
|
-
"mode": "none",
|
|
660
|
-
"runtime": "deployed",
|
|
661
|
-
"testResult": "[Frigg] No field-level encryption key is configured for stage \"dev\". ..."
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
# Response in a deployed stage with FRIGG_ENCRYPTION_DISABLED=true and no key:
|
|
665
|
-
{
|
|
666
|
-
"status": "disabled",
|
|
667
|
-
"optedOut": true,
|
|
668
|
-
"testResult": "Encryption explicitly disabled (FRIGG_ENCRYPTION_DISABLED=true); sensitive fields are stored in plaintext"
|
|
621
|
+
"reason": "Encryption bypassed for stage: development"
|
|
669
622
|
}
|
|
670
623
|
```
|
|
671
624
|
|
|
@@ -779,10 +732,9 @@ FRIGG_LOG_LEVEL=INFO
|
|
|
779
732
|
**Check environment variables:**
|
|
780
733
|
|
|
781
734
|
```bash
|
|
735
|
+
echo $STAGE # Should be 'production' (not dev/test/local)
|
|
782
736
|
echo $KMS_KEY_ARN # Should be set (for KMS)
|
|
783
|
-
echo $AES_KEY_ID
|
|
784
|
-
echo $STAGE # Local runs only: dev/test/local skip encryption
|
|
785
|
-
echo $FRIGG_ENCRYPTION_DISABLED # Should be unset
|
|
737
|
+
echo $AES_KEY_ID # Should be set (for AES)
|
|
786
738
|
```
|
|
787
739
|
|
|
788
740
|
**Check console logs:**
|
|
@@ -797,22 +749,6 @@ or
|
|
|
797
749
|
[Frigg] Field-level encryption disabled
|
|
798
750
|
```
|
|
799
751
|
|
|
800
|
-
### Deployed Stage Refuses to Start
|
|
801
|
-
|
|
802
|
-
**Error: "EncryptionConfigurationError: [Frigg] No field-level encryption key is configured for stage ..."**
|
|
803
|
-
|
|
804
|
-
The function runs in AWS and has neither `KMS_KEY_ARN` nor `AES_KEY_ID` + `AES_KEY`. Earlier versions skipped encryption on stages named `dev`, `test` or `local` and wrote plaintext when no key was set; deployed stages now refuse instead. Fix one of:
|
|
805
|
-
|
|
806
|
-
1. Set `encryption: { fieldLevelEncryptionMethod: 'kms' }` in the app definition and redeploy. Frigg creates or discovers a KMS key and sets `KMS_KEY_ARN` on every function, whatever the stage name.
|
|
807
|
-
2. Provide `AES_KEY_ID` and `AES_KEY` (32 characters) through the app definition's `environment` and the deploy environment.
|
|
808
|
-
3. To keep the stage in plaintext on purpose, set `encryption: { fieldLevelEncryptionMethod: 'none' }` (or `FRIGG_ENCRYPTION_DISABLED=true`).
|
|
809
|
-
|
|
810
|
-
Data written in plaintext before the key existed stays readable: reads leave values that are not in the encrypted `keyId:iv:cipher:encKey` format untouched, and the next write of each record encrypts it. Records that are never rewritten stay in plaintext until you re-save them.
|
|
811
|
-
|
|
812
|
-
**Error: "[Frigg] AES_KEY_ID is set but AES_KEY is not"**
|
|
813
|
-
|
|
814
|
-
Provide `AES_KEY` alongside `AES_KEY_ID`, or switch to KMS.
|
|
815
|
-
|
|
816
752
|
### AWS KMS Errors
|
|
817
753
|
|
|
818
754
|
**Error: "User is not authorized to perform: kms:GenerateDataKey"**
|