@friggframework/core 2.0.0--canary.680.6bacd26.0 → 2.0.0--canary.663.8de669c.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/application/commands/integration-commands.js +8 -18
- package/application/index.js +2 -6
- 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/encryption/README.md +0 -2
- package/database/encryption/encryption-schema-registry.js +12 -41
- package/database/encryption/field-encryption-service.js +5 -69
- package/generated/prisma-mongodb/edge.js +4 -20
- package/generated/prisma-mongodb/index-browser.js +1 -17
- package/generated/prisma-mongodb/index.d.ts +95 -1543
- package/generated/prisma-mongodb/index.js +4 -20
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -27
- package/generated/prisma-mongodb/wasm.js +4 -20
- package/generated/prisma-postgresql/edge.js +4 -20
- package/generated/prisma-postgresql/index-browser.js +1 -17
- package/generated/prisma-postgresql/index.d.ts +84 -1565
- package/generated/prisma-postgresql/index.js +4 -20
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -26
- package/generated/prisma-postgresql/wasm.js +4 -20
- package/handlers/app-definition-loader.js +8 -19
- 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 +3 -11
- package/handlers/routers/health.js +4 -11
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +2 -4
- package/integrations/index.js +4 -0
- package/integrations/integration-router.js +23 -16
- package/integrations/utils/map-integration-dto.js +15 -1
- package/modules/index.js +0 -6
- package/modules/requester/requester.js +2 -27
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +0 -27
- package/prisma-postgresql/schema.prisma +0 -26
- package/types/index.d.ts +0 -1
- package/credential/use-cases/credential-operations-for-user.js +0 -117
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/integrations/use-cases/integration-operations-for-user.js +0 -225
- 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 -82
- package/management-api/meta-router.js +0 -57
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -368
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -252
- package/management-api/route-registry.js +0 -294
- 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 -256
- package/management-api/v2/dto.js +0 -123
- package/management-api/v2/handlers/authorization.js +0 -93
- package/management-api/v2/handlers/credentials.js +0 -29
- package/management-api/v2/handlers/entities.js +0 -74
- package/management-api/v2/handlers/integrations.js +0 -125
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -54
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-errors.js +0 -20
- package/modules/proxy/proxy-path.js +0 -179
- package/modules/proxy/proxy-policy.js +0 -248
- 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/requester/proxy-request.js +0 -244
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/use-cases/entity-operations-for-user.js +0 -125
- package/modules/use-cases/entity-types.js +0 -115
- package/modules/use-cases/execute-entity-proxy-request.js +0 -239
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -86
- package/modules/use-cases/multi-step-authorization.js +0 -302
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/types/modules/index.d.ts +0 -58
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const { maskCredential } = require('../dto');
|
|
2
|
-
|
|
3
|
-
/** Management API v2 credential handlers, keyed by operationId. */
|
|
4
|
-
function createCredentialHandlers(useCases) {
|
|
5
|
-
const { listCredentialsForUser, getCredentialForUser, deleteCredentialForUser } =
|
|
6
|
-
useCases;
|
|
7
|
-
|
|
8
|
-
return {
|
|
9
|
-
async listCredentials(_req, res, user) {
|
|
10
|
-
const credentials = await listCredentialsForUser.execute(user);
|
|
11
|
-
res.json({ credentials: credentials.map(maskCredential) });
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
async getCredential(req, res, user) {
|
|
15
|
-
res.json(
|
|
16
|
-
maskCredential(
|
|
17
|
-
await getCredentialForUser.execute(req.params.credentialId, user)
|
|
18
|
-
)
|
|
19
|
-
);
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
async deleteCredential(req, res, user) {
|
|
23
|
-
await deleteCredentialForUser.execute(req.params.credentialId, user);
|
|
24
|
-
res.status(204).end();
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
module.exports = { createCredentialHandlers };
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
const { toEntityDto } = require('../dto');
|
|
2
|
-
const { objectBody, parseStep } = require('../request');
|
|
3
|
-
|
|
4
|
-
/** Management API v2 entity and entity-type handlers, keyed by operationId. */
|
|
5
|
-
function createEntityHandlers(useCases) {
|
|
6
|
-
const {
|
|
7
|
-
listEntitiesForUser,
|
|
8
|
-
getEntityForUser,
|
|
9
|
-
deleteEntityForUser,
|
|
10
|
-
testEntityAuthForUser,
|
|
11
|
-
getEntityOptionsForUser,
|
|
12
|
-
refreshEntityOptionsForUser,
|
|
13
|
-
listEntityTypes,
|
|
14
|
-
getEntityType,
|
|
15
|
-
getAuthorizationRequirements,
|
|
16
|
-
} = useCases;
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
async listEntities(_req, res, user) {
|
|
20
|
-
const entities = await listEntitiesForUser.execute(user);
|
|
21
|
-
res.json({ entities: entities.map(toEntityDto) });
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
async listEntityTypes(_req, res) {
|
|
25
|
-
res.json({ types: await listEntityTypes.execute() });
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
async getEntityType(req, res) {
|
|
29
|
-
res.json(await getEntityType.execute(req.params.entityType));
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
// Describes a step's requirements without starting an authorization
|
|
33
|
-
// session; GET /api/v2/authorize starts one.
|
|
34
|
-
async getEntityTypeRequirements(req, res, user) {
|
|
35
|
-
res.json(
|
|
36
|
-
await getAuthorizationRequirements.execute({
|
|
37
|
-
entityType: req.params.entityType,
|
|
38
|
-
step: parseStep(req.query.step),
|
|
39
|
-
userId: user.getId(),
|
|
40
|
-
})
|
|
41
|
-
);
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
async getEntity(req, res, user) {
|
|
45
|
-
res.json(toEntityDto(await getEntityForUser.execute(req.params.entityId, user)));
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
async deleteEntity(req, res, user) {
|
|
49
|
-
await deleteEntityForUser.execute(req.params.entityId, user);
|
|
50
|
-
res.status(204).end();
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async testEntityAuth(req, res, user) {
|
|
54
|
-
const ok = await testEntityAuthForUser.execute(req.params.entityId, user);
|
|
55
|
-
res.json({ status: ok ? 'ok' : 'failed' });
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
async getEntityOptions(req, res, user) {
|
|
59
|
-
res.json(await getEntityOptionsForUser.execute(req.params.entityId, user));
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
async refreshEntityOptions(req, res, user) {
|
|
63
|
-
res.json(
|
|
64
|
-
await refreshEntityOptionsForUser.execute(
|
|
65
|
-
req.params.entityId,
|
|
66
|
-
user,
|
|
67
|
-
objectBody(req)
|
|
68
|
-
)
|
|
69
|
-
);
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
module.exports = { createEntityHandlers };
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
const { toIntegrationDto } = require('../dto');
|
|
2
|
-
const { objectBody, optionalString } = require('../request');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Management API v2 integration handlers, keyed by registry operationId.
|
|
6
|
-
* Each takes (req, res, user) after authentication and only calls use cases.
|
|
7
|
-
*/
|
|
8
|
-
function createIntegrationHandlers(useCases) {
|
|
9
|
-
const {
|
|
10
|
-
getIntegrationsForUser,
|
|
11
|
-
getPossibleIntegrations,
|
|
12
|
-
createIntegrationForUser,
|
|
13
|
-
getIntegrationInstanceForUser,
|
|
14
|
-
updateIntegrationForUser,
|
|
15
|
-
removeIntegrationForUser,
|
|
16
|
-
sendIntegrationEventForUser,
|
|
17
|
-
runIntegrationActionForUser,
|
|
18
|
-
testIntegrationAuthForUser,
|
|
19
|
-
} = useCases;
|
|
20
|
-
|
|
21
|
-
const send = (req, user, event, payload) =>
|
|
22
|
-
sendIntegrationEventForUser.execute(
|
|
23
|
-
req.params.integrationId,
|
|
24
|
-
user,
|
|
25
|
-
event,
|
|
26
|
-
payload
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
async listIntegrations(_req, res, user) {
|
|
31
|
-
const integrations = await getIntegrationsForUser.execute(user.getId());
|
|
32
|
-
res.json({ integrations: integrations.map(toIntegrationDto) });
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
async listIntegrationOptions(_req, res) {
|
|
36
|
-
res.json({ integrations: await getPossibleIntegrations.execute() });
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
async createIntegration(req, res, user) {
|
|
40
|
-
const { entities, config } = objectBody(req);
|
|
41
|
-
const integration = await createIntegrationForUser.execute(
|
|
42
|
-
{ entities, config },
|
|
43
|
-
user
|
|
44
|
-
);
|
|
45
|
-
res.status(201).json(toIntegrationDto(integration));
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
async getIntegration(req, res, user) {
|
|
49
|
-
const instance = await getIntegrationInstanceForUser.execute(
|
|
50
|
-
req.params.integrationId,
|
|
51
|
-
user
|
|
52
|
-
);
|
|
53
|
-
res.json(toIntegrationDto(instance));
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
async updateIntegration(req, res, user) {
|
|
57
|
-
const { config } = objectBody(req);
|
|
58
|
-
const integration = await updateIntegrationForUser.execute(
|
|
59
|
-
req.params.integrationId,
|
|
60
|
-
user,
|
|
61
|
-
config
|
|
62
|
-
);
|
|
63
|
-
res.json(toIntegrationDto(integration));
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
async deleteIntegration(req, res, user) {
|
|
67
|
-
await removeIntegrationForUser.execute(req.params.integrationId, user);
|
|
68
|
-
res.status(204).end();
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
async getIntegrationConfigOptions(req, res, user) {
|
|
72
|
-
res.json(await send(req, user, 'GET_CONFIG_OPTIONS'));
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
async refreshIntegrationConfigOptions(req, res, user) {
|
|
76
|
-
res.json(await send(req, user, 'REFRESH_CONFIG_OPTIONS', objectBody(req)));
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
async listIntegrationActions(req, res, user) {
|
|
80
|
-
const actionType = optionalString(req.query.actionType, 'actionType');
|
|
81
|
-
res.json(
|
|
82
|
-
await send(req, user, 'GET_USER_ACTIONS', actionType ? { actionType } : {})
|
|
83
|
-
);
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
async getIntegrationActionOptions(req, res, user) {
|
|
87
|
-
res.json(
|
|
88
|
-
await send(req, user, 'GET_USER_ACTION_OPTIONS', {
|
|
89
|
-
actionId: req.params.actionId,
|
|
90
|
-
data: objectBody(req),
|
|
91
|
-
})
|
|
92
|
-
);
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
async refreshIntegrationActionOptions(req, res, user) {
|
|
96
|
-
res.json(
|
|
97
|
-
await send(req, user, 'REFRESH_USER_ACTION_OPTIONS', {
|
|
98
|
-
actionId: req.params.actionId,
|
|
99
|
-
data: objectBody(req),
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
async runIntegrationAction(req, res, user) {
|
|
105
|
-
res.json(
|
|
106
|
-
await runIntegrationActionForUser.execute(
|
|
107
|
-
req.params.integrationId,
|
|
108
|
-
user,
|
|
109
|
-
req.params.actionId,
|
|
110
|
-
objectBody(req)
|
|
111
|
-
)
|
|
112
|
-
);
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
async testIntegrationAuth(req, res, user) {
|
|
116
|
-
const { ok, errors } = await testIntegrationAuthForUser.execute(
|
|
117
|
-
req.params.integrationId,
|
|
118
|
-
user
|
|
119
|
-
);
|
|
120
|
-
res.json(ok ? { status: 'ok' } : { status: 'failed', errors });
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
module.exports = { createIntegrationHandlers };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const { toV2ErrorBody } = require('../../error-response');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* POST /api/v2/entities/:entityId/proxy (ADR-052). The HTTP status follows the
|
|
5
|
-
* upstream status. Proxy failures answer in the api-proxy.schema.json error
|
|
6
|
-
* shape, which is also the v2 error shape plus `success` and `status`.
|
|
7
|
-
*/
|
|
8
|
-
function createProxyHandlers({ executeEntityProxyRequest }) {
|
|
9
|
-
return {
|
|
10
|
-
async proxyEntityRequest(req, res, user) {
|
|
11
|
-
try {
|
|
12
|
-
const result = await executeEntityProxyRequest.execute(
|
|
13
|
-
req.params.entityId,
|
|
14
|
-
user,
|
|
15
|
-
req.body
|
|
16
|
-
);
|
|
17
|
-
res.status(result.status).json(result.body);
|
|
18
|
-
} catch (error) {
|
|
19
|
-
if (!error?.isBoom || !error.data?.proxy) throw error;
|
|
20
|
-
const status = error.output.statusCode;
|
|
21
|
-
if (error.data.retryAfter) {
|
|
22
|
-
res.set('Retry-After', String(error.data.retryAfter));
|
|
23
|
-
}
|
|
24
|
-
res.status(status).json({
|
|
25
|
-
success: false,
|
|
26
|
-
status,
|
|
27
|
-
...toV2ErrorBody(error),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
module.exports = { createProxyHandlers };
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
|
|
3
|
-
/** The JSON body as an object, or a 400 VALIDATION_ERROR. */
|
|
4
|
-
function objectBody(req) {
|
|
5
|
-
const body = req.body;
|
|
6
|
-
if (body === undefined || body === null || body === '') return {};
|
|
7
|
-
if (typeof body !== 'object' || Array.isArray(body)) {
|
|
8
|
-
throw Boom.badRequest('Request body must be a JSON object', {
|
|
9
|
-
code: 'VALIDATION_ERROR',
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
return body;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Fields that must be present (non-empty) in the body. */
|
|
16
|
-
function requireFields(body, fields) {
|
|
17
|
-
const missing = fields.filter(
|
|
18
|
-
(field) =>
|
|
19
|
-
body[field] === undefined || body[field] === null || body[field] === ''
|
|
20
|
-
);
|
|
21
|
-
if (missing.length) {
|
|
22
|
-
throw Boom.badRequest(
|
|
23
|
-
`Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
|
|
24
|
-
{ code: 'VALIDATION_ERROR', details: { missing } }
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
return body;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** A 1-based step number from a query or body value; 1 when absent. */
|
|
31
|
-
function parseStep(value) {
|
|
32
|
-
if (value === undefined || value === null || value === '') return 1;
|
|
33
|
-
const step = Number(value);
|
|
34
|
-
if (!Number.isInteger(step) || step < 1) {
|
|
35
|
-
throw Boom.badRequest('step must be a positive integer', {
|
|
36
|
-
code: 'INVALID_STEP',
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return step;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** An optional string query/body value. */
|
|
43
|
-
function optionalString(value, name) {
|
|
44
|
-
if (value === undefined || value === null || value === '') return undefined;
|
|
45
|
-
if (typeof value !== 'string') {
|
|
46
|
-
throw Boom.badRequest(`${name} must be a string`, {
|
|
47
|
-
code: 'VALIDATION_ERROR',
|
|
48
|
-
details: { field: name },
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
module.exports = { objectBody, requireFields, parseStep, optionalString };
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-memory doubles for every Management API v2 dependency. Each use case is
|
|
5
|
-
* a jest.fn() so a test can assert on calls or override one behaviour.
|
|
6
|
-
*/
|
|
7
|
-
function fakeV2Dependencies(overrides = {}) {
|
|
8
|
-
const user = { getId: () => 'user-1' };
|
|
9
|
-
const notFound = (code) => async () => {
|
|
10
|
-
throw Boom.notFound('not found', { code });
|
|
11
|
-
};
|
|
12
|
-
const fn = (impl) => ({ execute: jest.fn(impl) });
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
user,
|
|
16
|
-
config: { v1: true, proxy: { enable: false } },
|
|
17
|
-
authenticateUser: {
|
|
18
|
-
execute: jest.fn(async (req) => {
|
|
19
|
-
if (!req.headers.authorization) {
|
|
20
|
-
throw Boom.unauthorized('No valid authentication provided');
|
|
21
|
-
}
|
|
22
|
-
return user;
|
|
23
|
-
}),
|
|
24
|
-
},
|
|
25
|
-
getIntegrationsForUser: fn(async () => [
|
|
26
|
-
{ id: 'i1', entities: ['e1'], status: 'ENABLED', config: { type: 'sync' }, userActions: {} },
|
|
27
|
-
]),
|
|
28
|
-
getPossibleIntegrations: fn(async () => [{ type: 'sync', display: {} }]),
|
|
29
|
-
createIntegrationForUser: fn(async () => ({
|
|
30
|
-
id: 'i2', entities: ['e1'], status: 'ENABLED', config: { type: 'sync' },
|
|
31
|
-
})),
|
|
32
|
-
getIntegrationInstanceForUser: fn(async (id) => {
|
|
33
|
-
if (id !== 'i1') return notFound('INTEGRATION_NOT_FOUND')();
|
|
34
|
-
return { id: 'i1', entities: ['e1'], status: 'ENABLED', config: { type: 'sync' } };
|
|
35
|
-
}),
|
|
36
|
-
updateIntegrationForUser: fn(async (id, _user, config) => ({
|
|
37
|
-
id, entities: ['e1'], status: 'ENABLED', config,
|
|
38
|
-
})),
|
|
39
|
-
removeIntegrationForUser: fn(async () => undefined),
|
|
40
|
-
sendIntegrationEventForUser: fn(async (_id, _user, event, payload) => ({ event, payload })),
|
|
41
|
-
runIntegrationActionForUser: fn(async (_id, _user, actionId, payload) => ({ ran: actionId, payload })),
|
|
42
|
-
testIntegrationAuthForUser: fn(async () => ({ ok: true, errors: [] })),
|
|
43
|
-
|
|
44
|
-
listEntitiesForUser: fn(async () => [
|
|
45
|
-
{ id: 'e1', userId: 'user-1', moduleName: 'acme', name: 'Acme', externalId: 'x1', credential: { id: 'c1', authIsValid: true, access_token: 'secret' } },
|
|
46
|
-
]),
|
|
47
|
-
getEntityForUser: fn(async (id) => {
|
|
48
|
-
if (id !== 'e1') return notFound('ENTITY_NOT_FOUND')();
|
|
49
|
-
return { id: 'e1', userId: 'user-1', moduleName: 'acme', name: 'Acme', externalId: 'x1', credential: { id: 'c1', authIsValid: true, access_token: 'secret' } };
|
|
50
|
-
}),
|
|
51
|
-
deleteEntityForUser: fn(async () => undefined),
|
|
52
|
-
testEntityAuthForUser: fn(async () => true),
|
|
53
|
-
getEntityOptionsForUser: fn(async () => ({ jsonSchema: {} })),
|
|
54
|
-
refreshEntityOptionsForUser: fn(async (_id, _user, body) => ({ refreshed: body })),
|
|
55
|
-
listEntityTypes: fn(async () => [{ type: 'acme', name: 'Acme' }]),
|
|
56
|
-
getEntityType: fn(async (type) => {
|
|
57
|
-
if (type !== 'acme') return notFound('ENTITY_TYPE_NOT_FOUND')();
|
|
58
|
-
return { type: 'acme', name: 'Acme' };
|
|
59
|
-
}),
|
|
60
|
-
getAuthorizationRequirements: fn(async ({ step }) => ({
|
|
61
|
-
type: 'oauth2', data: { url: 'https://provider.example/authorize' }, step, totalSteps: 1, isMultiStep: false,
|
|
62
|
-
})),
|
|
63
|
-
listCredentialsForUser: fn(async () => [
|
|
64
|
-
{ id: 'c1', userId: 'user-1', type: 'acme', entityIds: ['e1'], authIsValid: true, access_token: 'ya29.secret-token-value-1234' },
|
|
65
|
-
]),
|
|
66
|
-
getCredentialForUser: fn(async (id) => {
|
|
67
|
-
if (id !== 'c1') return notFound('CREDENTIAL_NOT_FOUND')();
|
|
68
|
-
return { id: 'c1', userId: 'user-1', type: 'acme', entityIds: ['e1'], authIsValid: true, api_key: 'sk_live_abcdefghijkl9876' };
|
|
69
|
-
}),
|
|
70
|
-
deleteCredentialForUser: fn(async () => ({ id: 'c1', entityIds: ['e1'] })),
|
|
71
|
-
getAuthorizationStep: fn(async ({ entityType, step }) => {
|
|
72
|
-
if (entityType === 'nope') return notFound('ENTITY_TYPE_NOT_FOUND')();
|
|
73
|
-
return { type: 'email', data: {}, step, totalSteps: 2, isMultiStep: true, sessionId: 'sess-1' };
|
|
74
|
-
}),
|
|
75
|
-
submitAuthorizationStep: fn(async ({ step }) =>
|
|
76
|
-
step === 1
|
|
77
|
-
? { status: 'pending', step: 2, totalSteps: 2, sessionId: 'sess-1', requirements: { type: 'otp' } }
|
|
78
|
-
: {
|
|
79
|
-
status: 'complete',
|
|
80
|
-
entity: { id: 'e1', userId: 'user-1', moduleName: 'acme', credential: { id: 'c1', authIsValid: true } },
|
|
81
|
-
credential: { id: 'c1', userId: 'user-1', type: 'acme', entityIds: ['e1'], authIsValid: true, access_token: 'ya29.brand-new-token-0001' },
|
|
82
|
-
}
|
|
83
|
-
),
|
|
84
|
-
getCredentialReauthorization: fn(async (id, _user, { step }) => ({ type: 'oauth2', data: {}, step, totalSteps: 1 })),
|
|
85
|
-
reauthorizeCredentialForUser: fn(async () => ({
|
|
86
|
-
status: 'complete',
|
|
87
|
-
entity: { id: 'e1', userId: 'user-1', moduleName: 'acme' },
|
|
88
|
-
credential: { id: 'c2', userId: 'user-1', type: 'acme', entityIds: ['e1'], authIsValid: true },
|
|
89
|
-
previousCredentialId: 'c1',
|
|
90
|
-
})),
|
|
91
|
-
executeEntityProxyRequest: fn(async (entityId, _user, request) => {
|
|
92
|
-
if (request?.path === '/limited') {
|
|
93
|
-
throw Boom.tooManyRequests('The upstream API is rate limiting this account', {
|
|
94
|
-
code: 'RATE_LIMITED', proxy: true, expose: true, retryAfter: '30', details: { upstreamStatus: 429 },
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return { status: 201, body: { success: true, status: 201, headers: {}, data: { id: 'new' } } };
|
|
98
|
-
}),
|
|
99
|
-
...overrides,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
module.exports = { fakeV2Dependencies };
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
const crypto = require('node:crypto');
|
|
2
|
-
|
|
3
|
-
const DEFAULT_TTL_MINUTES = 15;
|
|
4
|
-
|
|
5
|
-
function sessionTtlMs() {
|
|
6
|
-
const minutes = parseInt(process.env.AUTH_SESSION_EXPIRY_MINUTES || '', 10);
|
|
7
|
-
return (Number.isInteger(minutes) && minutes > 0 ? minutes : DEFAULT_TTL_MINUTES) * 60 * 1000;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* State of one multi-step authorization (Management API v2
|
|
12
|
-
* `/api/v2/authorize` and `/api/v2/credentials/:id/reauthorize`).
|
|
13
|
-
*
|
|
14
|
-
* `stepData` holds what earlier steps collected (for example the email an OTP
|
|
15
|
-
* was sent to) and is encrypted at rest (encryption-schema-registry). A
|
|
16
|
-
* session is bound to the user who started it, the entity type, and, for a
|
|
17
|
-
* re-authorization, the credential being renewed.
|
|
18
|
-
*/
|
|
19
|
-
class AuthorizationSession {
|
|
20
|
-
/**
|
|
21
|
-
* @param {Object} params
|
|
22
|
-
* @param {string} params.sessionId - unguessable id (UUID v4)
|
|
23
|
-
* @param {string} params.userId
|
|
24
|
-
* @param {string} params.entityType - module name
|
|
25
|
-
* @param {string|null} [params.credentialId] - set for re-authorization
|
|
26
|
-
* @param {number} [params.currentStep=1] - the step the caller submits next
|
|
27
|
-
* @param {number} params.maxSteps
|
|
28
|
-
* @param {Object} [params.stepData={}]
|
|
29
|
-
* @param {Date} params.expiresAt
|
|
30
|
-
* @param {boolean} [params.completed=false]
|
|
31
|
-
* @param {Date} [params.createdAt]
|
|
32
|
-
* @param {Date} [params.updatedAt]
|
|
33
|
-
*/
|
|
34
|
-
constructor({
|
|
35
|
-
sessionId,
|
|
36
|
-
userId,
|
|
37
|
-
entityType,
|
|
38
|
-
credentialId = null,
|
|
39
|
-
currentStep = 1,
|
|
40
|
-
maxSteps,
|
|
41
|
-
stepData = {},
|
|
42
|
-
expiresAt,
|
|
43
|
-
completed = false,
|
|
44
|
-
createdAt = new Date(),
|
|
45
|
-
updatedAt = new Date(),
|
|
46
|
-
}) {
|
|
47
|
-
this.sessionId = sessionId;
|
|
48
|
-
this.userId = userId === undefined || userId === null ? userId : String(userId);
|
|
49
|
-
this.entityType = entityType;
|
|
50
|
-
this.credentialId =
|
|
51
|
-
credentialId === undefined || credentialId === null ? null : String(credentialId);
|
|
52
|
-
this.currentStep = currentStep;
|
|
53
|
-
this.maxSteps = maxSteps;
|
|
54
|
-
this.stepData = stepData && typeof stepData === 'object' ? stepData : {};
|
|
55
|
-
this.expiresAt = expiresAt instanceof Date ? expiresAt : new Date(expiresAt);
|
|
56
|
-
this.completed = completed;
|
|
57
|
-
this.createdAt = createdAt;
|
|
58
|
-
this.updatedAt = updatedAt;
|
|
59
|
-
this.validate();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* A new session for step 1, expiring after AUTH_SESSION_EXPIRY_MINUTES
|
|
64
|
-
* (default 15).
|
|
65
|
-
*/
|
|
66
|
-
static start({ userId, entityType, maxSteps, credentialId = null, now = new Date() }) {
|
|
67
|
-
return new AuthorizationSession({
|
|
68
|
-
sessionId: crypto.randomUUID(),
|
|
69
|
-
userId,
|
|
70
|
-
entityType,
|
|
71
|
-
credentialId,
|
|
72
|
-
currentStep: 1,
|
|
73
|
-
maxSteps,
|
|
74
|
-
stepData: {},
|
|
75
|
-
expiresAt: new Date(now.getTime() + sessionTtlMs()),
|
|
76
|
-
completed: false,
|
|
77
|
-
createdAt: now,
|
|
78
|
-
updatedAt: now,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
validate() {
|
|
83
|
-
if (!this.sessionId) throw new Error('sessionId is required');
|
|
84
|
-
if (!this.userId) throw new Error('userId is required');
|
|
85
|
-
if (!this.entityType) throw new Error('entityType is required');
|
|
86
|
-
if (!Number.isInteger(this.maxSteps) || this.maxSteps < 1) {
|
|
87
|
-
throw new Error('maxSteps must be a positive integer');
|
|
88
|
-
}
|
|
89
|
-
if (
|
|
90
|
-
!Number.isInteger(this.currentStep) ||
|
|
91
|
-
this.currentStep < 1 ||
|
|
92
|
-
this.currentStep > this.maxSteps
|
|
93
|
-
) {
|
|
94
|
-
throw new Error(`currentStep must be between 1 and ${this.maxSteps}`);
|
|
95
|
-
}
|
|
96
|
-
if (Number.isNaN(this.expiresAt.getTime())) {
|
|
97
|
-
throw new Error('expiresAt must be a valid date');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
isExpired(now = new Date()) {
|
|
102
|
-
return this.expiresAt.getTime() <= now.getTime();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** True when this session may be used by `userId` for `entityType`/`credentialId`. */
|
|
106
|
-
belongsTo({ userId, entityType, credentialId = null }) {
|
|
107
|
-
return (
|
|
108
|
-
this.userId === String(userId) &&
|
|
109
|
-
this.entityType === entityType &&
|
|
110
|
-
this.credentialId === (credentialId === null || credentialId === undefined ? null : String(credentialId))
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Records a finished step and moves to `nextStep`, merging what the step
|
|
116
|
-
* collected into stepData.
|
|
117
|
-
*/
|
|
118
|
-
advanceTo(nextStep, collected = {}) {
|
|
119
|
-
if (this.completed) throw new Error('Cannot advance a completed session');
|
|
120
|
-
if (nextStep !== this.currentStep + 1 || nextStep > this.maxSteps) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Cannot move from step ${this.currentStep} to step ${nextStep} of ${this.maxSteps}`
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
this.currentStep = nextStep;
|
|
126
|
-
this.stepData = { ...this.stepData, ...(collected || {}) };
|
|
127
|
-
this.updatedAt = new Date();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Starts the flow over at step 1, forgetting collected data. */
|
|
131
|
-
restart() {
|
|
132
|
-
this.currentStep = 1;
|
|
133
|
-
this.stepData = {};
|
|
134
|
-
this.completed = false;
|
|
135
|
-
this.updatedAt = new Date();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
markComplete() {
|
|
139
|
-
this.completed = true;
|
|
140
|
-
this.updatedAt = new Date();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
module.exports = { AuthorizationSession, DEFAULT_TTL_MINUTES };
|