@friggframework/core 2.0.0--canary.704.79d9a63.0 → 2.0.0--canary.680.daae24e.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +0 -40
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +18 -71
- package/application/index.js +6 -2
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +117 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +66 -0
- package/database/documentdb-encryption-service.js +9 -22
- package/database/documentdb-utils.js +1 -4
- package/database/encryption/README.md +78 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +240 -0
- package/database/encryption/encryption-schema-registry.js +41 -12
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/database/prisma.js +11 -36
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- package/generated/prisma-mongodb/edge.js +20 -4
- package/generated/prisma-mongodb/index-browser.js +17 -1
- package/generated/prisma-mongodb/index.d.ts +1543 -95
- package/generated/prisma-mongodb/index.js +20 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +27 -0
- package/generated/prisma-mongodb/wasm.js +20 -4
- package/generated/prisma-postgresql/edge.js +20 -4
- package/generated/prisma-postgresql/index-browser.js +17 -1
- package/generated/prisma-postgresql/index.d.ts +1570 -89
- package/generated/prisma-postgresql/index.js +20 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +26 -0
- package/generated/prisma-postgresql/wasm.js +20 -4
- package/handlers/WEBHOOKS.md +0 -29
- package/handlers/app-definition-loader.js +19 -8
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +11 -3
- package/handlers/routers/health.js +11 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +4 -4
- package/integrations/index.js +0 -4
- package/integrations/integration-base.js +0 -29
- package/integrations/integration-router.js +16 -23
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-repository-documentdb.js +0 -32
- package/integrations/repositories/integration-repository-interface.js +0 -30
- package/integrations/repositories/integration-repository-mongo.js +1 -53
- package/integrations/repositories/integration-repository-postgres.js +0 -58
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/use-cases/create-integration.js +19 -57
- package/integrations/use-cases/integration-operations-for-user.js +225 -0
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +82 -0
- package/management-api/meta-router.js +57 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +368 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +252 -0
- package/management-api/route-registry.js +294 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +256 -0
- package/management-api/v2/dto.js +123 -0
- package/management-api/v2/handlers/authorization.js +93 -0
- package/management-api/v2/handlers/credentials.js +29 -0
- package/management-api/v2/handlers/entities.js +74 -0
- package/management-api/v2/handlers/integrations.js +125 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +54 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +6 -0
- package/modules/proxy/proxy-errors.js +20 -0
- package/modules/proxy/proxy-path.js +179 -0
- package/modules/proxy/proxy-policy.js +248 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- package/modules/requester/proxy-request.js +244 -0
- package/modules/requester/requester.js +27 -2
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
- package/modules/use-cases/entity-operations-for-user.js +125 -0
- package/modules/use-cases/entity-types.js +115 -0
- package/modules/use-cases/execute-entity-proxy-request.js +239 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entity-module-for-user.js +86 -0
- package/modules/use-cases/multi-step-authorization.js +302 -0
- package/modules/utils/map-module-dto.js +3 -5
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +27 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +26 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +58 -0
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/get-user-from-bearer-token.js +2 -7
- package/errors/mapping-already-exists-error.js +0 -14
- package/integrations/use-cases/add-entities-to-integration.js +0 -92
- package/integrations/use-cases/remove-entities-from-integration.js +0 -70
- package/integrations/utils/assert-entity-ids-input.js +0 -42
- package/modules/utils/to-credential-id.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# Unreleased
|
|
2
|
+
|
|
3
|
+
#### Behaviour change: module credential fields are now encrypted
|
|
4
|
+
|
|
5
|
+
- `credentialFields` declared in an API module's `encryption` config (for example `Definition.encryption.credentialFields`) were never registered for encryption: `loadModuleEncryptionSchemas` required a path that does not exist and the error was swallowed. They are now registered, for modules bound by integrations and for modules registered in `appDefinition.entities` (ADR-059).
|
|
6
|
+
- Existing rows are not rewritten. A credential saved before this release stays plaintext until it is next written, and is read as stored.
|
|
7
|
+
- Reads now tolerate plaintext: a value is only treated as ciphertext when it has the exact `keyId:iv:ciphertext:encryptedKey` shape (previously any string with three or more colons, such as `https://user:pw@host:443`), and a value with that shape that fails to decrypt throws `FieldDecryptionError` in a deployed runtime (any stage, AWS Lambda or Netlify) and is returned as stored with a warning on a local run.
|
|
8
|
+
- An app's own `encryption.schema` is no longer replaced when module fields are added to it.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
1
12
|
# v1.2.2 (Fri Aug 09 2024)
|
|
2
13
|
|
|
3
14
|
#### 🐛 Bug Fix
|
package/CLAUDE.md
CHANGED
|
@@ -206,51 +206,11 @@ packages/core/
|
|
|
206
206
|
- `delete-integration-for-user.js` - Remove integration
|
|
207
207
|
- `get-integration-instance.js` - Load integration with modules
|
|
208
208
|
- `load-integration-context.js` - Full integration context loading
|
|
209
|
-
- `add-entities-to-integration.js` / `remove-entities-from-integration.js` - Attach or detach entities on an existing integration
|
|
210
209
|
|
|
211
210
|
**Repositories**:
|
|
212
211
|
- `integration-repository-factory.js` - Creates database-specific repositories
|
|
213
212
|
- `integration-repository-mongo.js` - MongoDB implementation
|
|
214
213
|
- `integration-repository-postgres.js` - PostgreSQL implementation
|
|
215
|
-
- `addEntitiesToIntegration(integrationId, entityIds)` /
|
|
216
|
-
`removeEntitiesFromIntegration(integrationId, entityIds)` change the entity
|
|
217
|
-
set of an existing integration (all three adapters; `null` for an unknown
|
|
218
|
-
integration). Reach them through the `addEntitiesToIntegration` /
|
|
219
|
-
`removeEntitiesFromIntegration` commands. The add use case
|
|
220
|
-
(`add-entities-to-integration.js`) rejects unknown entities
|
|
221
|
-
(`ENTITIES_NOT_FOUND`, 404), entities of another user
|
|
222
|
-
(`ENTITY_USER_MISMATCH`, 403) and entities of a module the integration does
|
|
223
|
-
not already hold (`ENTITY_MODULE_MISMATCH`, 400). The remove use case
|
|
224
|
-
(`remove-entities-from-integration.js`) refuses to leave a module with no
|
|
225
|
-
entity (`LAST_ENTITY_OF_MODULE`, 409). Re-adding an attached entity or
|
|
226
|
-
removing a detached one changes nothing; a detached entity is not deleted.
|
|
227
|
-
`integration-repository-entities-parity.test.js` runs every adapter against
|
|
228
|
-
real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
|
|
229
|
-
`INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
|
|
230
|
-
writes only `Integration.entityIds` (as its `createIntegration` does), not
|
|
231
|
-
`Entity.integrationIds`.
|
|
232
|
-
**One credential, many accounts**: when one credential covers several
|
|
233
|
-
provider accounts, give each account its own entity (externalId = account
|
|
234
|
-
id) and attach them all to one integration; webhooks then route with
|
|
235
|
-
`findIntegrationByEntityExternalId`. Traps:
|
|
236
|
-
(1) `IntegrationBase` keys modules by module name, so `this.<moduleKey>`
|
|
237
|
-
holds only one entity's module, and on the queue/webhook path
|
|
238
|
-
(`get-integration-instance.js`) `this.entities` holds entity **ids**, not
|
|
239
|
-
records. For per-account work (a webhook for one account, teardown of every
|
|
240
|
-
account in `onDelete`), load
|
|
241
|
-
`this.commands.loadIntegrationContextById(this.id)` and use
|
|
242
|
-
`context.modules`: one module per entity, each with its record on
|
|
243
|
-
`module.entity` (pick by `module.entity.externalId`).
|
|
244
|
-
(2) `findIntegrationByEntityExternalId` counts every entity with that
|
|
245
|
-
externalId, attached or not, across users: reuse an existing entity
|
|
246
|
-
(`findEntity`) when an account comes back, or delete it when it is revoked,
|
|
247
|
-
never create a second one.
|
|
248
|
-
(3) `createIntegration` reuses an existing integration of the same user
|
|
249
|
-
and type whose entities include all requested ones (an exact match wins),
|
|
250
|
-
so a reconnect that sends the original entities finds the integration that
|
|
251
|
-
now holds more.
|
|
252
|
-
(4) Every module's delegate is the integration: an auth failure on any
|
|
253
|
-
account marks the shared credential invalid and the integration ERROR.
|
|
254
214
|
- `integration-mapping-repository-*.js` - Mapping data persistence.
|
|
255
215
|
`queryMappings(integrationId, { where, orderBy, skip, take, omit })`
|
|
256
216
|
returns `{ mappings, total }`: one filtered, ordered page of an
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createModuleRepository,
|
|
3
3
|
} = require('../../modules/repositories/module-repository-factory');
|
|
4
|
-
const { toCredentialId } = require('../../modules/utils/to-credential-id');
|
|
5
4
|
|
|
6
5
|
const ERROR_CODE_MAP = {
|
|
7
6
|
ENTITY_NOT_FOUND: 404,
|
|
8
7
|
INVALID_ENTITY_DATA: 400,
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
function toEntityDto(entity) {
|
|
12
|
-
return {
|
|
13
|
-
id: entity.id,
|
|
14
|
-
userId: entity.userId,
|
|
15
|
-
externalId: entity.externalId,
|
|
16
|
-
name: entity.name,
|
|
17
|
-
moduleName: entity.moduleName,
|
|
18
|
-
credentialId: toCredentialId(entity.credential),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
function mapErrorToResponse(error) {
|
|
23
11
|
const status = ERROR_CODE_MAP[error?.code] || 500;
|
|
24
12
|
return {
|
|
@@ -78,7 +66,16 @@ function createEntityCommands() {
|
|
|
78
66
|
|
|
79
67
|
const entity = await moduleRepo.createEntity(entityData);
|
|
80
68
|
|
|
81
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
id: entity.id,
|
|
71
|
+
userId: entity.userId,
|
|
72
|
+
externalId: entity.externalId,
|
|
73
|
+
name: entity.name,
|
|
74
|
+
moduleName: entity.moduleName,
|
|
75
|
+
credentialId: entity.credential?._id
|
|
76
|
+
? entity.credential._id.toString()
|
|
77
|
+
: entity.credential,
|
|
78
|
+
};
|
|
82
79
|
} catch (error) {
|
|
83
80
|
return mapErrorToResponse(error);
|
|
84
81
|
}
|
|
@@ -112,7 +109,16 @@ function createEntityCommands() {
|
|
|
112
109
|
return null;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
return
|
|
112
|
+
return {
|
|
113
|
+
id: entity.id,
|
|
114
|
+
userId: entity.userId,
|
|
115
|
+
externalId: entity.externalId,
|
|
116
|
+
name: entity.name,
|
|
117
|
+
moduleName: entity.moduleName,
|
|
118
|
+
credentialId: entity.credential?._id
|
|
119
|
+
? entity.credential._id.toString()
|
|
120
|
+
: entity.credential,
|
|
121
|
+
};
|
|
116
122
|
} catch (error) {
|
|
117
123
|
return mapErrorToResponse(error);
|
|
118
124
|
}
|
|
@@ -133,7 +139,16 @@ function createEntityCommands() {
|
|
|
133
139
|
|
|
134
140
|
const entities = await moduleRepo.findEntitiesByUserId(userId);
|
|
135
141
|
|
|
136
|
-
return entities.map((entity) =>
|
|
142
|
+
return entities.map((entity) => ({
|
|
143
|
+
id: entity.id,
|
|
144
|
+
userId: entity.userId,
|
|
145
|
+
externalId: entity.externalId,
|
|
146
|
+
name: entity.name,
|
|
147
|
+
moduleName: entity.moduleName,
|
|
148
|
+
credentialId: entity.credential?._id
|
|
149
|
+
? entity.credential._id.toString()
|
|
150
|
+
: entity.credential,
|
|
151
|
+
}));
|
|
137
152
|
} catch (error) {
|
|
138
153
|
if (error.code) {
|
|
139
154
|
return mapErrorToResponse(error);
|
|
@@ -165,7 +180,16 @@ function createEntityCommands() {
|
|
|
165
180
|
moduleName
|
|
166
181
|
);
|
|
167
182
|
|
|
168
|
-
return entities.map((entity) =>
|
|
183
|
+
return entities.map((entity) => ({
|
|
184
|
+
id: entity.id,
|
|
185
|
+
userId: entity.userId,
|
|
186
|
+
externalId: entity.externalId,
|
|
187
|
+
name: entity.name,
|
|
188
|
+
moduleName: entity.moduleName,
|
|
189
|
+
credentialId: entity.credential?._id
|
|
190
|
+
? entity.credential._id.toString()
|
|
191
|
+
: entity.credential,
|
|
192
|
+
}));
|
|
169
193
|
} catch (error) {
|
|
170
194
|
if (error.code) {
|
|
171
195
|
return mapErrorToResponse(error);
|
|
@@ -189,7 +213,16 @@ function createEntityCommands() {
|
|
|
189
213
|
|
|
190
214
|
const entity = await moduleRepo.findEntityById(entityId);
|
|
191
215
|
|
|
192
|
-
return
|
|
216
|
+
return {
|
|
217
|
+
id: entity.id,
|
|
218
|
+
userId: entity.userId,
|
|
219
|
+
externalId: entity.externalId,
|
|
220
|
+
name: entity.name,
|
|
221
|
+
moduleName: entity.moduleName,
|
|
222
|
+
credentialId: entity.credential?._id
|
|
223
|
+
? entity.credential._id.toString()
|
|
224
|
+
: entity.credential,
|
|
225
|
+
};
|
|
193
226
|
} catch (error) {
|
|
194
227
|
return mapErrorToResponse(error);
|
|
195
228
|
}
|
|
@@ -217,7 +250,16 @@ function createEntityCommands() {
|
|
|
217
250
|
throw error;
|
|
218
251
|
}
|
|
219
252
|
|
|
220
|
-
return
|
|
253
|
+
return {
|
|
254
|
+
id: entity.id,
|
|
255
|
+
userId: entity.userId,
|
|
256
|
+
externalId: entity.externalId,
|
|
257
|
+
name: entity.name,
|
|
258
|
+
moduleName: entity.moduleName,
|
|
259
|
+
credentialId: entity.credential?._id
|
|
260
|
+
? entity.credential._id.toString()
|
|
261
|
+
: entity.credential,
|
|
262
|
+
};
|
|
221
263
|
} catch (error) {
|
|
222
264
|
return mapErrorToResponse(error);
|
|
223
265
|
}
|
|
@@ -30,14 +30,9 @@ const {
|
|
|
30
30
|
PatchIntegrationConfig,
|
|
31
31
|
} = require('../../integrations/use-cases/patch-integration-config');
|
|
32
32
|
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
RemoveEntitiesFromIntegration,
|
|
37
|
-
} = require('../../integrations/use-cases/remove-entities-from-integration');
|
|
38
|
-
const {
|
|
39
|
-
getModulesDefinitionFromIntegrationClasses,
|
|
40
|
-
} = require('../../integrations/utils/map-integration-dto');
|
|
33
|
+
getEntityTypeRegistry,
|
|
34
|
+
getEntityTypeDefinitions,
|
|
35
|
+
} = require('../../modules/entity-type-registry');
|
|
41
36
|
|
|
42
37
|
const ERROR_CODE_MAP = {
|
|
43
38
|
ENTITY_NOT_FOUND: 401,
|
|
@@ -46,11 +41,6 @@ const ERROR_CODE_MAP = {
|
|
|
46
41
|
EXTERNAL_ID_REQUIRED: 400,
|
|
47
42
|
TYPE_REQUIRED: 400,
|
|
48
43
|
INTEGRATION_RECORD_NOT_FOUND: 404,
|
|
49
|
-
INVALID_INTEGRATION_DATA: 400,
|
|
50
|
-
ENTITIES_NOT_FOUND: 404,
|
|
51
|
-
ENTITY_USER_MISMATCH: 403,
|
|
52
|
-
ENTITY_MODULE_MISMATCH: 400,
|
|
53
|
-
LAST_ENTITY_OF_MODULE: 409,
|
|
54
44
|
};
|
|
55
45
|
|
|
56
46
|
function mapErrorToResponse(error) {
|
|
@@ -62,7 +52,16 @@ function mapErrorToResponse(error) {
|
|
|
62
52
|
};
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @param {Object} [params]
|
|
57
|
+
* @param {Function} [params.integrationClass]
|
|
58
|
+
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module
|
|
59
|
+
* definitions (`loadAppDefinition().moduleDefinitions`). Integration code
|
|
60
|
+
* calls this from a constructor with no app definition in hand, so when it
|
|
61
|
+
* is omitted the commands hydrate with this integration's own modules only
|
|
62
|
+
* (a registry built from `{ integrations: [integrationClass] }`).
|
|
63
|
+
*/
|
|
64
|
+
function createIntegrationCommands({ integrationClass, moduleDefinitions } = {}) {
|
|
66
65
|
// Always use Frigg's default repositories and use cases
|
|
67
66
|
const integrationRepository = createIntegrationRepository();
|
|
68
67
|
|
|
@@ -119,13 +118,13 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
119
118
|
|
|
120
119
|
const moduleRepository = createModuleRepository();
|
|
121
120
|
|
|
122
|
-
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
|
|
123
|
-
integrationClass,
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
121
|
const moduleFactory = new ModuleFactory({
|
|
127
122
|
moduleRepository,
|
|
128
|
-
moduleDefinitions
|
|
123
|
+
moduleDefinitions:
|
|
124
|
+
moduleDefinitions ??
|
|
125
|
+
getEntityTypeDefinitions(
|
|
126
|
+
getEntityTypeRegistry({ integrations: [integrationClass] })
|
|
127
|
+
),
|
|
129
128
|
});
|
|
130
129
|
|
|
131
130
|
const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
|
|
@@ -174,17 +173,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
174
173
|
integrationRepository,
|
|
175
174
|
});
|
|
176
175
|
|
|
177
|
-
const addEntitiesToIntegrationUseCase = new AddEntitiesToIntegration({
|
|
178
|
-
integrationRepository,
|
|
179
|
-
moduleRepository,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const removeEntitiesFromIntegrationUseCase =
|
|
183
|
-
new RemoveEntitiesFromIntegration({
|
|
184
|
-
integrationRepository,
|
|
185
|
-
moduleRepository,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
176
|
return {
|
|
189
177
|
findIntegrationById,
|
|
190
178
|
listIntegrations,
|
|
@@ -329,47 +317,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
329
317
|
}
|
|
330
318
|
},
|
|
331
319
|
|
|
332
|
-
/**
|
|
333
|
-
* Attach entities to an existing integration. Every entity must
|
|
334
|
-
* exist, belong to the integration's user, and be of a module the
|
|
335
|
-
* integration already holds; entities that are already attached are
|
|
336
|
-
* left as they are.
|
|
337
|
-
* @param {Object} params
|
|
338
|
-
* @param {string} params.integrationId - Integration ID
|
|
339
|
-
* @param {Array<string>} params.entityIds - Entity IDs to attach
|
|
340
|
-
* @returns {Promise<Object>} Updated integration
|
|
341
|
-
*/
|
|
342
|
-
async addEntitiesToIntegration({ integrationId, entityIds }) {
|
|
343
|
-
try {
|
|
344
|
-
return await addEntitiesToIntegrationUseCase.execute(
|
|
345
|
-
integrationId,
|
|
346
|
-
entityIds
|
|
347
|
-
);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
return mapErrorToResponse(error);
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Detach entities from an integration. Entities that are not
|
|
355
|
-
* attached are ignored; detached entities are not deleted. Each
|
|
356
|
-
* module must keep at least one entity.
|
|
357
|
-
* @param {Object} params
|
|
358
|
-
* @param {string} params.integrationId - Integration ID
|
|
359
|
-
* @param {Array<string>} params.entityIds - Entity IDs to detach
|
|
360
|
-
* @returns {Promise<Object>} Updated integration
|
|
361
|
-
*/
|
|
362
|
-
async removeEntitiesFromIntegration({ integrationId, entityIds }) {
|
|
363
|
-
try {
|
|
364
|
-
return await removeEntitiesFromIntegrationUseCase.execute(
|
|
365
|
-
integrationId,
|
|
366
|
-
entityIds
|
|
367
|
-
);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
return mapErrorToResponse(error);
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
|
|
373
320
|
/**
|
|
374
321
|
* Delete an integration by ID
|
|
375
322
|
* @param {string} integrationId - Integration ID to delete
|
package/application/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
17
17
|
*
|
|
18
18
|
* @param {Object} params
|
|
19
19
|
* @param {Object} params.integrationClass - Integration class (required)
|
|
20
|
+
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module definitions; defaults to the integration's own modules
|
|
20
21
|
* @returns {Object} Unified commands object with all CRUD operations
|
|
21
22
|
*
|
|
22
23
|
* @example
|
|
@@ -24,9 +25,12 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
24
25
|
* const user = await commands.createUser({ username: 'user@example.com' });
|
|
25
26
|
* const credential = await commands.createCredential({ userId: user.id, ... });
|
|
26
27
|
*/
|
|
27
|
-
function createFriggCommands({ integrationClass }) {
|
|
28
|
+
function createFriggCommands({ integrationClass, moduleDefinitions }) {
|
|
28
29
|
// All commands use Frigg's default repositories and use cases
|
|
29
|
-
const integrationCommands = createIntegrationCommands({
|
|
30
|
+
const integrationCommands = createIntegrationCommands({
|
|
31
|
+
integrationClass,
|
|
32
|
+
moduleDefinitions,
|
|
33
|
+
});
|
|
30
34
|
|
|
31
35
|
const userCommands = createUserCommands();
|
|
32
36
|
|
|
@@ -186,6 +186,40 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
186
186
|
return this._mapCredential(decryptedCredential);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* All credentials owned by a user, newest first.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} userId - User ID
|
|
193
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
194
|
+
*/
|
|
195
|
+
async findCredentialsByUserId(userId) {
|
|
196
|
+
const userObjectId = toObjectId(userId);
|
|
197
|
+
if (!userObjectId) return [];
|
|
198
|
+
const docs = await findManyDrained(
|
|
199
|
+
this.prisma,
|
|
200
|
+
'Credential',
|
|
201
|
+
{ userId: userObjectId },
|
|
202
|
+
{ sort: { createdAt: -1 } }
|
|
203
|
+
);
|
|
204
|
+
const credentials = [];
|
|
205
|
+
for (const doc of docs) {
|
|
206
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
207
|
+
'Credential',
|
|
208
|
+
doc
|
|
209
|
+
);
|
|
210
|
+
credentials.push({
|
|
211
|
+
...this._mapCredential(decrypted),
|
|
212
|
+
id: fromObjectId(doc._id),
|
|
213
|
+
userId: fromObjectId(doc.userId) ?? doc.userId,
|
|
214
|
+
externalId: doc.externalId ?? null,
|
|
215
|
+
authIsValid: doc.authIsValid ?? null,
|
|
216
|
+
createdAt: doc.createdAt ?? null,
|
|
217
|
+
updatedAt: doc.updatedAt ?? null,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return credentials;
|
|
221
|
+
}
|
|
222
|
+
|
|
189
223
|
async updateCredential(credentialId, updates) {
|
|
190
224
|
const objectId = toObjectId(credentialId);
|
|
191
225
|
if (!objectId) return null;
|
|
@@ -80,6 +80,21 @@ class CredentialRepositoryInterface {
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* All credentials owned by a user, newest first.
|
|
85
|
+
*
|
|
86
|
+
* @param {string|number} userId - User ID (string from application layer)
|
|
87
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs, `createdAt`
|
|
88
|
+
* and `updatedAt`, and the (decrypted) data fields spread in, as
|
|
89
|
+
* findCredentialById returns them. Empty when the user has none.
|
|
90
|
+
* @abstract
|
|
91
|
+
*/
|
|
92
|
+
async findCredentialsByUserId(userId) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'Method findCredentialsByUserId must be implemented by subclass'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Update a credential by ID
|
|
85
100
|
*
|
|
@@ -188,6 +188,29 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* All credentials owned by a user, newest first.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} userId - User ID
|
|
195
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
196
|
+
*/
|
|
197
|
+
async findCredentialsByUserId(userId) {
|
|
198
|
+
if (!userId) return [];
|
|
199
|
+
const credentials = await this.prisma.credential.findMany({
|
|
200
|
+
where: { userId: String(userId) },
|
|
201
|
+
orderBy: { createdAt: 'desc' },
|
|
202
|
+
});
|
|
203
|
+
return credentials.map((credential) => ({
|
|
204
|
+
...(credential.data || {}),
|
|
205
|
+
id: credential.id,
|
|
206
|
+
userId: credential.userId,
|
|
207
|
+
externalId: credential.externalId,
|
|
208
|
+
authIsValid: credential.authIsValid,
|
|
209
|
+
createdAt: credential.createdAt,
|
|
210
|
+
updatedAt: credential.updatedAt,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
/**
|
|
192
215
|
* Update a credential by ID
|
|
193
216
|
* Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
|
|
@@ -205,6 +205,34 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* All credentials owned by a user, newest first.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} userId - User ID (string from application layer)
|
|
212
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
213
|
+
*/
|
|
214
|
+
async findCredentialsByUserId(userId) {
|
|
215
|
+
let intUserId;
|
|
216
|
+
try {
|
|
217
|
+
intUserId = this._convertId(userId);
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const credentials = await this.prisma.credential.findMany({
|
|
222
|
+
where: { userId: intUserId },
|
|
223
|
+
orderBy: { createdAt: 'desc' },
|
|
224
|
+
});
|
|
225
|
+
return credentials.map((credential) => ({
|
|
226
|
+
...(credential.data || {}),
|
|
227
|
+
id: credential.id.toString(),
|
|
228
|
+
userId: credential.userId?.toString(),
|
|
229
|
+
externalId: credential.externalId,
|
|
230
|
+
authIsValid: credential.authIsValid,
|
|
231
|
+
createdAt: credential.createdAt,
|
|
232
|
+
updatedAt: credential.updatedAt,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
208
236
|
/**
|
|
209
237
|
* Update a credential by ID
|
|
210
238
|
*
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Management API v2 credential operations. A credential has no module name
|
|
6
|
+
* of its own; its `type` and the entities that use it come from the
|
|
7
|
+
* caller's entities. A missing or foreign credential is a 404.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const credentialIdOf = (entity) => {
|
|
11
|
+
const ref = entity.credential ?? entity.credentialId;
|
|
12
|
+
if (ref === undefined || ref === null) return null;
|
|
13
|
+
if (typeof ref === 'object') {
|
|
14
|
+
const id = ref.id ?? ref._id;
|
|
15
|
+
return id === undefined || id === null ? null : String(id);
|
|
16
|
+
}
|
|
17
|
+
return String(ref);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Adds `type` and `entityIds` from the user's entities to each credential. */
|
|
21
|
+
function withEntityUsage(credentials, entities) {
|
|
22
|
+
const usage = new Map();
|
|
23
|
+
for (const entity of entities || []) {
|
|
24
|
+
const credentialId = credentialIdOf(entity);
|
|
25
|
+
if (!credentialId) continue;
|
|
26
|
+
const entry = usage.get(credentialId) || { type: null, entityIds: [] };
|
|
27
|
+
entry.type = entry.type || entity.moduleName || null;
|
|
28
|
+
entry.entityIds.push(String(entity.id));
|
|
29
|
+
usage.set(credentialId, entry);
|
|
30
|
+
}
|
|
31
|
+
return credentials.map((credential) => {
|
|
32
|
+
const entry = usage.get(String(credential.id)) || {
|
|
33
|
+
type: null,
|
|
34
|
+
entityIds: [],
|
|
35
|
+
};
|
|
36
|
+
return { ...credential, type: entry.type, entityIds: entry.entityIds };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The caller's credentials with their type and entity usage. */
|
|
41
|
+
class ListCredentialsForUser {
|
|
42
|
+
constructor({ credentialRepository, moduleRepository }) {
|
|
43
|
+
this.credentialRepository = credentialRepository;
|
|
44
|
+
this.moduleRepository = moduleRepository;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async execute(user) {
|
|
48
|
+
const userId = user.getId();
|
|
49
|
+
const [credentials, entities] = await Promise.all([
|
|
50
|
+
this.credentialRepository.findCredentialsByUserId(userId),
|
|
51
|
+
this.moduleRepository.findEntitiesByUserId(userId),
|
|
52
|
+
]);
|
|
53
|
+
return withEntityUsage(credentials || [], entities);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One credential the caller owns, with its type and entity usage. */
|
|
58
|
+
class GetCredentialForUserV2 {
|
|
59
|
+
constructor({ credentialRepository, moduleRepository }) {
|
|
60
|
+
this.credentialRepository = credentialRepository;
|
|
61
|
+
this.moduleRepository = moduleRepository;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async execute(credentialId, user) {
|
|
65
|
+
let credential = null;
|
|
66
|
+
try {
|
|
67
|
+
credential = await this.credentialRepository.findCredentialById(
|
|
68
|
+
credentialId
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!credential || !userOwns(user, credential.userId)) {
|
|
76
|
+
throw Boom.notFound(`Credential ${credentialId} not found`, {
|
|
77
|
+
code: 'CREDENTIAL_NOT_FOUND',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const entities = await this.moduleRepository.findEntitiesByUserId(
|
|
81
|
+
credential.userId
|
|
82
|
+
);
|
|
83
|
+
return withEntityUsage([credential], entities)[0];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Deletes a credential the caller owns. The entities that used it stay, with
|
|
89
|
+
* their credential unset, so they show as needing re-authorization rather
|
|
90
|
+
* than disappearing from integrations.
|
|
91
|
+
*/
|
|
92
|
+
class DeleteCredentialForUser {
|
|
93
|
+
constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
|
|
94
|
+
this.credentialRepository = credentialRepository;
|
|
95
|
+
this.moduleRepository = moduleRepository;
|
|
96
|
+
this.getCredentialForUser = getCredentialForUser;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async execute(credentialId, user) {
|
|
100
|
+
const credential = await this.getCredentialForUser.execute(
|
|
101
|
+
credentialId,
|
|
102
|
+
user
|
|
103
|
+
);
|
|
104
|
+
for (const entityId of credential.entityIds) {
|
|
105
|
+
await this.moduleRepository.unsetCredential(entityId);
|
|
106
|
+
}
|
|
107
|
+
await this.credentialRepository.deleteCredentialById(credential.id);
|
|
108
|
+
return { id: String(credential.id), entityIds: credential.entityIds };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
ListCredentialsForUser,
|
|
114
|
+
GetCredentialForUserV2,
|
|
115
|
+
DeleteCredentialForUser,
|
|
116
|
+
withEntityUsage,
|
|
117
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Re-authorization of a credential the caller owns (Management API v2). It
|
|
5
|
+
* runs the same flow as /api/v2/authorize for the credential's entity type,
|
|
6
|
+
* with the session bound to the credential so it cannot be reused for
|
|
7
|
+
* anything else.
|
|
8
|
+
*/
|
|
9
|
+
async function credentialEntityType(getCredentialForUser, credentialId, user) {
|
|
10
|
+
const credential = await getCredentialForUser.execute(credentialId, user);
|
|
11
|
+
if (!credential.type) {
|
|
12
|
+
throw Boom.conflict(
|
|
13
|
+
`Credential ${credentialId} is not used by any entity, so its type is unknown; authorize a new connection instead`,
|
|
14
|
+
{ code: 'CREDENTIAL_TYPE_UNKNOWN' }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return { entityType: credential.type, credentialId: String(credential.id) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** GET /api/v2/credentials/:credentialId/reauthorize */
|
|
21
|
+
class GetCredentialReauthorization {
|
|
22
|
+
constructor({ getCredentialForUser, getAuthorizationStep }) {
|
|
23
|
+
this.getCredentialForUser = getCredentialForUser;
|
|
24
|
+
this.getAuthorizationStep = getAuthorizationStep;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
|
|
28
|
+
const binding = await credentialEntityType(
|
|
29
|
+
this.getCredentialForUser,
|
|
30
|
+
credentialId,
|
|
31
|
+
user
|
|
32
|
+
);
|
|
33
|
+
return this.getAuthorizationStep.execute({
|
|
34
|
+
user,
|
|
35
|
+
...binding,
|
|
36
|
+
step,
|
|
37
|
+
sessionId,
|
|
38
|
+
state,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** POST /api/v2/credentials/:credentialId/reauthorize */
|
|
44
|
+
class ReauthorizeCredentialForUser {
|
|
45
|
+
constructor({ getCredentialForUser, submitAuthorizationStep }) {
|
|
46
|
+
this.getCredentialForUser = getCredentialForUser;
|
|
47
|
+
this.submitAuthorizationStep = submitAuthorizationStep;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
|
|
51
|
+
const binding = await credentialEntityType(
|
|
52
|
+
this.getCredentialForUser,
|
|
53
|
+
credentialId,
|
|
54
|
+
user
|
|
55
|
+
);
|
|
56
|
+
return this.submitAuthorizationStep.execute({
|
|
57
|
+
user,
|
|
58
|
+
...binding,
|
|
59
|
+
data,
|
|
60
|
+
step,
|
|
61
|
+
sessionId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { GetCredentialReauthorization, ReauthorizeCredentialForUser };
|