@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/application/commands/integration-commands.js +8 -18
  3. package/application/index.js +2 -6
  4. package/credential/repositories/credential-repository-documentdb.js +0 -34
  5. package/credential/repositories/credential-repository-interface.js +0 -15
  6. package/credential/repositories/credential-repository-mongo.js +0 -23
  7. package/credential/repositories/credential-repository-postgres.js +0 -28
  8. package/database/encryption/README.md +0 -2
  9. package/database/encryption/encryption-schema-registry.js +12 -41
  10. package/database/encryption/field-encryption-service.js +5 -69
  11. package/generated/prisma-mongodb/edge.js +4 -20
  12. package/generated/prisma-mongodb/index-browser.js +1 -17
  13. package/generated/prisma-mongodb/index.d.ts +95 -1543
  14. package/generated/prisma-mongodb/index.js +4 -20
  15. package/generated/prisma-mongodb/package.json +1 -1
  16. package/generated/prisma-mongodb/schema.prisma +0 -27
  17. package/generated/prisma-mongodb/wasm.js +4 -20
  18. package/generated/prisma-postgresql/edge.js +4 -20
  19. package/generated/prisma-postgresql/index-browser.js +1 -17
  20. package/generated/prisma-postgresql/index.d.ts +84 -1565
  21. package/generated/prisma-postgresql/index.js +4 -20
  22. package/generated/prisma-postgresql/package.json +1 -1
  23. package/generated/prisma-postgresql/schema.prisma +0 -26
  24. package/generated/prisma-postgresql/wasm.js +4 -20
  25. package/handlers/app-definition-loader.js +8 -19
  26. package/handlers/app-handler-helpers.js +3 -25
  27. package/handlers/backend-utils.js +25 -31
  28. package/handlers/middleware/admin-auth.js +1 -14
  29. package/handlers/routers/auth.js +3 -11
  30. package/handlers/routers/health.js +4 -11
  31. package/handlers/workers/integration-defined-workers.js +2 -5
  32. package/index.js +2 -4
  33. package/integrations/index.js +4 -0
  34. package/integrations/integration-router.js +23 -16
  35. package/integrations/utils/map-integration-dto.js +15 -1
  36. package/modules/index.js +0 -6
  37. package/modules/requester/requester.js +2 -27
  38. package/package.json +5 -5
  39. package/prisma-mongodb/schema.prisma +0 -27
  40. package/prisma-postgresql/schema.prisma +0 -26
  41. package/types/index.d.ts +0 -1
  42. package/credential/use-cases/credential-operations-for-user.js +0 -117
  43. package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
  44. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  45. package/integrations/use-cases/integration-operations-for-user.js +0 -225
  46. package/management-api/create-management-api-router.js +0 -49
  47. package/management-api/error-response.js +0 -116
  48. package/management-api/management-api-config.js +0 -82
  49. package/management-api/meta-router.js +0 -57
  50. package/management-api/openapi/build-openapi.js +0 -144
  51. package/management-api/openapi/components.js +0 -368
  52. package/management-api/openapi/v1-operations.js +0 -83
  53. package/management-api/openapi/v2-operations.js +0 -252
  54. package/management-api/route-registry.js +0 -294
  55. package/management-api/test-support/http-client.js +0 -39
  56. package/management-api/use-cases/get-management-api-meta.js +0 -57
  57. package/management-api/v1-deprecation.js +0 -120
  58. package/management-api/v2/create-v2-router.js +0 -85
  59. package/management-api/v2/dependencies.js +0 -256
  60. package/management-api/v2/dto.js +0 -123
  61. package/management-api/v2/handlers/authorization.js +0 -93
  62. package/management-api/v2/handlers/credentials.js +0 -29
  63. package/management-api/v2/handlers/entities.js +0 -74
  64. package/management-api/v2/handlers/integrations.js +0 -125
  65. package/management-api/v2/handlers/proxy.js +0 -34
  66. package/management-api/v2/request.js +0 -54
  67. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  68. package/modules/domain/authorization-session.js +0 -144
  69. package/modules/entity-type-registry.js +0 -311
  70. package/modules/proxy/proxy-errors.js +0 -20
  71. package/modules/proxy/proxy-path.js +0 -179
  72. package/modules/proxy/proxy-policy.js +0 -248
  73. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  74. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  75. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  76. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  77. package/modules/requester/proxy-request.js +0 -244
  78. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  79. package/modules/use-cases/entity-operations-for-user.js +0 -125
  80. package/modules/use-cases/entity-types.js +0 -115
  81. package/modules/use-cases/execute-entity-proxy-request.js +0 -239
  82. package/modules/use-cases/get-authorization-requirements.js +0 -100
  83. package/modules/use-cases/get-entity-module-for-user.js +0 -86
  84. package/modules/use-cases/multi-step-authorization.js +0 -302
  85. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  86. 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 };