@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.
Files changed (163) 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/database-migration-handler.js +227 -0
  82. package/handlers/routers/auth.js +1 -1
  83. package/handlers/routers/db-migration.handler.js +29 -0
  84. package/handlers/routers/db-migration.js +256 -0
  85. package/handlers/routers/health.js +41 -6
  86. package/handlers/routers/integration-webhook-routers.js +2 -2
  87. package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
  88. package/handlers/workers/db-migration.js +352 -0
  89. package/index.js +12 -0
  90. package/integrations/integration-router.js +60 -70
  91. package/integrations/repositories/integration-repository-interface.js +12 -0
  92. package/integrations/repositories/integration-repository-mongo.js +32 -0
  93. package/integrations/repositories/integration-repository-postgres.js +33 -0
  94. package/integrations/repositories/process-repository-postgres.js +2 -2
  95. package/integrations/tests/doubles/test-integration-repository.js +2 -2
  96. package/logs/logger.js +0 -4
  97. package/modules/entity.js +0 -1
  98. package/modules/repositories/module-repository-mongo.js +3 -12
  99. package/modules/repositories/module-repository-postgres.js +0 -11
  100. package/modules/repositories/module-repository.js +1 -12
  101. package/modules/use-cases/get-entity-options-by-id.js +1 -1
  102. package/modules/use-cases/get-module.js +1 -2
  103. package/modules/use-cases/refresh-entity-options.js +1 -1
  104. package/modules/use-cases/test-module-auth.js +1 -1
  105. package/package.json +82 -66
  106. package/prisma-mongodb/schema.prisma +21 -21
  107. package/prisma-postgresql/schema.prisma +15 -15
  108. package/queues/queuer-util.js +24 -21
  109. package/types/core/index.d.ts +2 -2
  110. package/types/module-plugin/index.d.ts +0 -2
  111. package/user/use-cases/authenticate-user.js +127 -0
  112. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  113. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  114. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  115. package/user/user.js +16 -0
  116. package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
  117. package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
  118. package/websocket/repositories/websocket-connection-repository.js +11 -10
  119. package/application/commands/integration-commands.test.js +0 -123
  120. package/database/encryption/encryption-integration.test.js +0 -553
  121. package/database/encryption/encryption-schema-registry.test.js +0 -392
  122. package/database/encryption/field-encryption-service.test.js +0 -525
  123. package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
  124. package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
  125. package/database/encryption/postgres-relation-decryption.test.js +0 -245
  126. package/database/encryption/prisma-encryption-extension.test.js +0 -439
  127. package/errors/base-error.test.js +0 -32
  128. package/errors/fetch-error.test.js +0 -79
  129. package/errors/halt-error.test.js +0 -11
  130. package/errors/validation-errors.test.js +0 -120
  131. package/handlers/auth-flow.integration.test.js +0 -147
  132. package/handlers/integration-event-dispatcher.test.js +0 -209
  133. package/handlers/routers/health.test.js +0 -210
  134. package/handlers/routers/integration-webhook-routers.test.js +0 -126
  135. package/handlers/webhook-flow.integration.test.js +0 -356
  136. package/handlers/workers/integration-defined-workers.test.js +0 -184
  137. package/integrations/tests/use-cases/create-integration.test.js +0 -131
  138. package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
  139. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
  140. package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
  141. package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
  142. package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
  143. package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
  144. package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
  145. package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
  146. package/integrations/tests/use-cases/update-integration.test.js +0 -141
  147. package/integrations/use-cases/create-process.test.js +0 -178
  148. package/integrations/use-cases/get-process.test.js +0 -190
  149. package/integrations/use-cases/load-integration-context-full.test.js +0 -329
  150. package/integrations/use-cases/load-integration-context.test.js +0 -114
  151. package/integrations/use-cases/update-process-metrics.test.js +0 -308
  152. package/integrations/use-cases/update-process-state.test.js +0 -256
  153. package/lambda/TimeoutCatcher.test.js +0 -68
  154. package/logs/logger.test.js +0 -76
  155. package/modules/module-hydration.test.js +0 -205
  156. package/modules/requester/requester.test.js +0 -28
  157. package/user/tests/use-cases/create-individual-user.test.js +0 -24
  158. package/user/tests/use-cases/create-organization-user.test.js +0 -28
  159. package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
  160. package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
  161. package/user/tests/use-cases/login-user.test.js +0 -220
  162. package/user/tests/user-password-encryption-isolation.test.js +0 -237
  163. 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 = "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,27 +1,32 @@
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 = {
17
23
  send: async (message, queueUrl) => {
18
24
  console.log(`Enqueuing message to SQS queue ${queueUrl}`);
19
- return sqs
20
- .sendMessage({
21
- MessageBody: JSON.stringify(message),
22
- QueueUrl: queueUrl,
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
- await sqs
43
- .sendMessageBatch({
44
- Entries: buffer,
45
- QueueUrl: queueUrl,
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
- return sqs
58
- .sendMessageBatch({
59
- Entries: buffer,
60
- QueueUrl: queueUrl,
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
@@ -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;
@@ -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
+