@friggframework/core 2.0.0-next.44 → 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.
Files changed (166) hide show
  1. package/README.md +28 -0
  2. package/application/commands/integration-commands.js +19 -0
  3. package/core/Worker.js +8 -21
  4. package/credential/repositories/credential-repository-mongo.js +14 -8
  5. package/credential/repositories/credential-repository-postgres.js +14 -8
  6. package/credential/repositories/credential-repository.js +3 -8
  7. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  8. package/database/adapters/lambda-invoker.js +97 -0
  9. package/database/config.js +11 -2
  10. package/database/models/WebsocketConnection.js +11 -10
  11. package/database/prisma.js +63 -3
  12. package/database/repositories/health-check-repository-mongodb.js +3 -0
  13. package/database/repositories/migration-status-repository-s3.js +137 -0
  14. package/database/use-cases/check-database-state-use-case.js +81 -0
  15. package/database/use-cases/check-encryption-health-use-case.js +3 -2
  16. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  17. package/database/use-cases/get-migration-status-use-case.js +93 -0
  18. package/database/use-cases/run-database-migration-use-case.js +137 -0
  19. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  20. package/database/utils/mongodb-collection-utils.js +91 -0
  21. package/database/utils/mongodb-schema-init.js +106 -0
  22. package/database/utils/prisma-runner.js +400 -0
  23. package/database/utils/prisma-schema-parser.js +182 -0
  24. package/encrypt/Cryptor.js +14 -16
  25. package/generated/prisma-mongodb/client.d.ts +1 -0
  26. package/generated/prisma-mongodb/client.js +4 -0
  27. package/generated/prisma-mongodb/default.d.ts +1 -0
  28. package/generated/prisma-mongodb/default.js +4 -0
  29. package/generated/prisma-mongodb/edge.d.ts +1 -0
  30. package/generated/prisma-mongodb/edge.js +334 -0
  31. package/generated/prisma-mongodb/index-browser.js +316 -0
  32. package/generated/prisma-mongodb/index.d.ts +22897 -0
  33. package/generated/prisma-mongodb/index.js +359 -0
  34. package/generated/prisma-mongodb/package.json +183 -0
  35. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  36. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  37. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  38. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  39. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  40. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  41. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  42. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  43. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  44. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  45. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  46. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  47. package/generated/prisma-mongodb/schema.prisma +362 -0
  48. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  49. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  50. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  51. package/generated/prisma-mongodb/wasm.js +341 -0
  52. package/generated/prisma-postgresql/client.d.ts +1 -0
  53. package/generated/prisma-postgresql/client.js +4 -0
  54. package/generated/prisma-postgresql/default.d.ts +1 -0
  55. package/generated/prisma-postgresql/default.js +4 -0
  56. package/generated/prisma-postgresql/edge.d.ts +1 -0
  57. package/generated/prisma-postgresql/edge.js +356 -0
  58. package/generated/prisma-postgresql/index-browser.js +338 -0
  59. package/generated/prisma-postgresql/index.d.ts +25071 -0
  60. package/generated/prisma-postgresql/index.js +381 -0
  61. package/generated/prisma-postgresql/package.json +183 -0
  62. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  63. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  64. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  65. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  66. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  67. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  68. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  69. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  70. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  71. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  72. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  73. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  74. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  75. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  76. package/generated/prisma-postgresql/schema.prisma +345 -0
  77. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  78. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  79. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  80. package/generated/prisma-postgresql/wasm.js +363 -0
  81. package/handlers/WEBHOOKS.md +653 -0
  82. package/handlers/backend-utils.js +118 -3
  83. package/handlers/database-migration-handler.js +227 -0
  84. package/handlers/routers/auth.js +1 -1
  85. package/handlers/routers/db-migration.handler.js +29 -0
  86. package/handlers/routers/db-migration.js +256 -0
  87. package/handlers/routers/health.js +41 -6
  88. package/handlers/routers/integration-webhook-routers.js +67 -0
  89. package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
  90. package/handlers/workers/db-migration.js +352 -0
  91. package/index.js +28 -0
  92. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  93. package/integrations/integration-base.js +74 -3
  94. package/integrations/integration-router.js +60 -70
  95. package/integrations/repositories/integration-repository-interface.js +12 -0
  96. package/integrations/repositories/integration-repository-mongo.js +32 -0
  97. package/integrations/repositories/integration-repository-postgres.js +33 -0
  98. package/integrations/repositories/process-repository-postgres.js +43 -20
  99. package/integrations/tests/doubles/dummy-integration-class.js +1 -8
  100. package/integrations/tests/doubles/test-integration-repository.js +2 -2
  101. package/logs/logger.js +0 -4
  102. package/modules/entity.js +0 -1
  103. package/modules/repositories/module-repository-mongo.js +3 -12
  104. package/modules/repositories/module-repository-postgres.js +0 -11
  105. package/modules/repositories/module-repository.js +1 -12
  106. package/modules/use-cases/get-entity-options-by-id.js +1 -1
  107. package/modules/use-cases/get-module.js +1 -2
  108. package/modules/use-cases/refresh-entity-options.js +1 -1
  109. package/modules/use-cases/test-module-auth.js +1 -1
  110. package/package.json +82 -66
  111. package/prisma-mongodb/schema.prisma +21 -21
  112. package/prisma-postgresql/schema.prisma +15 -15
  113. package/queues/queuer-util.js +28 -15
  114. package/types/core/index.d.ts +2 -2
  115. package/types/module-plugin/index.d.ts +0 -2
  116. package/user/repositories/user-repository-mongo.js +53 -12
  117. package/user/repositories/user-repository-postgres.js +53 -14
  118. package/user/use-cases/authenticate-user.js +127 -0
  119. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  120. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  121. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  122. package/user/use-cases/login-user.js +1 -1
  123. package/user/user.js +18 -2
  124. package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
  125. package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
  126. package/websocket/repositories/websocket-connection-repository.js +11 -10
  127. package/application/commands/integration-commands.test.js +0 -123
  128. package/database/encryption/encryption-integration.test.js +0 -553
  129. package/database/encryption/encryption-schema-registry.test.js +0 -392
  130. package/database/encryption/field-encryption-service.test.js +0 -525
  131. package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
  132. package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
  133. package/database/encryption/postgres-relation-decryption.test.js +0 -245
  134. package/database/encryption/prisma-encryption-extension.test.js +0 -439
  135. package/errors/base-error.test.js +0 -32
  136. package/errors/fetch-error.test.js +0 -79
  137. package/errors/halt-error.test.js +0 -11
  138. package/errors/validation-errors.test.js +0 -120
  139. package/handlers/auth-flow.integration.test.js +0 -147
  140. package/handlers/integration-event-dispatcher.test.js +0 -141
  141. package/handlers/routers/health.test.js +0 -210
  142. package/integrations/tests/use-cases/create-integration.test.js +0 -131
  143. package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
  144. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
  145. package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
  146. package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
  147. package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
  148. package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
  149. package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
  150. package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
  151. package/integrations/tests/use-cases/update-integration.test.js +0 -141
  152. package/integrations/use-cases/create-process.test.js +0 -178
  153. package/integrations/use-cases/get-process.test.js +0 -190
  154. package/integrations/use-cases/load-integration-context-full.test.js +0 -329
  155. package/integrations/use-cases/load-integration-context.test.js +0 -114
  156. package/integrations/use-cases/update-process-metrics.test.js +0 -308
  157. package/integrations/use-cases/update-process-state.test.js +0 -256
  158. package/lambda/TimeoutCatcher.test.js +0 -68
  159. package/logs/logger.test.js +0 -76
  160. package/modules/module-hydration.test.js +0 -205
  161. package/modules/requester/requester.test.js +0 -28
  162. package/user/tests/use-cases/create-individual-user.test.js +0 -24
  163. package/user/tests/use-cases/create-organization-user.test.js +0 -28
  164. package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
  165. package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
  166. package/user/tests/use-cases/login-user.test.js +0 -140
