@friggframework/core 2.0.0-next.45 → 2.0.0-next.46
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 +28 -0
- package/application/commands/integration-commands.js +19 -0
- package/core/Worker.js +8 -21
- package/credential/repositories/credential-repository-mongo.js +14 -8
- package/credential/repositories/credential-repository-postgres.js +14 -8
- package/credential/repositories/credential-repository.js +3 -8
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +11 -2
- package/database/models/WebsocketConnection.js +11 -10
- package/database/prisma.js +63 -3
- package/database/repositories/health-check-repository-mongodb.js +3 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +3 -2
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/run-database-migration-use-case.js +137 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +91 -0
- package/database/utils/mongodb-schema-init.js +106 -0
- package/database/utils/prisma-runner.js +400 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/encrypt/Cryptor.js +14 -16
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +4 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +4 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +334 -0
- package/generated/prisma-mongodb/index-browser.js +316 -0
- package/generated/prisma-mongodb/index.d.ts +22897 -0
- package/generated/prisma-mongodb/index.js +359 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
- package/generated/prisma-mongodb/runtime/binary.js +289 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
- package/generated/prisma-mongodb/runtime/edge.js +34 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
- package/generated/prisma-mongodb/runtime/react-native.js +83 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-mongodb/schema.prisma +362 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +341 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +4 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +4 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +356 -0
- package/generated/prisma-postgresql/index-browser.js +338 -0
- package/generated/prisma-postgresql/index.d.ts +25071 -0
- package/generated/prisma-postgresql/index.js +381 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
- package/generated/prisma-postgresql/runtime/binary.js +289 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
- package/generated/prisma-postgresql/runtime/edge.js +34 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
- package/generated/prisma-postgresql/runtime/react-native.js +83 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-postgresql/schema.prisma +345 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +363 -0
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/routers/auth.js +1 -1
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +256 -0
- package/handlers/routers/health.js +41 -6
- package/handlers/routers/integration-webhook-routers.js +2 -2
- package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
- package/handlers/workers/db-migration.js +352 -0
- package/index.js +12 -0
- package/integrations/integration-router.js +60 -70
- package/integrations/repositories/integration-repository-interface.js +12 -0
- package/integrations/repositories/integration-repository-mongo.js +32 -0
- package/integrations/repositories/integration-repository-postgres.js +33 -0
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/test-integration-repository.js +2 -2
- package/logs/logger.js +0 -4
- package/modules/entity.js +0 -1
- package/modules/repositories/module-repository-mongo.js +3 -12
- package/modules/repositories/module-repository-postgres.js +0 -11
- package/modules/repositories/module-repository.js +1 -12
- package/modules/use-cases/get-entity-options-by-id.js +1 -1
- package/modules/use-cases/get-module.js +1 -2
- package/modules/use-cases/refresh-entity-options.js +1 -1
- package/modules/use-cases/test-module-auth.js +1 -1
- package/package.json +82 -66
- package/prisma-mongodb/schema.prisma +21 -21
- package/prisma-postgresql/schema.prisma +15 -15
- package/queues/queuer-util.js +24 -21
- package/types/core/index.d.ts +2 -2
- package/types/module-plugin/index.d.ts +0 -2
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
- package/user/user.js +16 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
- package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
- package/websocket/repositories/websocket-connection-repository.js +11 -10
- package/application/commands/integration-commands.test.js +0 -123
- package/database/encryption/encryption-integration.test.js +0 -553
- package/database/encryption/encryption-schema-registry.test.js +0 -392
- package/database/encryption/field-encryption-service.test.js +0 -525
- package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
- package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
- package/database/encryption/postgres-relation-decryption.test.js +0 -245
- package/database/encryption/prisma-encryption-extension.test.js +0 -439
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/auth-flow.integration.test.js +0 -147
- package/handlers/integration-event-dispatcher.test.js +0 -209
- package/handlers/routers/health.test.js +0 -210
- package/handlers/routers/integration-webhook-routers.test.js +0 -126
- package/handlers/webhook-flow.integration.test.js +0 -356
- package/handlers/workers/integration-defined-workers.test.js +0 -184
- package/integrations/tests/use-cases/create-integration.test.js +0 -131
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
- package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
- package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
- package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
- package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
- package/integrations/tests/use-cases/update-integration.test.js +0 -141
- package/integrations/use-cases/create-process.test.js +0 -178
- package/integrations/use-cases/get-process.test.js +0 -190
- package/integrations/use-cases/load-integration-context-full.test.js +0 -329
- package/integrations/use-cases/load-integration-context.test.js +0 -114
- package/integrations/use-cases/update-process-metrics.test.js +0 -308
- package/integrations/use-cases/update-process-state.test.js +0 -256
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/modules/module-hydration.test.js +0 -205
- package/modules/requester/requester.test.js +0 -28
- package/user/tests/use-cases/create-individual-user.test.js +0 -24
- package/user/tests/use-cases/create-organization-user.test.js +0 -28
- package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
- package/user/tests/use-cases/login-user.test.js +0 -220
- package/user/tests/user-password-encryption-isolation.test.js +0 -237
- package/user/tests/user-password-hashing.test.js +0 -235
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// Migration from Mongoose ODM to Prisma ORM
|
|
4
4
|
|
|
5
5
|
generator client {
|
|
6
|
-
provider
|
|
7
|
-
output
|
|
6
|
+
provider = "prisma-client-js"
|
|
7
|
+
output = "../generated/prisma-mongodb"
|
|
8
|
+
binaryTargets = ["native", "rhel-openssl-3.0.x"] // native for local dev, rhel for Lambda deployment
|
|
9
|
+
engineType = "binary" // Use binary engines (smaller size)
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
datasource db {
|
|
@@ -19,8 +21,8 @@ datasource db {
|
|
|
19
21
|
/// User model with discriminator pattern support
|
|
20
22
|
/// Replaces Mongoose discriminators (IndividualUser, OrganizationUser)
|
|
21
23
|
model User {
|
|
22
|
-
id
|
|
23
|
-
type
|
|
24
|
+
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
25
|
+
type UserType
|
|
24
26
|
|
|
25
27
|
// Timestamps
|
|
26
28
|
createdAt DateTime @default(now())
|
|
@@ -87,12 +89,11 @@ model Token {
|
|
|
87
89
|
/// OAuth credentials and API tokens
|
|
88
90
|
/// All sensitive data encrypted with KMS at rest
|
|
89
91
|
model Credential {
|
|
90
|
-
id
|
|
91
|
-
userId
|
|
92
|
-
user
|
|
93
|
-
subType String?
|
|
92
|
+
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
93
|
+
userId String? @db.ObjectId
|
|
94
|
+
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
94
95
|
authIsValid Boolean?
|
|
95
|
-
externalId
|
|
96
|
+
externalId String?
|
|
96
97
|
|
|
97
98
|
// Dynamic OAuth fields stored as JSON (encrypted via Prisma middleware)
|
|
98
99
|
// Contains: access_token, refresh_token, domain, expires_in, token_type, etc.
|
|
@@ -114,7 +115,6 @@ model Entity {
|
|
|
114
115
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
115
116
|
credentialId String? @db.ObjectId
|
|
116
117
|
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)
|
|
117
|
-
subType String?
|
|
118
118
|
userId String? @db.ObjectId
|
|
119
119
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
120
120
|
name String?
|
|
@@ -125,11 +125,11 @@ model Entity {
|
|
|
125
125
|
updatedAt DateTime @updatedAt
|
|
126
126
|
|
|
127
127
|
// Relations - many-to-many with scalar lists
|
|
128
|
-
integrations
|
|
129
|
-
integrationIds
|
|
128
|
+
integrations Integration[] @relation("IntegrationEntities", fields: [integrationIds], references: [id])
|
|
129
|
+
integrationIds String[] @db.ObjectId
|
|
130
130
|
|
|
131
|
-
syncs
|
|
132
|
-
syncIds
|
|
131
|
+
syncs Sync[] @relation("SyncEntities", fields: [syncIds], references: [id])
|
|
132
|
+
syncIds String[] @db.ObjectId
|
|
133
133
|
|
|
134
134
|
dataIdentifiers DataIdentifier[]
|
|
135
135
|
associationObjects AssociationObject[]
|
|
@@ -153,7 +153,7 @@ model Integration {
|
|
|
153
153
|
status IntegrationStatus @default(ENABLED)
|
|
154
154
|
|
|
155
155
|
// Configuration and version
|
|
156
|
-
config Json?
|
|
156
|
+
config Json? // Integration configuration object
|
|
157
157
|
version String?
|
|
158
158
|
|
|
159
159
|
// Entity references (many-to-many via explicit scalar list)
|
|
@@ -320,11 +320,11 @@ model Association {
|
|
|
320
320
|
/// Association object entry
|
|
321
321
|
/// Replaces nested array structure in Mongoose
|
|
322
322
|
model AssociationObject {
|
|
323
|
-
id String
|
|
324
|
-
associationId String
|
|
325
|
-
association Association
|
|
326
|
-
entityId String
|
|
327
|
-
entity Entity
|
|
323
|
+
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
324
|
+
associationId String @db.ObjectId
|
|
325
|
+
association Association @relation(fields: [associationId], references: [id], onDelete: Cascade)
|
|
326
|
+
entityId String @db.ObjectId
|
|
327
|
+
entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)
|
|
328
328
|
objectType String
|
|
329
329
|
objId String
|
|
330
330
|
metadata Json? // Optional metadata
|
|
@@ -359,4 +359,4 @@ model WebsocketConnection {
|
|
|
359
359
|
|
|
360
360
|
@@index([connectionId])
|
|
361
361
|
@@map("WebsocketConnection")
|
|
362
|
-
}
|
|
362
|
+
}
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// Converted from MongoDB schema for relational database support
|
|
4
4
|
|
|
5
5
|
generator client {
|
|
6
|
-
provider
|
|
7
|
-
output
|
|
6
|
+
provider = "prisma-client-js"
|
|
7
|
+
output = "../generated/prisma-postgresql"
|
|
8
|
+
binaryTargets = ["native", "rhel-openssl-3.0.x"] // native for local dev, rhel for Lambda deployment
|
|
9
|
+
engineType = "binary" // Use binary engines (smaller size)
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
datasource db {
|
|
@@ -19,8 +21,8 @@ datasource db {
|
|
|
19
21
|
/// User model with discriminator pattern support
|
|
20
22
|
/// Replaces Mongoose discriminators (IndividualUser, OrganizationUser)
|
|
21
23
|
model User {
|
|
22
|
-
id
|
|
23
|
-
type
|
|
24
|
+
id Int @id @default(autoincrement())
|
|
25
|
+
type UserType
|
|
24
26
|
|
|
25
27
|
// Timestamps
|
|
26
28
|
createdAt DateTime @default(now())
|
|
@@ -85,12 +87,11 @@ model Token {
|
|
|
85
87
|
/// OAuth credentials and API tokens
|
|
86
88
|
/// All sensitive data encrypted with KMS at rest
|
|
87
89
|
model Credential {
|
|
88
|
-
id
|
|
89
|
-
userId
|
|
90
|
-
user
|
|
91
|
-
subType String?
|
|
90
|
+
id Int @id @default(autoincrement())
|
|
91
|
+
userId Int?
|
|
92
|
+
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
92
93
|
authIsValid Boolean?
|
|
93
|
-
externalId
|
|
94
|
+
externalId String?
|
|
94
95
|
|
|
95
96
|
// Dynamic OAuth fields stored as JSON (encrypted via Prisma middleware)
|
|
96
97
|
// Contains: access_token, refresh_token, domain, expires_in, token_type, etc.
|
|
@@ -111,7 +112,6 @@ model Entity {
|
|
|
111
112
|
id Int @id @default(autoincrement())
|
|
112
113
|
credentialId Int?
|
|
113
114
|
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)
|
|
114
|
-
subType String?
|
|
115
115
|
userId Int?
|
|
116
116
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
117
117
|
name String?
|
|
@@ -146,7 +146,7 @@ model Integration {
|
|
|
146
146
|
status IntegrationStatus @default(ENABLED)
|
|
147
147
|
|
|
148
148
|
// Configuration and version
|
|
149
|
-
config Json?
|
|
149
|
+
config Json? // Integration configuration object
|
|
150
150
|
version String?
|
|
151
151
|
|
|
152
152
|
// Entity references (many-to-many via implicit join table)
|
|
@@ -225,11 +225,11 @@ model Sync {
|
|
|
225
225
|
/// Data identifier for sync operations
|
|
226
226
|
/// Replaces nested array structure in Mongoose
|
|
227
227
|
model DataIdentifier {
|
|
228
|
-
id Int
|
|
228
|
+
id Int @id @default(autoincrement())
|
|
229
229
|
syncId Int?
|
|
230
|
-
sync Sync?
|
|
230
|
+
sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)
|
|
231
231
|
entityId Int
|
|
232
|
-
entity Entity
|
|
232
|
+
entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)
|
|
233
233
|
|
|
234
234
|
// Identifier data (can be any structure)
|
|
235
235
|
idData Json
|
|
@@ -332,7 +332,7 @@ model Process {
|
|
|
332
332
|
|
|
333
333
|
/// Generic state storage
|
|
334
334
|
model State {
|
|
335
|
-
id Int
|
|
335
|
+
id Int @id @default(autoincrement())
|
|
336
336
|
state Json?
|
|
337
337
|
}
|
|
338
338
|
|
package/queues/queuer-util.js
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
const { v4: uuid } = require('uuid');
|
|
2
|
-
const
|
|
2
|
+
const { SQSClient, SendMessageCommand, SendMessageBatchCommand } = require('@aws-sdk/client-sqs');
|
|
3
|
+
|
|
3
4
|
const awsConfigOptions = () => {
|
|
4
5
|
const config = {};
|
|
5
6
|
if (process.env.IS_OFFLINE) {
|
|
6
7
|
console.log('Running in offline mode');
|
|
8
|
+
config.credentials = {
|
|
9
|
+
accessKeyId: 'test-aws-key',
|
|
10
|
+
secretAccessKey: 'test-aws-secret',
|
|
11
|
+
};
|
|
12
|
+
config.region = 'us-east-1';
|
|
7
13
|
}
|
|
8
14
|
if (process.env.AWS_ENDPOINT) {
|
|
9
15
|
config.endpoint = process.env.AWS_ENDPOINT;
|
|
10
16
|
}
|
|
11
17
|
return config;
|
|
12
18
|
};
|
|
13
|
-
|
|
14
|
-
const sqs = new
|
|
19
|
+
|
|
20
|
+
const sqs = new SQSClient(awsConfigOptions());
|
|
15
21
|
|
|
16
22
|
const QueuerUtil = {
|
|
17
23
|
send: async (message, queueUrl) => {
|
|
18
24
|
console.log(`Enqueuing message to SQS queue ${queueUrl}`);
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.promise();
|
|
25
|
+
const command = new SendMessageCommand({
|
|
26
|
+
MessageBody: JSON.stringify(message),
|
|
27
|
+
QueueUrl: queueUrl,
|
|
28
|
+
});
|
|
29
|
+
return sqs.send(command);
|
|
25
30
|
},
|
|
26
31
|
|
|
27
32
|
batchSend: async (entries = [], queueUrl) => {
|
|
@@ -39,12 +44,11 @@ const QueuerUtil = {
|
|
|
39
44
|
// Sends 10, then purges the buffer
|
|
40
45
|
if (buffer.length === batchSize) {
|
|
41
46
|
console.log('Buffer at 10, sending batch');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.promise();
|
|
47
|
+
const command = new SendMessageBatchCommand({
|
|
48
|
+
Entries: buffer,
|
|
49
|
+
QueueUrl: queueUrl,
|
|
50
|
+
});
|
|
51
|
+
await sqs.send(command);
|
|
48
52
|
// Purge the buffer
|
|
49
53
|
buffer.splice(0, buffer.length);
|
|
50
54
|
}
|
|
@@ -54,12 +58,11 @@ const QueuerUtil = {
|
|
|
54
58
|
// If any remaining entries under 10 are left in the buffer, send and return
|
|
55
59
|
if (buffer.length > 0) {
|
|
56
60
|
console.log(buffer);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.promise();
|
|
61
|
+
const command = new SendMessageBatchCommand({
|
|
62
|
+
Entries: buffer,
|
|
63
|
+
QueueUrl: queueUrl,
|
|
64
|
+
});
|
|
65
|
+
return sqs.send(command);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// If we're exact... just return an empty object for now
|
package/types/core/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare module "@friggframework/core" {
|
|
2
|
-
import {
|
|
2
|
+
import type { SendMessageCommandInput } from "@aws-sdk/client-sqs";
|
|
3
3
|
|
|
4
4
|
export class Delegate implements IFriggDelegate {
|
|
5
5
|
delegate: any;
|
|
@@ -50,5 +50,5 @@ declare module "@friggframework/core" {
|
|
|
50
50
|
QueueOwnerAWSAccountId?: string;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
type SendSQSMessageParams =
|
|
53
|
+
type SendSQSMessageParams = SendMessageCommandInput;
|
|
54
54
|
}
|
|
@@ -5,7 +5,6 @@ declare module "@friggframework/module-plugin" {
|
|
|
5
5
|
export class Credential extends Model {
|
|
6
6
|
userId: string;
|
|
7
7
|
authIsValid: boolean;
|
|
8
|
-
subType: string;
|
|
9
8
|
externalId: string;
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -13,7 +12,6 @@ declare module "@friggframework/module-plugin" {
|
|
|
13
12
|
|
|
14
13
|
export class Entity extends Model {
|
|
15
14
|
credentialId: string;
|
|
16
|
-
subType: string;
|
|
17
15
|
userId: string;
|
|
18
16
|
name: string;
|
|
19
17
|
externalId: string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Use case for authenticating a user using multiple authentication strategies.
|
|
5
|
+
*
|
|
6
|
+
* Supports three authentication modes in priority order:
|
|
7
|
+
* 1. Shared Secret (backend-to-backend with x-frigg-api-key + x-frigg headers)
|
|
8
|
+
* 2. Adopter JWT (custom JWT authentication)
|
|
9
|
+
* 3. Frigg Native Token (bearer token from /user/login)
|
|
10
|
+
*
|
|
11
|
+
* x-frigg-appUserId and x-frigg-appOrgId headers are automatically supported
|
|
12
|
+
* for user identification with any auth mode. When present with JWT or Frigg
|
|
13
|
+
* tokens, they are validated to match the authenticated user.
|
|
14
|
+
*
|
|
15
|
+
* @class AuthenticateUser
|
|
16
|
+
*/
|
|
17
|
+
class AuthenticateUser {
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new AuthenticateUser instance.
|
|
20
|
+
* @param {Object} params - Configuration parameters.
|
|
21
|
+
* @param {import('./get-user-from-bearer-token').GetUserFromBearerToken} params.getUserFromBearerToken - Use case for bearer token auth.
|
|
22
|
+
* @param {import('./get-user-from-x-frigg-headers').GetUserFromXFriggHeaders} params.getUserFromXFriggHeaders - Use case for x-frigg header auth.
|
|
23
|
+
* @param {import('./get-user-from-adopter-jwt').GetUserFromAdopterJwt} params.getUserFromAdopterJwt - Use case for adopter JWT auth.
|
|
24
|
+
* @param {import('./authenticate-with-shared-secret').AuthenticateWithSharedSecret} params.authenticateWithSharedSecret - Use case for validating shared secret.
|
|
25
|
+
* @param {Object} params.userConfig - The user config in the app definition.
|
|
26
|
+
*/
|
|
27
|
+
constructor({
|
|
28
|
+
getUserFromBearerToken,
|
|
29
|
+
getUserFromXFriggHeaders,
|
|
30
|
+
getUserFromAdopterJwt,
|
|
31
|
+
authenticateWithSharedSecret,
|
|
32
|
+
userConfig,
|
|
33
|
+
}) {
|
|
34
|
+
this.getUserFromBearerToken = getUserFromBearerToken;
|
|
35
|
+
this.getUserFromXFriggHeaders = getUserFromXFriggHeaders;
|
|
36
|
+
this.getUserFromAdopterJwt = getUserFromAdopterJwt;
|
|
37
|
+
this.authenticateWithSharedSecret = authenticateWithSharedSecret;
|
|
38
|
+
this.userConfig = userConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Executes the use case.
|
|
43
|
+
* @async
|
|
44
|
+
* @param {Object} req - Express request object with headers.
|
|
45
|
+
* @returns {Promise<import('../user').User>} The authenticated user object.
|
|
46
|
+
* @throws {Boom} Unauthorized if no valid authentication provided.
|
|
47
|
+
* @throws {Boom} Forbidden if x-frigg headers don't match authenticated user.
|
|
48
|
+
*/
|
|
49
|
+
async execute(req) {
|
|
50
|
+
const authModes = this.userConfig.authModes || { friggToken: true };
|
|
51
|
+
const appUserId = req.headers['x-frigg-appuserid'];
|
|
52
|
+
const appOrgId = req.headers['x-frigg-apporgid'];
|
|
53
|
+
let user = null;
|
|
54
|
+
|
|
55
|
+
// Priority 1: Shared Secret (backend-to-backend with API key)
|
|
56
|
+
if (authModes.sharedSecret !== false) {
|
|
57
|
+
const apiKey = req.headers['x-frigg-api-key'];
|
|
58
|
+
if (apiKey) {
|
|
59
|
+
// Validate the API key (authentication)
|
|
60
|
+
await this.authenticateWithSharedSecret.execute(apiKey);
|
|
61
|
+
// Get user from x-frigg headers (authorization)
|
|
62
|
+
return await this.getUserFromXFriggHeaders.execute(
|
|
63
|
+
appUserId,
|
|
64
|
+
appOrgId
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Priority 2: Adopter JWT (if enabled)
|
|
70
|
+
if (
|
|
71
|
+
authModes.adopterJwt === true &&
|
|
72
|
+
req.headers.authorization?.startsWith('Bearer ')
|
|
73
|
+
) {
|
|
74
|
+
const token = req.headers.authorization.split(' ')[1];
|
|
75
|
+
// Detect JWT format (3 parts separated by dots)
|
|
76
|
+
if (token && token.split('.').length === 3) {
|
|
77
|
+
user = await this.getUserFromAdopterJwt.execute(token);
|
|
78
|
+
// Validate x-frigg headers match JWT claims if present
|
|
79
|
+
if (appUserId || appOrgId) {
|
|
80
|
+
this.validateUserMatch(user, appUserId, appOrgId);
|
|
81
|
+
}
|
|
82
|
+
return user;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Priority 3: Frigg native token (default)
|
|
87
|
+
if (authModes.friggToken !== false && req.headers.authorization) {
|
|
88
|
+
user = await this.getUserFromBearerToken.execute(
|
|
89
|
+
req.headers.authorization
|
|
90
|
+
);
|
|
91
|
+
// Validate x-frigg headers match token user if present
|
|
92
|
+
if (appUserId || appOrgId) {
|
|
93
|
+
this.validateUserMatch(user, appUserId, appOrgId);
|
|
94
|
+
}
|
|
95
|
+
return user;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw Boom.unauthorized('No valid authentication provided');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates that x-frigg headers match authenticated user if provided.
|
|
103
|
+
* This ensures that when both authentication (via token/JWT) and
|
|
104
|
+
* x-frigg headers are present, they refer to the same user.
|
|
105
|
+
*
|
|
106
|
+
* @param {import('../user').User} user - The authenticated user
|
|
107
|
+
* @param {string} [appUserId] - The x-frigg-appuserid header value
|
|
108
|
+
* @param {string} [appOrgId] - The x-frigg-apporgid header value
|
|
109
|
+
* @throws {Boom} 403 Forbidden if headers don't match user
|
|
110
|
+
*/
|
|
111
|
+
validateUserMatch(user, appUserId, appOrgId) {
|
|
112
|
+
if (appUserId && user.getAppUserId() !== appUserId) {
|
|
113
|
+
throw Boom.forbidden(
|
|
114
|
+
'x-frigg-appuserid header does not match authenticated user'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (appOrgId && user.getAppOrgId() !== appOrgId) {
|
|
118
|
+
throw Boom.forbidden(
|
|
119
|
+
'x-frigg-apporgid header does not match authenticated user'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { AuthenticateUser };
|
|
126
|
+
|
|
127
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Use case for authenticating requests with shared secret API key.
|
|
5
|
+
* This use case ONLY validates the authenticity of the request via API key.
|
|
6
|
+
* It does NOT retrieve user data - that's handled by GetUserFromXFriggHeaders.
|
|
7
|
+
*
|
|
8
|
+
* Used for backend-to-backend communication where the secret proves
|
|
9
|
+
* the request is legitimate, but user identification comes from x-frigg headers.
|
|
10
|
+
*
|
|
11
|
+
* @class AuthenticateWithSharedSecret
|
|
12
|
+
*/
|
|
13
|
+
class AuthenticateWithSharedSecret {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new AuthenticateWithSharedSecret instance.
|
|
16
|
+
* @param {Object} params - Configuration parameters (none needed currently, but kept for consistency).
|
|
17
|
+
*/
|
|
18
|
+
constructor() {
|
|
19
|
+
// No dependencies needed - just validates against env var
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validates the provided shared secret against FRIGG_API_KEY.
|
|
24
|
+
* @async
|
|
25
|
+
* @param {string} providedSecret - Secret from x-frigg-api-key header
|
|
26
|
+
* @returns {Promise<boolean>} True if valid (or throws error if invalid)
|
|
27
|
+
* @throws {Boom} 500 if FRIGG_API_KEY not configured
|
|
28
|
+
* @throws {Boom} 401 if provided secret doesn't match
|
|
29
|
+
*/
|
|
30
|
+
async execute(providedSecret) {
|
|
31
|
+
// Validate secret
|
|
32
|
+
const expectedSecret = process.env.FRIGG_API_KEY;
|
|
33
|
+
if (!expectedSecret) {
|
|
34
|
+
throw Boom.badImplementation(
|
|
35
|
+
'FRIGG_API_KEY environment variable is not configured. ' +
|
|
36
|
+
'Set FRIGG_API_KEY to enable shared secret authentication.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!providedSecret || providedSecret !== expectedSecret) {
|
|
41
|
+
throw Boom.unauthorized('Invalid API key');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { AuthenticateWithSharedSecret };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* STUB: Use case for retrieving a user from adopter-provided JWT token.
|
|
5
|
+
*
|
|
6
|
+
* This is a stub implementation for future JWT authentication support.
|
|
7
|
+
* When implemented, this will allow adopters to use their own JWT tokens
|
|
8
|
+
* for authentication instead of Frigg's native token system.
|
|
9
|
+
*
|
|
10
|
+
* FUTURE IMPLEMENTATION REQUIREMENTS:
|
|
11
|
+
* - Validate JWT signature using jwtConfig.secret from app definition
|
|
12
|
+
* - Support configurable signing algorithms (HS256, HS384, HS512, RS256, RS384, RS512)
|
|
13
|
+
* - Extract user identifiers from JWT claims based on jwtConfig.userIdClaim and jwtConfig.orgIdClaim
|
|
14
|
+
* - Find or create user based on extracted claim values
|
|
15
|
+
* - Handle token expiration and validation errors
|
|
16
|
+
* - Support refresh tokens (optional)
|
|
17
|
+
* - Validate user ID conflicts if both individual and org IDs present in JWT
|
|
18
|
+
*
|
|
19
|
+
* RECOMMENDED IMPLEMENTATION:
|
|
20
|
+
* - Use 'jsonwebtoken' package for JWT parsing and validation
|
|
21
|
+
* - Cache JWT public keys for RS* algorithms
|
|
22
|
+
* - Add comprehensive error handling for invalid tokens
|
|
23
|
+
* - Log authentication attempts for security auditing
|
|
24
|
+
*
|
|
25
|
+
* @todo Implement JWT validation with jsonwebtoken package
|
|
26
|
+
* @todo Add unit tests for JWT parsing and claim extraction
|
|
27
|
+
* @todo Document adopter JWT integration guide in Frigg docs
|
|
28
|
+
* @todo Add support for JWT refresh tokens
|
|
29
|
+
* @todo Implement JWT public key caching for RS* algorithms
|
|
30
|
+
*
|
|
31
|
+
* @class GetUserFromAdopterJwt
|
|
32
|
+
*/
|
|
33
|
+
class GetUserFromAdopterJwt {
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new GetUserFromAdopterJwt instance.
|
|
36
|
+
* @param {Object} params - Configuration parameters.
|
|
37
|
+
* @param {import('../repositories/user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
|
|
38
|
+
* @param {Object} params.userConfig - The user config in the app definition.
|
|
39
|
+
*/
|
|
40
|
+
constructor({ userRepository, userConfig }) {
|
|
41
|
+
this.userRepository = userRepository;
|
|
42
|
+
this.userConfig = userConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes the use case.
|
|
47
|
+
* @async
|
|
48
|
+
* @param {string} jwtToken - The JWT token from the Authorization header.
|
|
49
|
+
* @returns {Promise<import('../user').User>} The authenticated user object.
|
|
50
|
+
* @throws {Boom} 501 Not Implemented - This feature is not yet available.
|
|
51
|
+
*/
|
|
52
|
+
async execute(jwtToken) {
|
|
53
|
+
throw Boom.notImplemented(
|
|
54
|
+
'Adopter JWT authentication is not yet implemented. ' +
|
|
55
|
+
'This feature is planned for a future Frigg release. ' +
|
|
56
|
+
'Please use one of the supported authentication modes instead: ' +
|
|
57
|
+
'friggToken (native bearer token) or xFriggHeaders (backend-to-backend with x-frigg-appUserId/appOrgId headers).'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/* FUTURE IMPLEMENTATION PSEUDOCODE:
|
|
61
|
+
|
|
62
|
+
const jwt = require('jsonwebtoken');
|
|
63
|
+
|
|
64
|
+
// Validate JWT configuration exists
|
|
65
|
+
if (!this.userConfig.jwtConfig || !this.userConfig.jwtConfig.secret) {
|
|
66
|
+
throw Boom.badImplementation('JWT configuration is required when adopterJwt auth mode is enabled');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Verify and decode JWT
|
|
71
|
+
const decoded = jwt.verify(jwtToken, this.userConfig.jwtConfig.secret, {
|
|
72
|
+
algorithms: [this.userConfig.jwtConfig.algorithm || 'HS256']
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Extract user identifiers from claims
|
|
76
|
+
const appUserId = decoded[this.userConfig.jwtConfig.userIdClaim || 'sub'];
|
|
77
|
+
const appOrgId = decoded[this.userConfig.jwtConfig.orgIdClaim || 'org_id'];
|
|
78
|
+
|
|
79
|
+
// At least one identifier required
|
|
80
|
+
if (!appUserId && !appOrgId) {
|
|
81
|
+
throw Boom.badRequest('JWT must contain user or organization identifier claims');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find existing users
|
|
85
|
+
let individualUserData = null;
|
|
86
|
+
let organizationUserData = null;
|
|
87
|
+
|
|
88
|
+
if (appUserId) {
|
|
89
|
+
individualUserData = await this.userRepository.findIndividualUserByAppUserId(appUserId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (appOrgId) {
|
|
93
|
+
organizationUserData = await this.userRepository.findOrganizationUserByAppOrgId(appOrgId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate no conflicts if both IDs present
|
|
97
|
+
if (appUserId && appOrgId && individualUserData && organizationUserData) {
|
|
98
|
+
const individualOrgId = individualUserData.organizationUser?.toString();
|
|
99
|
+
const expectedOrgId = organizationUserData.id?.toString();
|
|
100
|
+
|
|
101
|
+
if (individualOrgId !== expectedOrgId) {
|
|
102
|
+
throw Boom.badRequest(
|
|
103
|
+
'User ID mismatch: JWT claims refer to different users. ' +
|
|
104
|
+
'Individual and organization IDs must belong to the same user.'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Auto-create if not found
|
|
110
|
+
if (!individualUserData && !organizationUserData) {
|
|
111
|
+
if (appUserId) {
|
|
112
|
+
individualUserData = await this.userRepository.createIndividualUser({
|
|
113
|
+
appUserId,
|
|
114
|
+
username: `jwt-user-${appUserId}`,
|
|
115
|
+
email: decoded.email || `${appUserId}@jwt.local`,
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
organizationUserData = await this.userRepository.createOrganizationUser({
|
|
119
|
+
appOrgId,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return new User(
|
|
125
|
+
individualUserData,
|
|
126
|
+
organizationUserData,
|
|
127
|
+
this.userConfig.usePassword,
|
|
128
|
+
this.userConfig.primary,
|
|
129
|
+
this.userConfig.individualUserRequired,
|
|
130
|
+
this.userConfig.organizationUserRequired
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.name === 'TokenExpiredError') {
|
|
135
|
+
throw Boom.unauthorized('JWT token has expired');
|
|
136
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
137
|
+
throw Boom.unauthorized('Invalid JWT token');
|
|
138
|
+
} else if (error.isBoom) {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
throw Boom.unauthorized('JWT authentication failed');
|
|
142
|
+
}
|
|
143
|
+
*/
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { GetUserFromAdopterJwt };
|
|
148
|
+
|
|
149
|
+
|