@friggframework/core 2.0.0--canary.690.3fe2b27.0 → 2.0.0--canary.705.9cfd819.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/CLAUDE.md +40 -28
  3. package/README.md +0 -105
  4. package/application/commands/entity-commands.js +18 -60
  5. package/application/commands/integration-commands.js +73 -20
  6. package/application/commands/scheduler-commands.js +7 -11
  7. package/application/commands/user-commands.js +5 -17
  8. package/application/index.js +2 -6
  9. package/core/Worker.js +7 -31
  10. package/core/create-handler.js +0 -9
  11. package/credential/repositories/credential-repository-documentdb.js +0 -34
  12. package/credential/repositories/credential-repository-interface.js +0 -15
  13. package/credential/repositories/credential-repository-mongo.js +0 -23
  14. package/credential/repositories/credential-repository-postgres.js +0 -28
  15. package/database/config.js +24 -48
  16. package/database/documentdb-encryption-service.js +22 -9
  17. package/database/documentdb-utils.js +0 -2
  18. package/database/encryption/README.md +14 -78
  19. package/database/encryption/documentdb-encryption-service.md +1 -78
  20. package/database/encryption/encryption-schema-registry.js +24 -121
  21. package/database/encryption/field-encryption-service.js +5 -69
  22. package/database/encryption/integration-mapping-encryption.js +1 -1
  23. package/database/prisma.js +38 -15
  24. package/database/use-cases/check-encryption-health-use-case.js +52 -43
  25. package/errors/index.js +4 -0
  26. package/errors/mapping-already-exists-error.js +14 -0
  27. package/generated/prisma-mongodb/edge.js +4 -43
  28. package/generated/prisma-mongodb/index-browser.js +1 -40
  29. package/generated/prisma-mongodb/index.d.ts +111 -4739
  30. package/generated/prisma-mongodb/index.js +4 -43
  31. package/generated/prisma-mongodb/package.json +1 -1
  32. package/generated/prisma-mongodb/schema.prisma +0 -92
  33. package/generated/prisma-mongodb/wasm.js +4 -43
  34. package/generated/prisma-postgresql/edge.js +4 -43
  35. package/generated/prisma-postgresql/index-browser.js +1 -40
  36. package/generated/prisma-postgresql/index.d.ts +33 -4966
  37. package/generated/prisma-postgresql/index.js +4 -43
  38. package/generated/prisma-postgresql/package.json +1 -1
  39. package/generated/prisma-postgresql/schema.prisma +0 -87
  40. package/generated/prisma-postgresql/wasm.js +4 -43
  41. package/handlers/WEBHOOKS.md +29 -0
  42. package/handlers/app-definition-loader.js +19 -128
  43. package/handlers/app-handler-helpers.js +3 -25
  44. package/handlers/backend-utils.js +25 -31
  45. package/handlers/middleware/admin-auth.js +1 -14
  46. package/handlers/routers/auth.js +10 -46
  47. package/handlers/routers/health.js +4 -13
  48. package/handlers/routers/integration-defined-routers.js +92 -144
  49. package/handlers/routers/integration-webhook-routers.js +64 -102
  50. package/handlers/workers/integration-defined-workers.js +2 -5
  51. package/index.js +3 -65
  52. package/infrastructure/scheduler/index.js +6 -19
  53. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  54. package/integrations/index.js +0 -1
  55. package/integrations/integration-base.js +29 -0
  56. package/integrations/integration-router.js +96 -131
  57. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  58. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  59. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  60. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  61. package/integrations/repositories/integration-mapping-repository.js +29 -0
  62. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  63. package/integrations/repositories/integration-repository-interface.js +31 -17
  64. package/integrations/repositories/integration-repository-mongo.js +129 -60
  65. package/integrations/repositories/integration-repository-postgres.js +134 -62
  66. package/integrations/repositories/process-repository-mongo.js +1 -1
  67. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  68. package/integrations/use-cases/add-entities-to-integration.js +92 -0
  69. package/integrations/use-cases/create-integration.js +75 -72
  70. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  71. package/integrations/use-cases/index.js +1 -7
  72. package/integrations/use-cases/remove-entities-from-integration.js +70 -0
  73. package/integrations/utils/assert-entity-ids-input.js +42 -0
  74. package/integrations/utils/map-integration-dto.js +14 -43
  75. package/modules/index.js +0 -14
  76. package/modules/module-factory.js +0 -12
  77. package/modules/module.js +2 -53
  78. package/modules/repositories/module-repository-documentdb.js +6 -51
  79. package/modules/repositories/module-repository-interface.js +2 -19
  80. package/modules/repositories/module-repository-mongo.js +74 -67
  81. package/modules/repositories/module-repository-postgres.js +74 -66
  82. package/modules/repositories/module-repository.js +1 -2
  83. package/modules/requester/requester.js +5 -35
  84. package/modules/use-cases/get-entities-for-user.js +13 -26
  85. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  86. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  87. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  88. package/modules/use-cases/get-module.js +4 -10
  89. package/modules/use-cases/process-authorization-callback.js +19 -53
  90. package/modules/use-cases/refresh-entity-options.js +4 -10
  91. package/modules/use-cases/test-module-auth.js +4 -10
  92. package/modules/utils/map-module-dto.js +5 -3
  93. package/modules/utils/to-credential-id.js +14 -0
  94. package/package.json +5 -6
  95. package/prisma-mongodb/schema.prisma +0 -92
  96. package/prisma-postgresql/schema.prisma +0 -87
  97. package/queues/index.js +0 -25
  98. package/types/index.d.ts +0 -2
  99. package/user/repositories/user-repository-documentdb.js +2 -27
  100. package/user/repositories/user-repository-interface.js +0 -18
  101. package/user/repositories/user-repository-mongo.js +0 -24
  102. package/user/repositories/user-repository-postgres.js +0 -18
  103. package/user/tests/doubles/test-user-repository.js +1 -1
  104. package/user/use-cases/authenticate-user.js +1 -7
  105. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  106. package/user/use-cases/get-user-from-bearer-token.js +7 -2
  107. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  108. package/user/user.js +1 -64
  109. package/credential/use-cases/credential-operations-for-user.js +0 -202
  110. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  111. package/database/encryption/encryption-config.js +0 -219
  112. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  113. package/database/organization-where.js +0 -18
  114. package/database/unique-violation.js +0 -25
  115. package/extensions/bootstrap-runner.js +0 -43
  116. package/extensions/extension-loader.js +0 -187
  117. package/extensions/index.js +0 -41
  118. package/extensions/initialize-app.js +0 -123
  119. package/extensions/route-mounter.js +0 -103
  120. package/extensions/schema-composer.js +0 -188
  121. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  122. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  123. package/integrations/repositories/integration-not-found.js +0 -14
  124. package/integrations/schedule-config.js +0 -80
  125. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  126. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  127. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  128. package/management-api/create-management-api-router.js +0 -49
  129. package/management-api/error-response.js +0 -116
  130. package/management-api/management-api-config.js +0 -151
  131. package/management-api/meta-router.js +0 -76
  132. package/management-api/openapi/build-openapi.js +0 -144
  133. package/management-api/openapi/components.js +0 -415
  134. package/management-api/openapi/v1-operations.js +0 -83
  135. package/management-api/openapi/v2-operations.js +0 -264
  136. package/management-api/route-registry.js +0 -411
  137. package/management-api/test-support/http-client.js +0 -39
  138. package/management-api/use-cases/get-management-api-meta.js +0 -57
  139. package/management-api/v1-deprecation.js +0 -120
  140. package/management-api/v2/create-v2-router.js +0 -85
  141. package/management-api/v2/dependencies.js +0 -281
  142. package/management-api/v2/dto.js +0 -143
  143. package/management-api/v2/handlers/authorization.js +0 -94
  144. package/management-api/v2/handlers/credentials.js +0 -33
  145. package/management-api/v2/handlers/entities.js +0 -77
  146. package/management-api/v2/handlers/integrations.js +0 -128
  147. package/management-api/v2/handlers/proxy.js +0 -34
  148. package/management-api/v2/request.js +0 -85
  149. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  150. package/modules/domain/authorization-session.js +0 -144
  151. package/modules/entity-scope.js +0 -15
  152. package/modules/entity-type-registry.js +0 -311
  153. package/modules/proxy/proxy-bases.js +0 -169
  154. package/modules/proxy/proxy-errors.js +0 -31
  155. package/modules/proxy/proxy-path.js +0 -183
  156. package/modules/proxy/proxy-policy.js +0 -277
  157. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  158. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  159. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  160. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  161. package/modules/repositories/entity-not-found.js +0 -18
  162. package/modules/requester/proxy-request.js +0 -245
  163. package/modules/scope-config.js +0 -192
  164. package/modules/scope-policy.js +0 -112
  165. package/modules/scope-resolution.js +0 -288
  166. package/modules/scope-visibility.js +0 -247
  167. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  168. package/modules/tests/doubles/ownership-matching.js +0 -18
  169. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  170. package/modules/use-cases/create-entity-from-credential.js +0 -113
  171. package/modules/use-cases/entity-operations-for-user.js +0 -162
  172. package/modules/use-cases/entity-types.js +0 -139
  173. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  174. package/modules/use-cases/get-authorization-requirements.js +0 -100
  175. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  176. package/modules/use-cases/manage-access.js +0 -21
  177. package/modules/use-cases/multi-step-authorization.js +0 -329
  178. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  179. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  180. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  181. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  182. package/providers/index.js +0 -23
  183. package/providers/resolve-provider.js +0 -93
  184. package/providers/runtime-detection.js +0 -124
  185. package/queues/providers/index.js +0 -11
  186. package/queues/providers/netlify-background-provider.js +0 -150
  187. package/queues/providers/qstash-queue-provider.js +0 -219
  188. package/queues/providers/sqs-queue-provider.js +0 -70
  189. package/queues/queue-client-interface.js +0 -55
  190. package/queues/queue-message-auth.js +0 -193
  191. package/queues/queue-provider-factory.js +0 -101
  192. package/queues/queue-provider.js +0 -65
  193. package/queues/sqs-queue-client.js +0 -81
  194. package/types/modules/index.d.ts +0 -214
  195. package/types/user/index.d.ts +0 -134
  196. package/user/adopter-jwt-config.js +0 -177
  197. package/user/adopter-jwt-errors.js +0 -59
  198. package/user/adopter-token-verifier.js +0 -122
  199. package/user/app-user-helpers.js +0 -97
  200. package/user/create-user-authentication.js +0 -64
  201. package/user/membership.js +0 -51
  202. package/user/repositories/organization-membership-record.js +0 -19
  203. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  204. package/user/repositories/organization-membership-repository-factory.js +0 -42
  205. package/user/repositories/organization-membership-repository-interface.js +0 -57
  206. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  207. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  208. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  209. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  210. package/user/use-cases/ensure-organization-membership.js +0 -92
  211. package/user/use-cases/find-individual-users-by-organization.js +0 -54