@@ -3,8 +3,10 @@
3
3
  // Migration from Mongoose ODM to Prisma ORM
4
4
 
5
5
  generator client {
6
- provider = "prisma-client-js"
7
- output = "../node_modules/@prisma-mongodb/client"
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 String @id @default(auto()) @map("_id") @db.ObjectId
23
- type UserType
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 String @id @default(auto()) @map("_id") @db.ObjectId
91
- userId String? @db.ObjectId
92
- user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
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 String?
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 Integration[] @relation("IntegrationEntities", fields: [integrationIds], references: [id])
129
- integrationIds String[] @db.ObjectId
128
+ integrations Integration[] @relation("IntegrationEntities", fields: [integrationIds], references: [id])
129
+ integrationIds String[] @db.ObjectId
130
130
 
131
- syncs Sync[] @relation("SyncEntities", fields: [syncIds], references: [id])
132
- syncIds String[] @db.ObjectId
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? // Integration configuration object
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 @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)
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 = "prisma-client-js"
7
- output = "../node_modules/@prisma-postgresql/client"
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 Int @id @default(autoincrement())
23
- type UserType
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 Int @id @default(autoincrement())
89
- userId Int?
90
- user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
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 String?
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? // Integration configuration object
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 @id @default(autoincrement())
228
+ id Int @id @default(autoincrement())
229
229
  syncId Int?
