@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.
- package/CHANGELOG.md +0 -11
- package/CLAUDE.md +40 -28
- package/README.md +0 -105
- package/application/commands/entity-commands.js +18 -60
- package/application/commands/integration-commands.js +73 -20
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/application/index.js +2 -6
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/config.js +24 -48
- package/database/documentdb-encryption-service.js +22 -9
- package/database/documentdb-utils.js +0 -2
- package/database/encryption/README.md +14 -78
- package/database/encryption/documentdb-encryption-service.md +1 -78
- package/database/encryption/encryption-schema-registry.js +24 -121
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/prisma.js +38 -15
- package/database/use-cases/check-encryption-health-use-case.js +52 -43
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +4 -43
- package/generated/prisma-mongodb/index-browser.js +1 -40
- package/generated/prisma-mongodb/index.d.ts +111 -4739
- package/generated/prisma-mongodb/index.js +4 -43
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -92
- package/generated/prisma-mongodb/wasm.js +4 -43
- package/generated/prisma-postgresql/edge.js +4 -43
- package/generated/prisma-postgresql/index-browser.js +1 -40
- package/generated/prisma-postgresql/index.d.ts +33 -4966
- package/generated/prisma-postgresql/index.js +4 -43
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -87
- package/generated/prisma-postgresql/wasm.js +4 -43
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -128
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +10 -46
- package/handlers/routers/health.js +4 -13
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +3 -65
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +96 -131
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +31 -17
- package/integrations/repositories/integration-repository-mongo.js +129 -60
- package/integrations/repositories/integration-repository-postgres.js +134 -62
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/add-entities-to-integration.js +92 -0
- package/integrations/use-cases/create-integration.js +75 -72
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -7
- package/integrations/use-cases/remove-entities-from-integration.js +70 -0
- package/integrations/utils/assert-entity-ids-input.js +42 -0
- package/integrations/utils/map-integration-dto.js +14 -43
- package/modules/index.js +0 -14
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/requester.js +5 -35
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +4 -10
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/modules/utils/map-module-dto.js +5 -3
- package/modules/utils/to-credential-id.js +14 -0
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -92
- package/prisma-postgresql/schema.prisma +0 -87
- package/queues/index.js +0 -25
- package/types/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-bearer-token.js +7 -2
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/credential/use-cases/credential-operations-for-user.js +0 -202
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
- package/database/encryption/encryption-config.js +0 -219
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -187
- package/extensions/index.js +0 -41
- package/extensions/initialize-app.js +0 -123
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/integration-operations-for-user.js +0 -256
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -151
- package/management-api/meta-router.js +0 -76
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -415
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -264
- package/management-api/route-registry.js +0 -411
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -281
- package/management-api/v2/dto.js +0 -143
- package/management-api/v2/handlers/authorization.js +0 -94
- package/management-api/v2/handlers/credentials.js +0 -33
- package/management-api/v2/handlers/entities.js +0 -77
- package/management-api/v2/handlers/integrations.js +0 -128
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -85
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-scope.js +0 -15
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-bases.js +0 -169
- package/modules/proxy/proxy-errors.js +0 -31
- package/modules/proxy/proxy-path.js +0 -183
- package/modules/proxy/proxy-policy.js +0 -277
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -245
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/entity-operations-for-user.js +0 -162
- package/modules/use-cases/entity-types.js +0 -139
- package/modules/use-cases/execute-entity-proxy-request.js +0 -331
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/modules/use-cases/multi-step-authorization.js +0 -329
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- package/types/modules/index.d.ts +0 -214
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- 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
|
|
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
|
-
};
|