package/user/user.js CHANGED
@@ -1,20 +1,5 @@
1
1
  const bcrypt = require('bcryptjs');
2
2
 
3
- /**
4
- * How the request that produced a `User` proved who it is. Set by
5
- * AuthenticateUser, never inferred from a missing value. Organization roles
6
- * (ADR-056 §3, §3.6) depend on it: a shared-secret caller is the app's own
7
- * backend and is trusted to manage; a Frigg bearer token keeps its pre-roles
8
- * behaviour unless the app opts in (`user.orgAdminGateBearerTokens`); an
9
- * adopter token has the role it carries, else the stored membership role,
10
- * else `MEMBER`.
11
- */
12
- const AUTH_SOURCES = Object.freeze({
13
- SHARED_SECRET: 'shared-secret',
14
- ADOPTER_JWT: 'adopter-jwt',
15
- BEARER_TOKEN: 'bearer-token',
16
- });
17
-
18
3
  /**
19
4
  * Represents a user in the system. The User class is a domain entity,
20
5
  * @class User
@@ -33,14 +18,6 @@ class User {
33
18
  this.individualUser = individualUser;
34
19
  this.organizationUser = organizationUser;
35
20
  this.usePassword = usePassword;
36
- // Organization role from an adopter token claim (ADR-056 §2), per
37
- // request. When present it wins over the stored membership role.
38
- this.orgRole = null;
39
- // The role stored on the person's membership of the active organization
40
- // (OrganizationMembership.orgRole); used when the token carries none.
41
- this.membershipRole = null;
42
- // How this request authenticated (one of AUTH_SOURCES); null until AuthenticateUser says.
43
- this.authSource = null;
44
21
 
45
22
  this.config = {
46
23
  primary,
@@ -49,17 +26,6 @@ class User {
49
26
  };
50
27
  }
51
28
 
52
- /**
53
- * The organization this request acts in (ADR-056 §3.6): the one the token or
54
- * `x-frigg-appOrgId` named, not the person's legacy `organizationId`
55
- * pointer. Null when the request has no organization.
56
- * @returns {string|null}
57
- */
58
- getActiveOrganizationId() {
59
- const id = this.organizationUser?.id;
60
- return id === undefined || id === null || id === '' ? null : String(id);
61
- }
62
-
63
29
  getPrimaryUser() {
64
30
  if (this.config.primary === 'organization') {
65
31
  return this.organizationUser;
@@ -67,11 +33,6 @@ class User {
67
33
  return this.individualUser;
68
34
  }
69
35
 
70
- /**
71
- * The id of the primary user. With `primary: 'organization'` this is the
72
- * active organization (the one the request named), never the organization
73
- * the person was first linked to.
74
- */
75
36
  getId() {
76
37
  return this.getPrimaryUser()?.id;
77
38
  }
@@ -104,30 +65,6 @@ class User {
104
65
  return this.config.individualUserRequired;
105
66
  }
106
67
 
107
- getAuthSource() {
108
- return this.authSource;
109
- }
110
-
111
- setAuthSource(authSource) {
112
- this.authSource = authSource ?? null;
113
- }
114
-
115
- getOrgRole() {
116
- return this.orgRole;
117
- }
118
-
119
- setOrgRole(orgRole) {
120
- this.orgRole = orgRole ?? null;
121
- }
122
-
123
- getMembershipRole() {
124
- return this.membershipRole;
125
- }
126
-
127
- setMembershipRole(membershipRole) {
128
- this.membershipRole = membershipRole ?? null;
129
- }
130
-
131
68
  getIndividualUser() {
132
69
  return this.individualUser;
133
70
  }
@@ -185,4 +122,4 @@ class User {
185
122
  }
186
123
  }
187
124
 
188
- module.exports = { User, AUTH_SOURCES };
125
+ module.exports = { User };
@@ -1,202 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const {
3
- createScopePolicy,
4
- describeRecord,
5
- visibilityClauses,
6
- matchesListFilters,
7
- SCOPES,
8
- } = require('../../modules/entity-scope');
9
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
10
-
11
- /**
12
- * Management API v2 credential operations (ADR-056 §3.4, §3.6). A credential
13
- * has no module name and no organization of its own:
14
- *
15
- * - its `type` and `entityIds` come from the entities that use it that the
16
- * caller may see, so the same person's account connected in two
17
- * organizations (one credential, two entities) lists in each organization
18
- * with that organization's entity only;
19
- * - who it belongs to is its OWNER (`credential.userId`), never the entities:
20
- * a credential an individual entity and an organization entity both point at
21
- * (possible only through legacy data) is described by whoever owns the
22
- * credential, so the answer cannot change with the order the entities load;
23
- * - a credential whose entities are all out of sight (another organization's)
24
- * is a 404, and one with no entities at all (its entity was deleted) is
25
- * visible to its owner only, as it was before scoping.
26
- */
27
-
28
- const credentialIdOf = (entity) => {
29
- const ref = entity.credential ?? entity.credentialId;
30
- if (ref === undefined || ref === null) return null;
31
- if (typeof ref === 'object') {
32
- const id = ref.id ?? ref._id;
33
- return id === undefined || id === null ? null : String(id);
34
- }
35
- return String(ref);
36
- };
37
-
38
- /** Adds `type` and `entityIds` from the user's entities to each credential. */
39
- function withEntityUsage(credentials, entities) {
40
- const usage = new Map();
41
- for (const entity of entities || []) {
42
- const credentialId = credentialIdOf(entity);
43
- if (!credentialId) continue;
44
- const entry = usage.get(credentialId) || { type: null, entityIds: [] };
45
- entry.type = entry.type || entity.moduleName || null;
46
- entry.entityIds.push(String(entity.id));
47
- usage.set(credentialId, entry);
48
- }
49
- return credentials.map((credential) => {
50
- const entry = usage.get(String(credential.id)) || {
51
- type: null,
52
- entityIds: [],
53
- };
54
- return { ...credential, type: entry.type, entityIds: entry.entityIds };
55
- });
56
- }
57
-
58
- /**
59
- * `{scope, owner, organizationId, access}` of a credential for this caller, or
60
- * null when its owner is neither the caller nor the active organization. Built
61
- * by `describeRecord` over the row a connection by this owner has: the
62
- * organization of a visible entity, else the one a new entity would get.
63
- */
64
- function credentialTenancy(credential, visibleEntities, ctx) {
65
- const entityOrg = visibleEntities.find(
66
- (entity) => entity.organizationId !== undefined && entity.organizationId !== null
67
- )?.organizationId;
68
- const organizationId =
69
- entityOrg ?? (ctx.individualAcross === 'all' ? null : ctx.activeOrgId);
70
- return describeRecord({ userId: credential.userId, organizationId }, ctx, 'entity');
71
- }
72
-
73
- /** The entities of `entities` the caller may see, each with its `tenancy`. */
74
- function visibleOf(entities, ctx) {
75
- const visible = [];
76
- for (const entity of entities || []) {
77
- const tenancy = describeRecord(entity, ctx, 'entity');
78
- if (tenancy) visible.push({ ...entity, tenancy });
79
- }
80
- return visible;
81
- }
82
-
83
- /**
84
- * The caller's credentials with their type, the entities of them they may see
85
- * and `tenancy`. Credentials with no entity at all are listed too, for the
86
- * person and the active organization that own them.
87
- */
88
- class ListCredentialsForUser {
89
- constructor({ credentialRepository, moduleRepository, scopePolicy }) {
90
- this.credentialRepository = credentialRepository;
91
- this.moduleRepository = moduleRepository;
92
- this.scopePolicy = scopePolicy || createScopePolicy();
93
- }
94
-
95
- /** @param {{scope?: string, owner?: string}} [filters] */
96
- async execute(user, filters = {}) {
97
- const ctx = this.scopePolicy.accessContext(user);
98
- const owners = [...new Set([ctx.callerId, ctx.activeOrgId].filter(Boolean))];
99
- const [found, perOwner, owned] = await Promise.all([
100
- this.moduleRepository.findEntitiesByOwnership(visibilityClauses(ctx, 'entity')),
101
- Promise.all(owners.map((id) => this.credentialRepository.findCredentialsByUserId(id))),
102
- // Every entity of those owners, in any organization: only to tell a credential
103
- // with no entity at all from one used elsewhere.
104
- Promise.all(owners.map((id) => this.moduleRepository.findEntitiesByUserId(id))),
105
- ]);
106
- const visible = visibleOf(found, ctx);
107
- const usedAnywhere = new Set(owned.flat().map(credentialIdOf).filter(Boolean));
108
- const listed = [];
109
- for (const credential of perOwner.flat().filter(Boolean)) {
110
- const id = String(credential.id);
111
- const referencing = visible.filter((entity) => credentialIdOf(entity) === id);
112
- if (referencing.length === 0 && usedAnywhere.has(id)) continue;
113
- const tenancy = credentialTenancy(credential, referencing, ctx);
114
- if (!tenancy || !matchesListFilters(tenancy, filters)) continue;
115
- listed.push({ ...withEntityUsage([credential], referencing)[0], tenancy });
116
- }
117
- return listed;
118
- }
119
- }
120
-
121
- /** One credential the caller may see: see the header. 404 otherwise. */
122
- class GetCredentialForUserV2 {
123
- constructor({ credentialRepository, moduleRepository, scopePolicy }) {
124
- this.credentialRepository = credentialRepository;
125
- this.moduleRepository = moduleRepository;
126
- this.scopePolicy = scopePolicy || createScopePolicy();
127
- }
128
-
129
- async execute(credentialId, user) {
130
- const notFound = () =>
131
- Boom.notFound(`Credential ${credentialId} not found`, {
132
- code: 'CREDENTIAL_NOT_FOUND',
133
- });
134
- let credential = null;
135
- try {
136
- credential = await this.credentialRepository.findCredentialById(
137
- credentialId
138
- );
139
- } catch (error) {
140
- if (!/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
141
- throw error;
142
- }
143
- }
144
- if (!credential) throw notFound();
145
- const ctx = this.scopePolicy.accessContext(user);
146
-
147
- const referencing =
148
- (await this.moduleRepository.findEntities({ credentialId: credential.id })) || [];
149
- const visible = visibleOf(referencing, ctx);
150
- if (referencing.length > 0 && visible.length === 0) throw notFound();
151
- const tenancy = credentialTenancy(credential, visible, ctx);
152
- if (!tenancy) throw notFound();
153
- return { ...withEntityUsage([credential], visible)[0], tenancy };
154
- }
155
- }
156
-
157
- /**
158
- * Deletes a credential the caller may manage. The entities that used it stay,
159
- * with their credential unset, so they show as needing re-authorization rather
160
- * than disappearing from integrations.
161
- *
162
- * Refuses with 409 `CREDENTIAL_SHARED` while an entity the caller cannot see
163
- * still uses it: one credential row serves the same provider account in every
164
- * organization, and deleting it from one would break the other. The message
165
- * says nothing about the other connections (not even how many): remove the
166
- * entities that use it, and it goes with the last one.
167
- */
168
- class DeleteCredentialForUser {
169
- constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
170
- this.credentialRepository = credentialRepository;
171
- this.moduleRepository = moduleRepository;
172
- this.getCredentialForUser = getCredentialForUser;
173
- }
174
-
175
- async execute(credentialId, user) {
176
- const credential = await this.getCredentialForUser.execute(
177
- credentialId,
178
- user
179
- );
180
- assertCanManage(credential.tenancy.access, `credential ${credential.id}`);
181
- const usedBy =
182
- (await this.moduleRepository.findEntities({ credentialId: credential.id })) || [];
183
- if (usedBy.length > credential.entityIds.length) {
184
- throw Boom.conflict(
185
- `Credential ${credential.id} is still in use by other connections and cannot be deleted from here`,
186
- { code: 'CREDENTIAL_SHARED' }
187
- );
188
- }
189
- for (const entityId of credential.entityIds) {
190
- await this.moduleRepository.unsetCredential(entityId);
191
- }
192
- await this.credentialRepository.deleteCredentialById(credential.id);
193
- return { id: String(credential.id), entityIds: credential.entityIds };
194
- }
195
- }
196
-
197
- module.exports = {
198
- ListCredentialsForUser,
199
- GetCredentialForUserV2,
200
- DeleteCredentialForUser,
201
- withEntityUsage,
202
- };
@@ -1,73 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
3
-
4
- /**
5
- * Re-authorization of a credential the caller owns (Management API v2). It
6
- * runs the same flow as /api/v2/authorize for the credential's entity type,
7
- * with the session bound to the credential so it cannot be reused for
8
- * anything else.
9
- */
10
- async function credentialEntityType(getCredentialForUser, credentialId, user) {
11
- const credential = await getCredentialForUser.execute(credentialId, user);
12
- if (!credential.type) {
13
- throw Boom.conflict(
14
- `Credential ${credentialId} is not used by any entity, so its type is unknown; authorize a new connection instead`,
15
- { code: 'CREDENTIAL_TYPE_UNKNOWN' }
16
- );
17
- }
18
- return {
19
- entityType: credential.type,
20
- credentialId: String(credential.id),
21
- // A re-authorization keeps the scope the connection already has.
22
- fixedScope: credential.tenancy?.scope,
23
- access: credential.tenancy?.access,
24
- };
25
- }
26
-
27
- /** GET /api/v2/credentials/:credentialId/reauthorize */
28
- class GetCredentialReauthorization {
29
- constructor({ getCredentialForUser, getAuthorizationStep }) {
30
- this.getCredentialForUser = getCredentialForUser;
31
- this.getAuthorizationStep = getAuthorizationStep;
32
- }
33
-
34
- async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
35
- const { entityType, credentialId: boundCredentialId } =
36
- await credentialEntityType(this.getCredentialForUser, credentialId, user);
37
- return this.getAuthorizationStep.execute({
38
- user,
39
- entityType,
40
- credentialId: boundCredentialId,
41
- step,
42
- sessionId,
43
- state,
44
- });
45
- }
46
- }
47
-
48
- /** POST /api/v2/credentials/:credentialId/reauthorize */
49
- class ReauthorizeCredentialForUser {
50
- constructor({ getCredentialForUser, submitAuthorizationStep }) {
51
- this.getCredentialForUser = getCredentialForUser;
52
- this.submitAuthorizationStep = submitAuthorizationStep;
53
- }
54
-
55
- async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
56
- const { access, ...binding } = await credentialEntityType(
57
- this.getCredentialForUser,
58
- credentialId,
59
- user
60
- );
61
- // A credential an organization owns: the organization's admins only.
62
- assertCanManage(access, `credential ${credentialId}`);
63
- return this.submitAuthorizationStep.execute({
64
- user,
65
- ...binding,
66
- data,
67
- step,
68
- sessionId,
69
- });
70
- }
71
- }
72
-
73
- module.exports = { GetCredentialReauthorization, ReauthorizeCredentialForUser };
@@ -1,219 +0,0 @@
1
- /**
2
- * Field-level encryption configuration: decides whether sensitive fields are
3
- * encrypted, and with which key, from the process environment.
4
- *
5
- * The rule fails closed:
6
- *
7
- * - **Deployed** (running in AWS Lambda, and not under
8
- * serverless-offline, `serverless invoke local` or Jest): the stage name is
9
- * ignored. A configured key (KMS_KEY_ARN, or AES_KEY_ID with AES_KEY) turns
10
- * encryption on. With no key, the configuration is an error and the Prisma
11
- * client refuses to start, unless FRIGG_ENCRYPTION_DISABLED=true opts out
12
- * explicitly.
13
- * - **Local** (`frigg start`, tests, scripts on a laptop): the stages dev,
14
- * test and local skip encryption. Any other stage encrypts when a key is
15
- * configured and stays plaintext, with a warning, when none is.
16
- *
17
- * Every module that encrypts or reports on encryption reads this one rule.
18
- */
19
-
20
- const { logger } = require('./logger');
21
- const {
22
- isDeployedRuntime: isDeployedProviderRuntime,
23
- } = require('../../providers/runtime-detection');
24
-
25
- const LOCAL_BYPASS_STAGES = ['dev', 'test', 'local'];
26
- const OPT_OUT_VAR = 'FRIGG_ENCRYPTION_DISABLED';
27
-
28
- class EncryptionConfigurationError extends Error {
29
- constructor(message) {
30
- super(message);
31
- this.name = 'EncryptionConfigurationError';
32
- }
33
- }
34
-
35
- function isSet(value) {
36
- return typeof value === 'string' && value.trim() !== '';
37
- }
38
-
39
- function isTrue(value) {
40
- if (typeof value !== 'string') return false;
41
- const normalized = value.trim().toLowerCase();
42
- return normalized === 'true' || normalized === '1';
43
- }
44
-
45
- /**
46
- * True when the process runs as a deployed function, on AWS Lambda or on the
47
- * active provider's platform (provider-netlify: FRIGG_PROVIDER=netlify, set by
48
- * its generated entry points), and not under a local emulator or test runner.
49
- * One rule for encryption and queue authentication: core/providers/
50
- * runtime-detection asks the provider's detect(), and fails closed when a
51
- * named non-AWS provider cannot be loaded.
52
- *
53
- * @param {Object} [env=process.env]
54
- * @returns {boolean}
55
- */
56
- function isDeployedRuntime(env = process.env) {
57
- return isDeployedProviderRuntime(env);
58
- }
59
-
60
- function missingKeyMessage(stage) {
61
- return (
62
- `[Frigg] No field-level encryption key is configured for stage "${stage}". ` +
63
- 'A deployed Frigg app will not write credentials and other sensitive fields in plaintext. ' +
64
- "Fix: set encryption: { fieldLevelEncryptionMethod: 'kms' } in the app definition and redeploy " +
65
- '(Frigg creates or discovers a KMS key and sets KMS_KEY_ARN), ' +
66
- 'or provide AES_KEY_ID and AES_KEY (32 characters) in the Lambda environment. ' +
67
- `To run this stage without encryption on purpose, set ${OPT_OUT_VAR}=true ` +
68
- "(or encryption: { fieldLevelEncryptionMethod: 'none' } in the app definition)."
69
- );
70
- }
71
-
72
- function incompleteAesMessage(stage) {
73
- return (
74
- `[Frigg] AES_KEY_ID is set but AES_KEY is not (stage "${stage}"). ` +
75
- 'Provide AES_KEY (32 characters) alongside AES_KEY_ID, ' +
76
- "or use KMS with encryption: { fieldLevelEncryptionMethod: 'kms' }."
77
- );
78
- }
79
-
80
- /**
81
- * Resolves the encryption configuration without side effects.
82
- *
83
- * @param {Object} [env=process.env]
84
- * @returns {{
85
- * enabled: boolean,
86
- * method: 'kms'|'aes'|undefined,
87
- * mode: 'kms'|'aes'|'none',
88
- * runtime: 'deployed'|'local',
89
- * stage: string,
90
- * bypassed: boolean,
91
- * optedOut: boolean,
92
- * optOutIgnored: boolean,
93
- * hasKMS: boolean,
94
- * hasAES: boolean,
95
- * error: string|null,
96
- * }} `error` is set when a deployed runtime has no usable key.
97
- */
98
- function resolveEncryptionConfig(env = process.env) {
99
- const stage = env.STAGE || env.NODE_ENV || 'development';
100
- const runtime = isDeployedRuntime(env) ? 'deployed' : 'local';
101
- const hasKMS = isSet(env.KMS_KEY_ARN);
102
- const hasAES = isSet(env.AES_KEY_ID);
103
- const optOutRequested = isTrue(env[OPT_OUT_VAR]);
104
-
105
- const base = {
106
- enabled: false,
107
- method: undefined,
108
- mode: 'none',
109
- runtime,
110
- stage,
111
- bypassed: false,
112
- optedOut: false,
113
- optOutIgnored: false,
114
- hasKMS,
115
- hasAES,
116
- error: null,
117
- };
118
-
119
- if (runtime === 'local') {
120
- if (LOCAL_BYPASS_STAGES.includes(String(stage).toLowerCase())) {
121
- return { ...base, bypassed: true };
122
- }
123
- if (hasKMS || hasAES) {
124
- const method = hasKMS ? 'kms' : 'aes';
125
- return { ...base, enabled: true, method, mode: method };
126
- }
127
- return base;
128
- }
129
-
130
- const hasCompleteAES = hasAES && isSet(env.AES_KEY);
131
- if (hasKMS || hasCompleteAES) {
132
- const method = hasKMS ? 'kms' : 'aes';
133
- return {
134
- ...base,
135
- enabled: true,
136
- method,
137
- mode: method,
138
- optOutIgnored: optOutRequested,
139
- };
140
- }
141
-
142
- if (optOutRequested) {
143
- return { ...base, optedOut: true };
144
- }
145
-
146
- return {
147
- ...base,
148
- error: hasAES ? incompleteAesMessage(stage) : missingKeyMessage(stage),
149
- };
150
- }
151
-
152
- const warned = new Set();
153
-
154
- function warnOnce(key, message) {
155
- if (warned.has(key)) return;
156
- warned.add(key);
157
- logger.warn(message);
158
- }
159
-
160
- /**
161
- * Resolves the encryption configuration for use: throws when a deployed
162
- * runtime has no usable key, and warns once per process (once per Lambda
163
- * cold start) when encryption is explicitly disabled.
164
- *
165
- * @param {Object} [env=process.env]
166
- * @returns {ReturnType<typeof resolveEncryptionConfig>}
167
- * @throws {EncryptionConfigurationError}
168
- */
169
- function getEncryptionConfig(env = process.env) {
170
- const config = resolveEncryptionConfig(env);
171
-
172
- if (config.error) {
173
- throw new EncryptionConfigurationError(config.error);
174
- }
175
-
176
- if (config.optedOut) {
177
- warnOnce(
178
- 'opted-out',
179
- `[Frigg] ${OPT_OUT_VAR}=true: field-level encryption is OFF for stage "${config.stage}". ` +
180
- 'Credentials and other sensitive fields are stored in PLAINTEXT. ' +
181
- `Remove ${OPT_OUT_VAR} and configure a key (fieldLevelEncryptionMethod: 'kms') to encrypt them.`
182
- );
183
- }
184
-
185
- if (config.optOutIgnored) {
186
- warnOnce(
187
- 'opt-out-ignored',
188
- `[Frigg] ${OPT_OUT_VAR}=true is ignored because an encryption key is configured; ` +
189
- `fields are encrypted with ${config.method.toUpperCase()}. ` +
190
- 'Turning encryption off while a key is present would leave encrypted data unreadable.'
191
- );
192
- }
193
-
194
- if (config.runtime === 'local' && !config.enabled && !config.bypassed) {
195
- warnOnce(
196
- 'local-no-keys',
197
- `No encryption keys configured (KMS_KEY_ARN or AES_KEY_ID) for local stage "${config.stage}". ` +
198
- 'Field-level encryption disabled for this local run. ' +
199
- 'A deployed stage without a key refuses to start.'
200
- );
201
- }
202
-
203
- return config;
204
- }
205
-
206
- /** Test helper: let the once-per-process warnings fire again. */
207
- function resetEncryptionConfigWarnings() {
208
- warned.clear();
209
- }
210
-
211
- module.exports = {
212
- EncryptionConfigurationError,
213
- LOCAL_BYPASS_STAGES,
214
- OPT_OUT_VAR,
215
- getEncryptionConfig,
216
- isDeployedRuntime,
217
- resetEncryptionConfigWarnings,
218
- resolveEncryptionConfig,
219
- };
@@ -1,15 +0,0 @@
1
- /**
2
- * Test double for Cryptor output. Same shape as the real thing
3
- * (`keyId:iv:ciphertext:encryptedKey`: base64, 16-byte hex, hex, base64) so
4
- * FieldEncryptionService._isEncrypted accepts it, with the plaintext
5
- * recoverable for the fake decrypt.
6
- */
7
- const fakeCiphertext = (plain) =>
8
- `a2V5SWQ=:${'0'.repeat(32)}:${Buffer.from(String(plain)).toString(
9
- 'hex'
10
- )}:ZW5jS2V5`;
11
-
12
- const fakePlaintext = (ciphertext) =>
13
- Buffer.from(String(ciphertext).split(':')[2], 'hex').toString();
14
-
15
- module.exports = { fakeCiphertext, fakePlaintext };
@@ -1,18 +0,0 @@
1
- /**
2
- * The Prisma `where` fragment for an `organizationId` on MongoDB (ADR-056
3
- * §3.6), shared by the entity and integration adapters.
4
- *
5
- * Prisma on MongoDB: `null` matches only an explicit null and `isSet: false`
6
- * only a missing field. A row written before the column existed is the common
7
- * "no organization" case, so `null` here means both.
8
- */
9
- function organizationWhere(organizationId) {
10
- if (organizationId === null) {
11
- return {
12
- OR: [{ organizationId: null }, { organizationId: { isSet: false } }],
13
- };
14
- }
15
- return { organizationId };
16
- }
17
-
18
- module.exports = { organizationWhere };
@@ -1,25 +0,0 @@
1
- /** Mongo/DocumentDB duplicate-key error code. */
2
- const DUPLICATE_KEY_CODE = 11000;
3
- /** Prisma's unique-constraint error code (PostgreSQL and MongoDB). */
4
- const PRISMA_UNIQUE_CODE = 'P2002';
5
-
6
- /**
7
- * Whether `error` is a unique-constraint violation, however the database
8
- * layer raised it: a Prisma `P2002`, or a duplicate-key `11000`
9
- * (`documentdb-utils` `insertOne` sets `code` on the error it throws).
10
- * Matches on the code only, never on message text.
11
- *
12
- * @param {unknown} error
13
- * @returns {boolean}
14
- */
15
- function isUniqueViolation(error) {
16
- return (
17
- error?.code === PRISMA_UNIQUE_CODE || error?.code === DUPLICATE_KEY_CODE
18
- );
19
- }
20
-
21
- module.exports = {
22
- isUniqueViolation,
23
- DUPLICATE_KEY_CODE,
24
- PRISMA_UNIQUE_CODE,
25
- };