230
- sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)
230
+ sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)
231
231
  entityId Int
232
- entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)
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 @id @default(autoincrement())
335
+ id Int @id @default(autoincrement())
336
336
  state Json?
337
337
  }
338
338
 
@@ -1,19 +1,34 @@
1
1
  const { v4: uuid } = require('uuid');
2
- const AWS = require('aws-sdk');
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
- AWS.config.update(awsConfigOptions());
14
- const sqs = new AWS.SQS();
19
+
20
+ const sqs = new SQSClient(awsConfigOptions());
15
21
 
16
22
  const QueuerUtil = {
23
+ send: async (message, queueUrl) => {
24
+ console.log(`Enqueuing message to SQS queue ${queueUrl}`);
25
+ const command = new SendMessageCommand({
26
+ MessageBody: JSON.stringify(message),
27
+ QueueUrl: queueUrl,
28
+ });
29
+ return sqs.send(command);
30
+ },
31
+
17
32
  batchSend: async (entries = [], queueUrl) => {
18
33
  console.log(
19
34
  `Enqueuing ${entries.length} entries on SQS to queue ${queueUrl}`
@@ -29,12 +44,11 @@ const QueuerUtil = {
29
44
  // Sends 10, then purges the buffer
30
45
  if (buffer.length === batchSize) {
31
46
  console.log('Buffer at 10, sending batch');
32
- await sqs
33
- .sendMessageBatch({
34
- Entries: buffer,
35
- QueueUrl: queueUrl,
36
- })
37
- .promise();
47
+ const command = new SendMessageBatchCommand({
48
+ Entries: buffer,
49
+ QueueUrl: queueUrl,
50
+ });
51
+ await sqs.send(command);
38
52
  // Purge the buffer
39
53
  buffer.splice(0, buffer.length);
40
54
  }
@@ -44,12 +58,11 @@ const QueuerUtil = {
44
58
  // If any remaining entries under 10 are left in the buffer, send and return
45
59
  if (buffer.length > 0) {
46
60
  console.log(buffer);
47
- return sqs
48
- .sendMessageBatch({
49
- Entries: buffer,
50
- QueueUrl: queueUrl,
51
- })
52
- .promise();
61
+ const command = new SendMessageBatchCommand({
62
+ Entries: buffer,
63
+ QueueUrl: queueUrl,
64
+ });
65
+ return sqs.send(command);
53
66
  }
54
67
 
55
68
  // If we're exact... just return an empty object for now
@@ -1,5 +1,5 @@
1
1
  declare module "@friggframework/core" {
2
- import { SQS } from "aws-sdk";
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 = SQS.SendMessageRequest;
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;
@@ -1,4 +1,4 @@
1
- //todo: this repository is tightly coupled to the token repository.
1
+ const bcrypt = require('bcryptjs');
2
2
  const { prisma } = require('../../database/prisma');
3
3
  const {
4
4
  createTokenRepository,
@@ -95,19 +95,38 @@ class UserRepositoryMongo extends UserRepositoryInterface {
95
95
  * Replaces: IndividualUser.create(params)
96
96
  *
97
97
  * @param {Object} params - User creation parameters
98
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
98
99
  * @returns {Promise<Object>} Created user object with string IDs
99
100
  */
100
101
  async createIndividualUser(params) {
101
- return await this.prisma.user.create({
102
- data: {
103
- type: 'INDIVIDUAL',
104
- email: params.email,
105
- username: params.username,
106
- hashword: params.hashword,
107
- appUserId: params.appUserId,
108
- organizationId: params.organization || params.organizationId,
109
- },
110
- });
102
+ const data = {
103
+ type: 'INDIVIDUAL',
104
+ email: params.email,
105
+ username: params.username,
106
+ appUserId: params.appUserId,
107
+ organizationId: params.organization || params.organizationId,
108
+ };
109
+
110
+ if (
111
+ params.hashword !== undefined &&
112
+ params.hashword !== null &&
113
+ params.hashword !== ''
114
+ ) {
115
+ if (typeof params.hashword !== 'string') {
116
+ throw new Error('Password must be a string');
117
+ }
118
+
119
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
120
+ if (params.hashword.startsWith('$2')) {
121
+ throw new Error(
122
+ 'Password appears to be already hashed. Pass plain text password only.'
123
+ );
124
+ }
125
+
126
+ data.hashword = await bcrypt.hash(params.hashword, 10);
127
+ }
128
+
129
+ return await this.prisma.user.create({ data });
111
130
  }
112
131
 
113
132
  /**
@@ -204,12 +223,34 @@ class UserRepositoryMongo extends UserRepositoryInterface {
204
223
  * Update individual user
205
224
  * @param {string} userId - User ID
206
225
  * @param {Object} updates - Fields to update
226
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
207
227
  * @returns {Promise<Object>} Updated user object with string IDs
208
228
  */
209
229
  async updateIndividualUser(userId, updates) {
230
+ const data = { ...updates };
231
+
232
+ if (
233
+ data.hashword !== undefined &&
234
+ data.hashword !== null &&
235
+ data.hashword !== ''
236
+ ) {
237
+ if (typeof data.hashword !== 'string') {
238
+ throw new Error('Password must be a string');
239
+ }
240
+
241
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
242
+ if (data.hashword.startsWith('$2')) {
243
+ throw new Error(
244
+ 'Password appears to be already hashed. Pass plain text password only.'
245
+ );
246
+ }
247
+
248
+ data.hashword = await bcrypt.hash(data.hashword, 10);
249
+ }
250
+
210
251
  return await this.prisma.user.update({
211
252
  where: { id: userId },
212
- data: updates,
253
+ data,
213
254
  });
214
255
  }
215
256
 
@@ -1,4 +1,4 @@
1
- //todo: this repository is tightly coupled to the token repository.
1
+ const bcrypt = require('bcryptjs');
2
2
  const { prisma } = require('../../database/prisma');
3
3
  const {
4
4
  createTokenRepository,
@@ -130,21 +130,40 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
130
130
  * Replaces: IndividualUser.create(params)
131
131
  *
132
132
  * @param {Object} params - User creation parameters (with string IDs from application layer)
133
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
133
134
  * @returns {Promise<Object>} Created user object with string IDs
134
135
  */
135
136
  async createIndividualUser(params) {
136
- const user = await this.prisma.user.create({
137
- data: {
138
- type: 'INDIVIDUAL',
139
- email: params.email,
140
- username: params.username,
141
- hashword: params.hashword,
142
- appUserId: params.appUserId,
143
- organizationId: this._convertId(
144
- params.organization || params.organizationId
145
- ),
146
- },
147
- });
137
+ const data = {
138
+ type: 'INDIVIDUAL',
139
+ email: params.email,
140
+ username: params.username,
141
+ appUserId: params.appUserId,
142
+ organizationId: this._convertId(
143
+ params.organization || params.organizationId
144
+ ),
145
+ };
146
+
147
+ if (
148
+ params.hashword !== undefined &&
149
+ params.hashword !== null &&
150
+ params.hashword !== ''
151
+ ) {
152
+ if (typeof params.hashword !== 'string') {
153
+ throw new Error('Password must be a string');
154
+ }
155
+
156
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
157
+ if (params.hashword.startsWith('$2')) {
158
+ throw new Error(
159
+ 'Password appears to be already hashed. Pass plain text password only.'
160
+ );
161
+ }
162
+
163
+ data.hashword = await bcrypt.hash(params.hashword, 10);
164
+ }
165
+
166
+ const user = await this.prisma.user.create({ data });
148
167
  return this._convertUserIds(user);
149
168
  }
150
169
 
@@ -249,13 +268,14 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
249
268
  * Update individual user
250
269
  * @param {string} userId - User ID (string from application layer)
251
270
  * @param {Object} updates - Fields to update (with string IDs from application layer)
271
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
252
272
  * @returns {Promise<Object>} Updated user object with string IDs
253
273
  */
254
274
  async updateIndividualUser(userId, updates) {
255
275
  const intId = this._convertId(userId);
256
276
 
257
- // Convert organizationId if present in updates
258
277
  const data = { ...updates };
278
+
259
279
  if (data.organizationId !== undefined) {
260
280
  data.organizationId = this._convertId(data.organizationId);
261
281
  }
@@ -264,6 +284,25 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
264
284
  delete data.organization;
265
285
  }
266
286
 
287
+ if (
288
+ data.hashword !== undefined &&
289
+ data.hashword !== null &&
290
+ data.hashword !== ''
291
+ ) {
292
+ if (typeof data.hashword !== 'string') {
293
+ throw new Error('Password must be a string');
294
+ }
295
+
296
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
297
+ if (data.hashword.startsWith('$2')) {
298
+ throw new Error(
299
+ 'Password appears to be already hashed. Pass plain text password only.'
300
+ );
301
+ }
302
+
303
+ data.hashword = await bcrypt.hash(data.hashword, 10);
304
+ }
305
+
267
306
  const user = await this.prisma.user.update({
268
307
  where: { id: intId },
269
308
  data,
@@ -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 };